Skip to content

QEC messages

Stim stabilizer circuits are a first-class kernel framework, reachable two ways:

  • Python mode — code that import stim and builds stim.Circuit objects runs through the ordinary parse/execute pipeline (framework detection, last-circuit-in-namespace, params/seed/record_metric all work). This is the power path for parameterized experiments.
  • Raw .stim text — send language: "stim" and the code field is parsed with stim.Circuit(text) directly, never through Python exec (the same source-mode routing Q# uses). This is the interchange path: researchers share bare .stim files constantly.

Simulation uses Stim’s compiled sampler. An explicit integer seed is always honored (seed_honored: true) — with Stim’s documented scope: same seed + same stim version + same machine reproduces identical samples, but results can differ across CPUs (SIMD width) and stim versions. Manifests remain honest either way. Because stabilizer circuits routinely exceed statevector sizes, the result is honest about being a sampler: state_vector, probabilities, and bloch_coords are empty, and measurements keys are measurement-record bitstrings (first recorded measurement rightmost, matching the Python frameworks’ bit order).

Generates one of Stim’s built-in QEC circuits and returns its text. The desktop app writes the result into a real project file — the circuit you run is always a file you can read and edit, never hidden state.

qec_generate.request.json
{
"type": "qec_generate",
"code": "repetition_code:memory",
"distance": 3,
"rounds": 2,
"noise": {
"before_measure_flip_probability": 0.01
}
}
qec_generate.responses.json
[
{
"type": "qec_generated",
"code": "repetition_code:memory",
"distance": 3,
"rounds": 2,
"circuit_text": "R 0 1 2 3 4\nTICK\nCX 0 1 2 3\nTICK\nCX 2 1 4 3\nTICK\nX_ERROR(0.01) 1 3\nMR 1 3\nDETECTOR(1, 0) rec[-2]\nDETECTOR(3, 0) rec[-1]\nTICK\nCX 0 1 2 3\nTICK\nCX 2 1 4 3\nTICK\nX_ERROR(0.01) 1 3\nMR 1 3\nSHIFT_COORDS(0, 1)\nDETECTOR(1, 0) rec[-2] rec[-4]\nDETECTOR(3, 0) rec[-1] rec[-3]\nX_ERROR(0.01) 0 2 4\nM 0 2 4\nDETECTOR(1, 1) rec[-2] rec[-3] rec[-5]\nDETECTOR(3, 1) rec[-1] rec[-2] rec[-4]\nOBSERVABLE_INCLUDE(0) rec[-1]"
}
]

Request fields:

FieldTypeNotes
codestringOne of repetition_code:memory, surface_code:rotated_memory_x, surface_code:rotated_memory_z, surface_code:unrotated_memory_x, surface_code:unrotated_memory_z, color_code:memory_xyz
distanceint ≥ 2Code distance (stim itself rejects invalid combinations, e.g. even distances for surface codes)
roundsint ≥ 1Syndrome-extraction rounds
noiseobject, optionalAny of stim’s generator noise arguments: after_clifford_depolarization, before_round_data_depolarization, before_measure_flip_probability, after_reset_flip_probability — each a probability in [0, 1]

Validation failures answer with an error of code qec_generate_invalid whose message is safe to show verbatim. A kernel without Stim installed answers missing_dependency (install it from the setup wizard’s Research group).

When parse or execute produces a Stim circuit, one extra response follows the snapshot (and precedes the result): the QEC sidecar with detector/observable counts, qubit and detector coordinates, and the Detector Error Model reduced to a graph.

{
"type": "qec_snapshot",
"data": {
"num_qubits": 5, "num_detectors": 4, "num_observables": 1,
"num_ticks": 6,
"coords": { "qubits": [[1.0, 0.0], null, "…"], "detectors": [[1.0, 0.0, 0.0], "…"] },
"dem": {
"nodes": 4,
"edge_count": 5, "boundary_edge_count": 2, "hyperedges_count": 0,
"truncated": false,
"edges": [{ "d1": 0, "d2": 1, "obs": [], "p": 0.01 }],
"boundary_edges": [{ "d": 2, "obs": [0], "p": 0.0066 }]
},
"sample_decode": null
}
}

Reading it honestly:

  • The DEM is extracted with decompose_errors=True, so edges are the matchable components decoders like PyMatching consume. Parallel error mechanisms on the same detector pair are merged with the XOR rule p = p₁(1−p₂) + p₂(1−p₁).
  • Circuits whose errors don’t decompose (non-matchable codes) fall back to the undecomposed model with hyperedges_count > 0 — render the pairwise projection with a badge, don’t pretend it’s a matching graph.
  • Circuits with no valid DEM at all (e.g. a non-deterministic observable) keep their stats but carry dem: null plus a dem_error string.
  • Payload cap: above 5,000 total edges the lists are emptied, truncated: true, and the counts remain — never a silently missing graph. Re-request with a higher cap to render anyway (below).
  • dem_text (present only when truncated: true): the flattened DEM as text (str(dem.flattened())). The frontend parses it client-side (a WASM parser) to render the full graph with no kernel-side edge cap — so arbitrarily large detector graphs render without the kernel serializing a huge edge list. Absent when the graph fits, so the common case pays nothing.
  • coords arrays are index-aligned (entry i = qubit/detector i), null where stim has no coordinates. sample_decode is reserved for the campaign protocol increment.

The same payload is available on demand — with code (stateless, any .stim text or stim-building Python), or without code to reuse the last Stim circuit this connection parsed or executed, optionally at a different DEM cap. That second form is the graph view’s “render anyway” button.

The full loop — generate, execute (watch the sidecar arrive between snapshot and result), then re-request the DEM at a deliberately tiny cap to see truncation:

qec_studio.session.json
[
{
"request": {
"type": "qec_generate",
"code": "repetition_code:memory",
"distance": 3,
"rounds": 2,
"noise": {
"before_measure_flip_probability": 0.01
}
},
"responses": [
{
"type": "qec_generated",
"code": "repetition_code:memory",
"distance": 3,
"rounds": 2,
"circuit_text": "R 0 1 2 3 4\nTICK\nCX 0 1 2 3\nTICK\nCX 2 1 4 3\nTICK\nX_ERROR(0.01) 1 3\nMR 1 3\nDETECTOR(1, 0) rec[-2]\nDETECTOR(3, 0) rec[-1]\nTICK\nCX 0 1 2 3\nTICK\nCX 2 1 4 3\nTICK\nX_ERROR(0.01) 1 3\nMR 1 3\nSHIFT_COORDS(0, 1)\nDETECTOR(1, 0) rec[-2] rec[-4]\nDETECTOR(3, 0) rec[-1] rec[-3]\nX_ERROR(0.01) 0 2 4\nM 0 2 4\nDETECTOR(1, 1) rec[-2] rec[-3] rec[-5]\nDETECTOR(3, 1) rec[-1] rec[-2] rec[-4]\nOBSERVABLE_INCLUDE(0) rec[-1]"
}
]
},
{
"request": {
"type": "execute",
"code": "R 0 1 2 3 4\nTICK\nCX 0 1 2 3\nTICK\nCX 2 1 4 3\nTICK\nX_ERROR(0.01) 1 3\nMR 1 3\nDETECTOR(1, 0) rec[-2]\nDETECTOR(3, 0) rec[-1]\nTICK\nCX 0 1 2 3\nTICK\nCX 2 1 4 3\nTICK\nX_ERROR(0.01) 1 3\nMR 1 3\nSHIFT_COORDS(0, 1)\nDETECTOR(1, 0) rec[-2] rec[-4]\nDETECTOR(3, 0) rec[-1] rec[-3]\nX_ERROR(0.01) 0 2 4\nM 0 2 4\nDETECTOR(1, 1) rec[-2] rec[-3] rec[-5]\nDETECTOR(3, 1) rec[-1] rec[-2] rec[-4]\nOBSERVABLE_INCLUDE(0) rec[-1]",
"shots": 128,
"language": "stim",
"seed": 42
},
"responses": [
{
"type": "snapshot",
"data": {
"framework": "stim",
"qubit_count": 5,
"classical_bit_count": 7,
"depth": 7,
"gates": [
{
"type": "Reset",
"targets": [
0
],
"controls": [],
"params": [],
"layer": 0
},
{
"type": "Reset",
"targets": [
1
],
"controls": [],
"params": [],
"layer": 0
},
{
"type": "Reset",
"targets": [
2
],
"controls": [],
"params": [],
"layer": 0
},
{
"type": "Reset",
"targets": [
3
],
"controls": [],
"params": [],
"layer": 0
},
{
"type": "Reset",
"targets": [
4
],
"controls": [],
"params": [],
"layer": 0
},
{
"type": "CNOT",
"targets": [
1
],
"controls": [
0
],
"params": [],
"layer": 1
},
{
"type": "CNOT",
"targets": [
3
],
"controls": [
2
],
"params": [],
"layer": 1
},
{
"type": "CNOT",
"targets": [
1
],
"controls": [
2
],
"params": [],
"layer": 2
},
{
"type": "CNOT",
"targets": [
3
],
"controls": [
4
],
"params": [],
"layer": 2
},
{
"type": "NOISE:X_ERROR",
"targets": [
1
],
"controls": [],
"params": [
0.01
],
"layer": 3
},
{
"type": "NOISE:X_ERROR",
"targets": [
3
],
"controls": [],
"params": [
0.01
],
"layer": 3
},
{
"type": "MR",
"targets": [
1
],
"controls": [],
"params": [],
"layer": 3
},
{
"type": "MR",
"targets": [
3
],
"controls": [],
"params": [],
"layer": 3
},
{
"type": "DETECTOR",
"targets": [],
"controls": [],
"params": [
1.0,
0.0
],
"layer": 3
},
{
"type": "DETECTOR",
"targets": [],
"controls": [],
"params": [
3.0,
0.0
],
"layer": 3
},
{
"type": "CNOT",
"targets": [
1
],
"controls": [
0
],
"params": [],
"layer": 4
},
{
"type": "CNOT",
"targets": [
3
],
"controls": [
2
],
"params": [],
"layer": 4
},
{
"type": "CNOT",
"targets": [
1
],
"controls": [
2
],
"params": [],
"layer": 5
},
{
"type": "CNOT",
"targets": [
3
],
"controls": [
4
],
"params": [],
"layer": 5
},
{
"type": "NOISE:X_ERROR",
"targets": [
1
],
"controls": [],
"params": [
0.01
],
"layer": 6
},
{
"type": "NOISE:X_ERROR",
"targets": [
3
],
"controls": [],
"params": [
0.01
],
"layer": 6
},
{
"type": "MR",
"targets": [
1
],
"controls": [],
"params": [],
"layer": 6
},
{
"type": "MR",
"targets": [
3
],
"controls": [],
"params": [],
"layer": 6
},
{
"type": "DETECTOR",
"targets": [],
"controls": [],
"params": [
1.0,
1.0
],
"layer": 6
},
{
"type": "DETECTOR",
"targets": [],
"controls": [],
"params": [
3.0,
1.0
],
"layer": 6
},
{
"type": "NOISE:X_ERROR",
"targets": [
0
],
"controls": [],
"params": [
0.01
],
"layer": 6
},
{
"type": "NOISE:X_ERROR",
"targets": [
2
],
"controls": [],
"params": [
0.01
],
"layer": 6
},
{
"type": "NOISE:X_ERROR",
"targets": [
4
],
"controls": [],
"params": [
0.01
],
"layer": 6
},
{
"type": "Measure",
"targets": [
0
],
"controls": [],
"params": [],
"layer": 6
},
{
"type": "Measure",
"targets": [
2
],
"controls": [],
"params": [],
"layer": 6
},
{
"type": "Measure",
"targets": [
4
],
"controls": [],
"params": [],
"layer": 6
},
{
"type": "DETECTOR",
"targets": [],
"controls": [],
"params": [
1.0,
2.0
],
"layer": 6
},
{
"type": "DETECTOR",
"targets": [],
"controls": [],
"params": [
3.0,
2.0
],
"layer": 6
},
{
"type": "OBSERVABLE",
"targets": [],
"controls": [],
"params": [
0.0
],
"layer": 6
}
]
}
},
{
"type": "qec_snapshot",
"data": {
"num_qubits": 5,
"num_detectors": 6,
"num_observables": 1,
"num_ticks": 6,
"coords": {
"qubits": [
null,
null,
null,
null,
null
],
"detectors": [
[
1.0,
0.0,
0.0
],
[
3.0,
0.0,
0.0
],
[
1.0,
1.0,
0.0
],
[
3.0,
1.0,
0.0
],
[
1.0,
2.0,
0.0
],
[
3.0,
2.0,
0.0
]
]
},
"dem": {
"nodes": 6,
"edge_count": 5,
"boundary_edge_count": 2,
"hyperedges_count": 0,
"truncated": false,
"edges": [
{
"d1": 0,
"d2": 2,
"obs": [],
"p": 0.01
},
{
"d1": 1,
"d2": 3,
"obs": [],
"p": 0.01
},
{
"d1": 2,
"d2": 4,
"obs": [],
"p": 0.01
},
{
"d1": 3,
"d2": 5,
"obs": [],
"p": 0.01
},
{
"d1": 4,
"d2": 5,
"obs": [],
"p": 0.01
}
],
"boundary_edges": [
{
"d": 4,
"obs": [],
"p": 0.01
},
{
"d": 5,
"obs": [
0
],
"p": 0.01
}
]
},
"sample_decode": null
}
},
{
"type": "result",
"data": {
"state_vector": [],
"probabilities": {},
"measurements": "<any>",
"bloch_coords": [],
"execution_time_ms": "<any>",
"shot_count": 128,
"metrics": {},
"seed_honored": true
}
}
]
},
{
"request": {
"type": "qec_snapshot",
"max_edges": 5
},
"responses": [
{
"type": "qec_snapshot",
"data": {
"num_qubits": 5,
"num_detectors": 6,
"num_observables": 1,
"num_ticks": 6,
"coords": {
"qubits": [
null,
null,
null,
null,
null
],
"detectors": [
[
1.0,
0.0,
0.0
],
[
3.0,
0.0,
0.0
],
[
1.0,
1.0,
0.0
],
[
3.0,
1.0,
0.0
],
[
1.0,
2.0,
0.0
],
[
3.0,
2.0,
0.0
]
]
},
"dem": {
"nodes": 6,
"edge_count": 5,
"boundary_edge_count": 2,
"hyperedges_count": 0,
"truncated": true,
"edges": [],
"boundary_edges": []
},
"sample_decode": null,
"dem_text": "<any>"
}
}
]
}
]

Azure Quantum Resource Estimator over a circuit, wrapping the estimator that ships in qdk. language: "qsharp" compiles the Q# source and estimates its entry expression; language: "qasm3" estimates OpenQASM 3 (e.g. qiskit.qasm3.dumps of the active qiskit circuit). options carries a qubit model preset, a QEC scheme, and an error budget; the reply’s data leads with the headline numbers (physical qubits, runtime, code distance, T-factory count) plus the estimator’s pre-formatted strings and the full document for export. Available for any framework — not just stim.

qec_estimate.request.json
{
"type": "qec_estimate",
"language": "qsharp",
"code": "operation Main() : Result {\n use qs = Qubit[2];\n H(qs[0]);\n CNOT(qs[0], qs[1]);\n T(qs[0]);\n let r = M(qs[0]);\n ResetAll(qs);\n return r;\n}\n",
"options": {
"qubit_params": "qubit_gate_ns_e3",
"qec_scheme": "surface_code",
"error_budget": 0.001
}
}
qec_estimate.responses.json
[
{
"type": "qec_estimate_result",
"data": {
"physical_qubits": "<any>",
"runtime_ns": "<any>",
"rqops": "<any>",
"code_distance": "<any>",
"logical_error_rate": "<any>",
"num_tfactories": "<any>",
"physical_qubits_algorithm": "<any>",
"physical_qubits_tfactories": "<any>",
"qubit_params": "<any>",
"qec_scheme": "<any>",
"formatted": "<any>",
"full": "<any>"
}
}
]

A campaign is a sinter Monte Carlo collection — tasks (circuit × decoder), adaptive shot allocation, multiprocess workers — wrapped as a managed kernel job. This is the deliberate exception to the “frontend orchestrates, kernel stays dumb” rule: sinter is the orchestrator.

{
"type": "qec_campaign_start",
"campaign_id": "surface-vs-repetition",
"tasks": [
{ "circuit_text": "R 0 1 2 3 4\n…", "decoder": "pymatching",
"json_metadata": { "code": "repetition", "d": 3, "p": 0.02 } }
],
"collect": { "max_shots": 50000, "max_errors": 500 },
"workers": "auto",
"progress_interval_s": 1.0,
"existing_stats_csv": " shots, errors, …"
}
  • At least one of collect.max_shots / collect.max_errors is required — an unbounded campaign never terminates.
  • workers: "auto" (default) means CPU count − 1. Task count is capped at 2,000 (an explicit error above — sinter’s adaptive allocation makes large grids sane, but not unbounded ones).
  • One campaign at a time per kernel: a second start answers an error with code campaign_already_running (409-style) naming the running id.
  • Resume: pass the previous run’s csv text as existing_stats_csv. It loads through sinter’s own existing_data machinery, counts toward the collection bounds, and completed tasks are never re-sampled — the result’s sampled_shots reports newly collected shots only (0 for an already-complete campaign). Kernel restarts kill workers by design; resume is what makes that acceptable.
  • No seed field. sinter.collect has no seeding API — Monte Carlo campaigns are not shot-for-shot reproducible, and the protocol refuses to imply otherwise. (Single runs and qec_decode_sample do seed.)

The ack is qec_campaign_started {campaign_id, tasks_total, workers}; progress and the final result then stream on the same connection:

  • qec_campaign_progress (throttled — at most one per progress_interval_s, default 1s): tasks holds accumulated totals for tasks whose numbers changed since the last update (merge client-side by strong_id), plus tasks_complete/tasks_total and sinter’s free-form status_message (its ETA text, version-dependent — display, don’t parse).
  • qec_campaign_result: partial (true after cancel or a worker failure), sampled_shots, stats rows, and csv — the stats in sinter’s native CSV format, bit-for-bit loadable by existing researcher scripts. Write it to disk as-is; never invent a private format.

A validation failure is a plain error (code qec_campaign_invalid) naming exactly what’s wrong — this one omitted the collection bounds:

qec_campaign_start_invalid.request.json
{
"type": "qec_campaign_start",
"campaign_id": "demo",
"tasks": [
{
"circuit_text": "R 0 1 2 3 4\nTICK\nCX 0 1 2 3\nTICK\nCX 2 1 4 3\nTICK\nX_ERROR(0.02) 1 3\nMR 1 3\nDETECTOR(1, 0) rec[-2]\nDETECTOR(3, 0) rec[-1]\nREPEAT 2 {\n TICK\n CX 0 1 2 3\n TICK\n CX 2 1 4 3\n TICK\n X_ERROR(0.02) 1 3\n MR 1 3\n SHIFT_COORDS(0, 1)\n DETECTOR(1, 0) rec[-2] rec[-4]\n DETECTOR(3, 0) rec[-1] rec[-3]\n}\nX_ERROR(0.02) 0 2 4\nM 0 2 4\nDETECTOR(1, 1) rec[-2] rec[-3] rec[-5]\nDETECTOR(3, 1) rec[-1] rec[-2] rec[-4]\nOBSERVABLE_INCLUDE(0) rec[-1]",
"decoder": "pymatching",
"json_metadata": {
"d": 3
}
}
]
}
qec_campaign_start_invalid.responses.json
[
{
"type": "error",
"message": "at least one of collect.max_shots / collect.max_errors is required \u2014 an unbounded campaign never terminates",
"code": "qec_campaign_invalid",
"phase": "qec_campaign"
}
]

{type: "qec_campaign_cancel", campaign_id} → immediate ack qec_campaign_cancelled {campaign_id, accepted}, then a graceful sinter shutdown: everything already collected (plus any resumed prior data) comes back as a qec_campaign_result with "partial": true.

A full captured session — start, one progress update, cancel, partial result. Illustrative (timing-dependent message counts; the semantics are pinned by kernel/tests/test_qec_campaign.py instead):

illustrative/qec_campaign.session.json
[
{
"request": {
"type": "qec_campaign_start",
"campaign_id": "surface-vs-repetition",
"tasks": [
{
"circuit_text": "R 0 1 2 3 4\nTICK\nCX 0 1 2 3\nTICK\nCX 2 1 4 3\nTICK\nX_ERROR(0.02) 1 3\nMR 1 3\nDETECTOR(1, 0) rec[-2]\nDETECTOR(3, 0) rec[-1]\nREPEAT 2 {\n TICK\n CX 0 1 2 3\n TICK\n CX 2 1 4 3\n TICK\n X_ERROR(0.02) 1 3\n MR 1 3\n SHIFT_COORDS(0, 1)\n DETECTOR(1, 0) rec[-2] rec[-4]\n DETECTOR(3, 0) rec[-1] rec[-3]\n}\nX_ERROR(0.02) 0 2 4\nM 0 2 4\nDETECTOR(1, 1) rec[-2] rec[-3] rec[-5]\nDETECTOR(3, 1) rec[-1] rec[-2] rec[-4]\nOBSERVABLE_INCLUDE(0) rec[-1]",
"decoder": "pymatching",
"json_metadata": {
"code": "repetition",
"d": 3,
"p": 0.02,
"i": 0
}
},
{
"circuit_text": "R 0 1 2 3 4\nTICK\nCX 0 1 2 3\nTICK\nCX 2 1 4 3\nTICK\nX_ERROR(0.02) 1 3\nMR 1 3\nDETECTOR(1, 0) rec[-2]\nDETECTOR(3, 0) rec[-1]\nREPEAT 2 {\n TICK\n CX 0 1 2 3\n TICK\n CX 2 1 4 3\n TICK\n X_ERROR(0.02) 1 3\n MR 1 3\n SHIFT_COORDS(0, 1)\n DETECTOR(1, 0) rec[-2] rec[-4]\n DETECTOR(3, 0) rec[-1] rec[-3]\n}\nX_ERROR(0.02) 0 2 4\nM 0 2 4\nDETECTOR(1, 1) rec[-2] rec[-3] rec[-5]\nDETECTOR(3, 1) rec[-1] rec[-2] rec[-4]\nOBSERVABLE_INCLUDE(0) rec[-1]",
"decoder": "pymatching",
"json_metadata": {
"code": "repetition",
"d": 3,
"p": 0.02,
"i": 1
}
}
],
"collect": {
"max_shots": 50000,
"max_errors": 500
},
"workers": 2,
"progress_interval_s": 0.3
},
"responses": [
{
"type": "qec_campaign_started",
"campaign_id": "surface-vs-repetition",
"tasks_total": 30,
"workers": 2
},
{
"type": "qec_campaign_progress",
"campaign_id": "surface-vs-repetition",
"tasks": [
{
"strong_id": "533523ae9d5717332fe8905449e21bbbdee0562c4ffa0bb2ca99066612a9d80b",
"decoder": "pymatching",
"json_metadata": {
"code": "repetition",
"d": 3,
"p": 0.02,
"i": 1
},
"shots": 50000,
"errors": 37,
"discards": 0,
"seconds": 0.004,
"custom_counts": {}
}
],
"tasks_complete": 1,
"tasks_total": 30,
"status_message": "29 tasks left:\n workers decoder eta shots_left errors_left json_metadata \n 1 pymatching ? 50000 500 code=repetition,d=3,p=0.02,i=0 \n 1 pymatching ? 50000 500 code=repetition,d=3,p=0.02,i=2 \n 0 pymatching ?\u00b7\u221e 50000 500 code=repetition,d=3,p=0.02,i=3 \n 0 pymatching ?\u00b7\u221e 50000 500 code=repetition,d=3,p=0.02,i=4 \n 0 pymatching ?\u00b7\u221e 50000 500 code=repetition,d=3,p=0.02,i=5 \n 0 pymatching ?\u00b7\u221e 50000 500 code=repetition,d=3,p=0.02,i=6 \n 0 pymatching ?\u00b7\u221e 50000 500 code=repetition,d=3,p=0.02,i=7 \n 0 pymatching ?\u00b7\u221e 50000 500 code=repetition,d=3,p=0.02,i=8 \n 0 pymatching ?\u00b7\u221e 50000 500 code=repetition,d=3,p=0.02,i=9 \n 0 pymatching ?\u00b7\u221e 50000 500 code=repetition,d=3,p=0.02,i=10\n 0 pymatching ?\u00b7\u221e 50000 500 code=repetition,d=3,p=0.02,i=11\n 0 pymatching ?\u00b7\u221e 50000 500 code=repetition,d=3,p=0.02,i=12\n 0 pymatching ?\u00b7\u221e 50000 500 code=repetition,d=3,p=0.02,i=13\n 0 pymatching ?\u00b7\u221e 50000 500 code=repetition,d=3,p=0.02,i=14\n 0 pymatching ?\u00b7\u221e 50000 500 code=repetition,d=3,p=0.02,i=15\n 0 pymatching ?\u00b7\u221e 50000 500 code=repetition,d=3,p=0.02,i=16\n 0 pymatching ?\u00b7\u221e 50000 500 code=repetition,d=3,p=0.02,i=17\n 0 pymatching ?\u00b7\u221e 50000 500 code=repetition,d=3,p=0.02,i=18\n 0 pymatching ?\u00b7\u221e 50000 500 code=repetition,d=3,p=0.02,i=19\n 0 pymatching ?\u00b7\u221e 50000 500 code=repetition,d=3,p=0.02,i=20\n 0 pymatching ?\u00b7\u221e 50000 500 code=repetition,d=3,p=0.02,i=21\n 0 pymatching ?\u00b7\u221e 50000 500 code=repetition,d=3,p=0.02,i=22\n 0 pymatching ?\u00b7\u221e 50000 500 code=repetition,d=3,p=0.02,i=23\n 0 pymatching ?\u00b7\u221e 50000 500 code=repetition,d=3,p=0.02,i=24\n 0 pymatching ?\u00b7\u221e 50000 500 code=repetition,d=3,p=0.02,i=25\n 0 pymatching ?\u00b7\u221e 50000 500 code=repetition,d=3,p=0.02,i=26\n 0 pymatching ?\u00b7\u221e 50000 500 code=repetition,d=3,p=0.02,i=27\n 0 pymatching ?\u00b7\u221e 50000 500 code=repetition,d=3,p=0.02,i=28\n 0 pymatching ?\u00b7\u221e 50000 500 code=repetition,d=3,p=0.02,i=29"
}
]
},
{
"request": {
"type": "qec_campaign_cancel",
"campaign_id": "surface-vs-repetition"
},
"responses": [
{
"type": "qec_campaign_cancelled",
"campaign_id": "surface-vs-repetition",
"accepted": true
},
{
"type": "qec_campaign_result",
"campaign_id": "surface-vs-repetition",
"partial": true,
"sampled_shots": 250000,
"stats": [
{
"strong_id": "533523ae9d5717332fe8905449e21bbbdee0562c4ffa0bb2ca99066612a9d80b",
"decoder": "pymatching",
"json_metadata": {
"code": "repetition",
"d": 3,
"p": 0.02,
"i": 1
},
"shots": 50000,
"errors": 37,
"discards": 0,
"seconds": 0.004,
"custom_counts": {}
},
{
"strong_id": "8c8ed2950a964d713970cb7ff54fe27c771bbd8c3c31908a1a95cbccbe259ce3",
"decoder": "pymatching",
"json_metadata": {
"code": "repetition",
"d": 3,
"p": 0.02,
"i": 0
},
"shots": 50000,
"errors": 58,
"discards": 0,
"seconds": 0.004,
"custom_counts": {}
}
],
"csv": " shots, errors, discards, seconds,decoder,strong_id,json_metadata,custom_counts\n 50000, 37, 0, 0.004,pymatching,533523ae9d5717332fe8905449e21bbbdee0562c4ffa0bb2ca99066612a9d80b,\"{\"\"code\"\":\"\"repetition\"\",\"\"d\"\":3,\"\"i\"\":1,\"\"p\"\":0.02}\",\n 50000, 58, 0, 0.004,pymatching,8c8ed2950a964d713970cb7ff54fe27c771bbd8c3c31908a1a95cbccbe259ce3,\"{\"\"code\"\":\"\"repetition\"\",\"\"d\"\":3,\"\"i\"\":0,\"\"p\"\":0.02}\",\n..."
}
]
}
]

Campaigns whose source is a Python entry: file provide circuits via the nuclei_circuits(noise) contract: the file defines nuclei_circuits(noise: dict) -> dict[str, stim.Circuit], the runner sends the file’s code plus one grid point’s resolved noise dict, and the kernel returns {label: circuit_text} (qec_circuits). Arbitrary custom noise stays possible — the user applies the dict however they like. Contract violations (missing function, wrong return shape, non-Circuit values) are qec_materialize_invalid errors with the exact complaint.

qec_materialize.request.json
{
"type": "qec_materialize",
"code": "import stim\n\ndef nuclei_circuits(noise):\n p = noise[\"p\"]\n return {\n \"rep_d3\": stim.Circuit.generated(\n \"repetition_code:memory\", distance=3, rounds=2,\n before_measure_flip_probability=p,\n ),\n }\n",
"noise": {
"p": 0.01
}
}
qec_materialize.responses.json
[
{
"type": "qec_circuits",
"circuits": {
"rep_d3": "R 0 1 2 3 4\nTICK\nCX 0 1 2 3\nTICK\nCX 2 1 4 3\nTICK\nX_ERROR(0.01) 1 3\nMR 1 3\nDETECTOR(1, 0) rec[-2]\nDETECTOR(3, 0) rec[-1]\nTICK\nCX 0 1 2 3\nTICK\nCX 2 1 4 3\nTICK\nX_ERROR(0.01) 1 3\nMR 1 3\nSHIFT_COORDS(0, 1)\nDETECTOR(1, 0) rec[-2] rec[-4]\nDETECTOR(3, 0) rec[-1] rec[-3]\nX_ERROR(0.01) 0 2 4\nM 0 2 4\nDETECTOR(1, 1) rec[-2] rec[-3] rec[-5]\nDETECTOR(3, 1) rec[-1] rec[-2] rec[-4]\nOBSERVABLE_INCLUDE(0) rec[-1]"
}
}
]

One sampled shot, decoded — the detector-graph overlay’s “sample a shot” button. Cheap, synchronous, and deterministic for a given seed. v1 decodes with pymatching only (campaigns support every decoder sinter knows); matched_edges pairs detector indices, with d2: null for boundary matches.

qec_decode_sample.request.json
{
"type": "qec_decode_sample",
"circuit_text": "R 0 1 2 3 4\nTICK\nCX 0 1 2 3\nTICK\nCX 2 1 4 3\nTICK\nX_ERROR(0.02) 1 3\nMR 1 3\nDETECTOR(1, 0) rec[-2]\nDETECTOR(3, 0) rec[-1]\nREPEAT 2 {\n TICK\n CX 0 1 2 3\n TICK\n CX 2 1 4 3\n TICK\n X_ERROR(0.02) 1 3\n MR 1 3\n SHIFT_COORDS(0, 1)\n DETECTOR(1, 0) rec[-2] rec[-4]\n DETECTOR(3, 0) rec[-1] rec[-3]\n}\nX_ERROR(0.02) 0 2 4\nM 0 2 4\nDETECTOR(1, 1) rec[-2] rec[-3] rec[-5]\nDETECTOR(3, 1) rec[-1] rec[-2] rec[-4]\nOBSERVABLE_INCLUDE(0) rec[-1]",
"decoder": "pymatching",
"seed": 3
}
qec_decode_sample.responses.json
[
{
"type": "qec_decode_sample",
"num_detectors": 8,
"syndrome": "<any>",
"matched_edges": "<any>",
"predicted_observable_flips": "<any>",
"actual_observable_flips": "<any>"
}
]

A concrete response, captured on one machine (values are seed-and-machine deterministic — like all stim sampling, they vary across CPUs, which is why the replayed fixture above pins the shape rather than the values):

{
"type": "qec_decode_sample",
"num_detectors": 8,
"syndrome": [5, 7],
"matched_edges": [{ "d1": 5, "d2": 7 }],
"predicted_observable_flips": [0],
"actual_observable_flips": [0]
}

Static analysis of a Stim circuit’s text, answering “why does this circuit fail?” without sampling anything. Request: { "type": "qec_explain", "circuit_text": "...", "analysis": "both" | "nondeterministic" | "min_weight", "request_id": "..." }. Reply: qec_explain_result with data.nondeterministic and/or data.min_weight; failures arrive as an error with phase: "qec_explain".

  • nondeterministic.status is ok, nondeterministic, invalid_circuit or parse_error. For nondeterministic it carries the detector/observable indices, the anticommuting operation (description, qubits), the tick, the .stim source line (where Stim found the collapse), declaration_lines ({"D0": 12, "L0": 30}: the line that declares each named detector or observable, usually the real cause), a plain-language sentence, and Stim’s own detslice-with-ops-svg diagram as diagram_svg. Circuits using ELSE_CORRELATED_ERROR are analysed with approximate_disjoint_errors (determinism does not depend on probabilities).
  • When Stim blames a reset or measurement but the real cause is a basis mismatch (for example an X-type detector in round 0 of a Z-basis memory experiment, or MX after R), nondeterministic.cause is { "kind": "preparation_basis_mismatch" | "measurement_basis_mismatch", "basis": "Z", "operation_line", "preparation_line", "detector_lines" } and the sentence points at the DETECTOR declaration and the preparation line instead of calling the reset the error. Otherwise cause is null.
  • Every flagged detector/observable is listed, bounded at 20: targets, sensitivities ([{ "target": "D0", "pauli": "Z0" }]), target_count (the total) and targets_omitted (how many were left out; the sentence says “and N more”). sensitivity remains the first detector’s Pauli.
  • min_weight.weight is the length of the shortest graphlike undetected logical error (shortest_graphlike_error, falling back to a bounded search_for_undetectable_logical_errors). It is an upper bound on the circuit-level distance, not a proof. Each entry of faults has tick, qubits, gate, instruction, line and (inside a REPEAT) the iteration index. caveat states which search produced the weight. circuit_text larger than 1,000,000 bytes is refused with qec_explain_too_large (the WebSocket limit is 1 MiB). Replies echo request_id.
qec_explain.request.json
{
"type": "qec_explain",
"request_id": "explain-1",
"analysis": "nondeterministic",
"circuit_text": "R 0\nH 0\nM 0\nDETECTOR rec[-1]\n"
}
qec_explain.responses.json
[
{
"type": "qec_explain_result",
"data": {
"nondeterministic": {
"kind": "detector",
"stim_message": "<any>",
"line": 1,
"lines": [
1
],
"line_text": "R 0",
"tick": 0,
"status": "nondeterministic",
"detector_indices": [
0
],
"observable_indices": [],
"targets": [
"D0"
],
"target_count": 1,
"targets_omitted": 0,
"declaration_lines": {
"D0": 4
},
"cause": {
"kind": "preparation_basis_mismatch",
"basis": "Z",
"operation_line": 1,
"preparation_line": 1,
"detector_lines": {
"D0": 4
}
},
"sensitivities": [
{
"target": "D0",
"pauli": "X0"
}
],
"operation": {
"description": "Z-basis reset (R)",
"qubits": [
0
],
"instruction": "R 0"
},
"sensitivity": "X0",
"diagram_svg": "<any>",
"sentence": "Detector D0 is non-deterministic, but the Z-basis reset (R) on qubit 0 (line 1, after 0 TICKs) is only where Stim noticed it: the parity of D0 is not fixed by the preparation in that basis, so it is random even without noise. The Pauli it is sensitive to there is X0. Check the DETECTOR declaration (D0 at line 4) and the preparation at line 1: either the DETECTOR compares measurements that are not fixed in the Z basis, or the qubit is prepared in the wrong basis. This is a static property of the circuit text, independent of noise or sampled data. Common sources are a shifted or missing DETECTOR rec target, a wrong rec offset, or a measurement/reset in the wrong basis."
}
},
"request_id": "explain-1"
}
]

Lines are 1-based and are null whenever the instruction-to-line mapping cannot be verified against the parsed circuit. The analysis never edits the circuit and makes no claim about sampled data.