qldpc-certificate-benchmark 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. qldpc_certificate_benchmark-0.1.0/.gitignore +14 -0
  2. qldpc_certificate_benchmark-0.1.0/LICENSE +21 -0
  3. qldpc_certificate_benchmark-0.1.0/PKG-INFO +326 -0
  4. qldpc_certificate_benchmark-0.1.0/README.md +288 -0
  5. qldpc_certificate_benchmark-0.1.0/examples/code_capacity_bb72.py +19 -0
  6. qldpc_certificate_benchmark-0.1.0/examples/stim_surface_code.py +27 -0
  7. qldpc_certificate_benchmark-0.1.0/pyproject.toml +61 -0
  8. qldpc_certificate_benchmark-0.1.0/src/qldpc_certificate_benchmark/__init__.py +42 -0
  9. qldpc_certificate_benchmark-0.1.0/src/qldpc_certificate_benchmark/__main__.py +35 -0
  10. qldpc_certificate_benchmark-0.1.0/src/qldpc_certificate_benchmark/_engine/__init__.py +0 -0
  11. qldpc_certificate_benchmark-0.1.0/src/qldpc_certificate_benchmark/_engine/clusters.py +195 -0
  12. qldpc_certificate_benchmark-0.1.0/src/qldpc_certificate_benchmark/_engine/constants.py +42 -0
  13. qldpc_certificate_benchmark-0.1.0/src/qldpc_certificate_benchmark/_engine/engine.py +154 -0
  14. qldpc_certificate_benchmark-0.1.0/src/qldpc_certificate_benchmark/_engine/kernels.py +765 -0
  15. qldpc_certificate_benchmark-0.1.0/src/qldpc_certificate_benchmark/_engine/relaxations.py +407 -0
  16. qldpc_certificate_benchmark-0.1.0/src/qldpc_certificate_benchmark/_engine/sector.py +123 -0
  17. qldpc_certificate_benchmark-0.1.0/src/qldpc_certificate_benchmark/_engine/selftest.py +627 -0
  18. qldpc_certificate_benchmark-0.1.0/src/qldpc_certificate_benchmark/_engine/tesseract.py +113 -0
  19. qldpc_certificate_benchmark-0.1.0/src/qldpc_certificate_benchmark/_engine/verdicts.py +425 -0
  20. qldpc_certificate_benchmark-0.1.0/src/qldpc_certificate_benchmark/_version.py +1 -0
  21. qldpc_certificate_benchmark-0.1.0/src/qldpc_certificate_benchmark/benchmark.py +347 -0
  22. qldpc_certificate_benchmark-0.1.0/src/qldpc_certificate_benchmark/certifier.py +214 -0
  23. qldpc_certificate_benchmark-0.1.0/src/qldpc_certificate_benchmark/codes.py +138 -0
  24. qldpc_certificate_benchmark-0.1.0/src/qldpc_certificate_benchmark/config.py +90 -0
  25. qldpc_certificate_benchmark-0.1.0/src/qldpc_certificate_benchmark/decoders.py +321 -0
  26. qldpc_certificate_benchmark-0.1.0/src/qldpc_certificate_benchmark/problem.py +286 -0
  27. qldpc_certificate_benchmark-0.1.0/src/qldpc_certificate_benchmark/results.py +252 -0
  28. qldpc_certificate_benchmark-0.1.0/tests/conftest.py +32 -0
  29. qldpc_certificate_benchmark-0.1.0/tests/test_api.py +109 -0
  30. qldpc_certificate_benchmark-0.1.0/tests/test_bruteforce.py +61 -0
  31. qldpc_certificate_benchmark-0.1.0/tests/test_multiprocessing.py +38 -0
  32. qldpc_certificate_benchmark-0.1.0/tests/test_selftest.py +7 -0
  33. qldpc_certificate_benchmark-0.1.0/tests/test_stim.py +48 -0
  34. qldpc_certificate_benchmark-0.1.0/tests/test_worker_errors.py +20 -0
@@ -0,0 +1,14 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *.nbi
4
+ *.nbc
5
+ .venv/
6
+ venv/
7
+ build/
8
+ dist/
9
+ *.egg-info/
10
+ .pytest_cache/
11
+ .DS_Store
12
+ results/
13
+ *.npz
14
+ .env
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Michele Banfi
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,326 @@
1
+ Metadata-Version: 2.5
2
+ Name: qldpc-certificate-benchmark
3
+ Version: 0.1.0
4
+ Summary: Certified maximum-likelihood benchmarking of quantum error-correction decoders: prove, shot by shot, whether ML decoding succeeds.
5
+ Project-URL: Homepage, https://github.com/michelebanfi/qldpc-certificate-benchmark
6
+ Project-URL: Repository, https://github.com/michelebanfi/qldpc-certificate-benchmark
7
+ Project-URL: Issues, https://github.com/michelebanfi/qldpc-certificate-benchmark/issues
8
+ Author-email: Michele Banfi <michi.banfi01@gmail.com>
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Keywords: benchmark,bivariate bicycle,certificate,decoder,detector error model,maximum likelihood,qLDPC,quantum error correction,stim
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Intended Audience :: Science/Research
14
+ Classifier: Operating System :: OS Independent
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Topic :: Scientific/Engineering :: Physics
21
+ Requires-Python: >=3.10
22
+ Requires-Dist: ldpc>=2.2
23
+ Requires-Dist: numba>=0.58
24
+ Requires-Dist: numpy>=1.23
25
+ Requires-Dist: scipy>=1.10
26
+ Provides-Extra: all
27
+ Requires-Dist: stim>=1.12; extra == 'all'
28
+ Requires-Dist: tesseract-decoder>=0.1.1.dev0; extra == 'all'
29
+ Provides-Extra: stim
30
+ Requires-Dist: stim>=1.12; extra == 'stim'
31
+ Provides-Extra: tesseract
32
+ Requires-Dist: stim>=1.12; extra == 'tesseract'
33
+ Requires-Dist: tesseract-decoder>=0.1.1.dev0; extra == 'tesseract'
34
+ Provides-Extra: test
35
+ Requires-Dist: pytest>=7; extra == 'test'
36
+ Requires-Dist: stim>=1.12; extra == 'test'
37
+ Description-Content-Type: text/markdown
38
+
39
+ # qldpc-certificate-benchmark
40
+
41
+ **Certified maximum-likelihood benchmarking for quantum error-correction decoders.**
42
+
43
+ Decoder papers usually compare against other heuristic decoders, because the
44
+ true maximum-likelihood (ML) decoder is intractable to run. This library
45
+ avoids that problem. For every shot it **proves** whether ML decoding would
46
+ succeed or fail. Averaged over shots, this gives a *certified bracket* on the
47
+ ML logical error rate, and any decoder can then be measured against the
48
+ optimum instead of against another heuristic.
49
+
50
+ It works on any independent-error decoding problem:
51
+
52
+ * a **Stim detector error model** or circuit (circuit-level noise),
53
+ * a **parity-check matrix** with its logical operators (code-capacity or
54
+ phenomenological noise),
55
+ * any `(H, L, priors)` triple you build yourself.
56
+
57
+ Here is an example: a rotated surface code, d=5, 5 rounds, circuit-level
58
+ p=0.005, 2000 Stim shots, PyMatching as the decoder under test
59
+ ([`examples/stim_surface_code.py`](examples/stim_surface_code.py)):
60
+
61
+ ```
62
+ Certified ML benchmark -- rotated surface d=5, p=0.005 (m=120, n=1679, k=1)
63
+ shots: 2000 wall: 9.3s verdicts: 1992 success / 8 fail / 0 tie / 0 undecided
64
+ ML LER certified bracket: [4.000e-03, 4.000e-03] (95% CI-widened: [1.992e-03, 7.206e-03])
65
+
66
+ decoder role LER 95% CI recov. intrin. unkn. p(worse than ML)
67
+ pymatching probe 6.500e-03 [3.47e-03, 1.11e-02] 8 5 0 0.113
68
+ bp_osd seed 4.500e-03 [2.06e-03, 8.53e-03] 2 7 0 0.5
69
+ tesseract seed 4.000e-03 [1.73e-03, 7.87e-03] 0 8 0 1
70
+ ```
71
+
72
+ On these shots, ML provably fails on exactly 8 (the bracket is closed).
73
+ PyMatching fails on 13: 5 of them no decoder could have corrected, and 8 are
74
+ shots where ML provably succeeds.
75
+
76
+ ---
77
+
78
+ ## Installation
79
+
80
+ ```bash
81
+ pip install qldpc-certificate-benchmark # core: numpy, scipy, numba, ldpc
82
+ pip install "qldpc-certificate-benchmark[stim]" # + Stim DEM / circuit import
83
+ pip install "qldpc-certificate-benchmark[all]" # + Tesseract beam MLE & exact Simplex MLE
84
+ ```
85
+
86
+ Python ≥ 3.10. The first call JIT-compiles the Numba kernels, which takes
87
+ about 20 s. The compiled kernels are cached for later runs.
88
+
89
+ ## Quick start
90
+
91
+ ### 1. Benchmark your decoder on a code (code-capacity noise)
92
+
93
+ ```python
94
+ import qldpc_certificate_benchmark as qcb
95
+
96
+ Hx, Hz, Lx, Lz = qcb.codes.bb_72_12_6() # [[72,12,6]] bivariate-bicycle code
97
+ problem = qcb.DecodingProblem.from_css_code(Hx, Hz, p=0.04) # X errors; logicals computed for you
98
+
99
+ def my_decoder(syndrome): # any function: syndrome -> correction (length n)
100
+ ... # or syndrome -> observables (length k)
101
+ return correction
102
+
103
+ result = qcb.benchmark(problem, {"mine": my_decoder}, shots=2000)
104
+ print(result)
105
+ ```
106
+
107
+ ### 2. Circuit-level noise from Stim
108
+
109
+ ```python
110
+ import stim, pymatching
111
+ import qldpc_certificate_benchmark as qcb
112
+
113
+ circuit = stim.Circuit.generated("surface_code:rotated_memory_x", distance=5, rounds=5,
114
+ after_clifford_depolarization=0.005,
115
+ before_measure_flip_probability=0.005,
116
+ after_reset_flip_probability=0.005)
117
+ problem = qcb.DecodingProblem.from_stim_circuit(circuit)
118
+ # or: qcb.DecodingProblem.from_dem(circuit.detector_error_model())
119
+ # or: qcb.DecodingProblem.from_dem("path/to/model.dem")
120
+
121
+ def make_matcher(problem): # called once in each worker process
122
+ dem = circuit.detector_error_model(decompose_errors=True)
123
+ return pymatching.Matching.from_detector_error_model(dem).decode
124
+
125
+ result = qcb.benchmark(problem,
126
+ {"pymatching": qcb.FunctionDecoder(factory=make_matcher,
127
+ output="observables")},
128
+ shots=5000)
129
+ ```
130
+
131
+ ### 3. Certify predictions you already computed
132
+
133
+ If you already have a sampling and batch-decoding pipeline, hand over the
134
+ shots and predictions. Nothing is re-sampled.
135
+
136
+ ```python
137
+ dets, obs = circuit.compile_detector_sampler(seed=0).sample(10_000, separate_observables=True)
138
+ pred = matcher.decode_batch(dets) # (shots, k) observables
139
+
140
+ result = qcb.certify_shots(problem, dets, obs, predictions={"pymatching": pred})
141
+ print(result.ml_bracket(), result.ler("pymatching"), result.gap_to_ml("pymatching"))
142
+ ```
143
+
144
+ `predictions` may hold `(shots, k)` observable predictions or `(shots, n)`
145
+ corrections over the columns of `H`.
146
+
147
+ ### 4. Certify a single shot
148
+
149
+ ```python
150
+ cert = qcb.Certifier(problem)
151
+ c = cert.certify(syndrome, true_observables)
152
+ c.verdict # "success" | "fail" | "tie" | "undecided"
153
+ c.ml_correct # True / False / None
154
+ ```
155
+
156
+ ### Your own matrices
157
+
158
+ ```python
159
+ problem = qcb.DecodingProblem(H, L, priors) # H: (m,n), L: (k,n), priors: scalar or (n,)
160
+ ```
161
+
162
+ `H` and `L` may be dense arrays or `scipy.sparse` matrices. Column `j` is one
163
+ independent error mechanism that fires with probability `priors[j]`, flips
164
+ the detectors in `H[:, j]`, and flips the logical observables in `L[:, j]`.
165
+
166
+ ### Where the logical operators come from
167
+
168
+ The certificate compares *logical classes*: the class of an error `e` is
169
+ `L e mod 2`. So it needs `L`, and where `L` comes from depends on the input:
170
+
171
+ * **Stim DEM or circuit.** `L` is the DEM's `L0, L1, ...` observables, as
172
+ defined by the circuit's `OBSERVABLE_INCLUDE`. Nothing is computed; the
173
+ certificate answers exactly the question your Stim benchmark asks.
174
+ * **CSS code, no logicals at hand.** `DecodingProblem.from_css_code(Hx, Hz, p,
175
+ error_type="X")` computes them with GF(2) elimination. A basis of `ker Hx`
176
+ modulo `rowspace Hz` gives the Z logicals, which classify X errors. Both
177
+ matrices are needed: from `Hz` alone you cannot tell a stabilizer
178
+ (in `rowspace Hx`) from a logical operator. `qcb.codes.css_logicals(Hx, Hz)`
179
+ returns the logicals directly.
180
+ * **Your own `L`.** Used as given. Any valid basis gives identical verdicts,
181
+ because another basis, or logicals shifted by stabilizers, only relabels the
182
+ classes. If you pass only a subset of the logicals, the certificate
183
+ answers "does ML predict *these* observables correctly", which is the same
184
+ convention as Stim.
185
+
186
+ ---
187
+
188
+ ## What exactly is certified
189
+
190
+ For a shot with syndrome `s` and true observables `λ`, write the cost of an
191
+ error pattern `e` as `Σ_j e_j·log((1-p_j)/p_j)` (its negative log-likelihood,
192
+ up to a constant). The certifier decides, **with proof**, how the cheapest
193
+ solution of `H e = s` in the true class `L e = λ` compares with the cheapest
194
+ solution in every wrong class:
195
+
196
+ | verdict | meaning |
197
+ |-------------|--------------------------------------------------------------------------|
198
+ | `success` | every wrong-class solution is strictly more expensive, so ML is correct |
199
+ | `fail` | some wrong-class solution is strictly cheaper, so ML is wrong |
200
+ | `tie` | the two optima are equal (within 1e-9), so ML depends on tie-breaking |
201
+ | `undecided` | the search budget ran out before a proof was found |
202
+
203
+ Over `N` shots, `#fail/N ≤ LER_ML ≤ (#fail + #tie + #undecided)/N` holds
204
+ **exactly** on the sampled shots. `result.ml_bracket()` also widens the
205
+ bracket with one-sided Clopper-Pearson intervals to cover sampling error.
206
+
207
+ This is *non-degenerate* ML: the most likely single error pattern, which is
208
+ the target of minimum-weight / MLE decoders such as Tesseract and exact ILP
209
+ decoders. It is not the most likely logical coset. A decoder can
210
+ occasionally beat non-degenerate ML; those shots are reported as `lucky`.
211
+ Code-capacity models with *uniform* priors have many exactly-degenerate
212
+ minima and therefore many `tie`s. Circuit-level DEMs, whose priors are not
213
+ uniform, rarely produce them.
214
+
215
+ **How it is proven.** Each shot runs an exact A* search over the coset with
216
+ admissible heuristics, strengthened by a covering-LP dual bound, adaptive
217
+ parity-cut LPs (Feldman), and exact integer programs (HiGHS). A certified
218
+ cluster decomposition splits shots with independent error regions. Every
219
+ witness produced by an LP/ILP solver or a seeding decoder is re-verified in
220
+ NumPy before it can enter a proof, so a solver bug or a wrong decoder can
221
+ make a shot `undecided` but never flip a verdict. The engine is validated
222
+ against exhaustive enumeration by `qcb.selftest()`. The kernels are compiled
223
+ without `fastmath`, so the strict comparisons keep IEEE semantics.
224
+
225
+ ## Reading the results
226
+
227
+ ```python
228
+ result.ml_bracket() # {'lb', 'ub', 'lb_ci', 'ub_ci', 'uncertified_frac'}
229
+ result.ler("mine") # LER, failures, shots, Clopper-Pearson CI
230
+ result.decomposition("mine")
231
+ # intrinsic -- ML also provably fails: no decoder could fix these
232
+ # recoverable -- ML provably succeeds: the decoder is provably suboptimal here
233
+ # unknown -- ML verdict is tie/undecided
234
+ # lucky -- decoder right where non-degenerate ML provably fails
235
+ result.gap_to_ml("mine") # one-sided exact McNemar vs ML; uncertified shots are
236
+ # resolved AGAINST the claim, so p is certified
237
+ result.compare("mine", "bp_osd") # paired exact McNemar between two decoders
238
+ result.summary() # everything as a dict
239
+ result.save("run1") # run1.json (summary) + run1.npz (per-shot arrays)
240
+ ```
241
+
242
+ `result.verdict`, `result.ok[name]`, and `result.ran[name]` hold the per-shot
243
+ arrays **in shot order**.
244
+
245
+ ## Decoders and roles (why the benchmark stays independent)
246
+
247
+ Every decoder has a role:
248
+
249
+ | role | measured | may tighten the certificate | default for |
250
+ |--------------|:--------:|:---------------------------:|---------------------------------------|
251
+ | `PROBE` | ✓ | ✗ | your decoders |
252
+ | `SEED` | ✓ | ✓ (re-verified) | `BPOSD`, `Tesseract` (via `seeders`) |
253
+ | `CROSSCHECK` | ✓ | ✗ | `Simplex` (exact MLE) |
254
+
255
+ The bracket is built only from `SEED` decoders plus an internal
256
+ decoder-agnostic anchor. A `PROBE`'s answer structurally cannot influence its
257
+ own certificate, so its comparison with ML is independent. Seeders only make
258
+ certification faster, by lowering the `undecided` fraction. By default
259
+ (`seeders="auto"`) BP+OSD seeds, plus Tesseract when it is installed. Seeding
260
+ with Tesseract is strongly recommended on large circuit-level DEMs. Pass
261
+ `seeders=None` to disable seeding.
262
+
263
+ Built-in decoders:
264
+
265
+ * `qcb.BPOSD(max_iter=50, osd_method="osd_cs", osd_order=7, **kw)`: BP+OSD from
266
+ Joschka Roffe's [`ldpc`](https://github.com/quantumgizmos/ldpc) package
267
+ (`ldpc.BpOsdDecoder`). Extra keyword arguments go straight to `ldpc`. Pass
268
+ `role="probe"` to benchmark it rather than seed with it.
269
+ * `qcb.Tesseract(beam=20, beam_climbing=True)`: Google's beam-search MLE
270
+ (`[all]` extra). Keep `beam_climbing=True`: without it, a wider beam gives
271
+ worse answers.
272
+ * `qcb.Simplex(shot_cap=100)`: exact MLE ILP. Pass `crosscheck=True` to
273
+ `benchmark` to verify that it never contradicts a proven verdict.
274
+
275
+ Custom decoders: pass a function, or subclass `qcb.Decoder`:
276
+
277
+ ```python
278
+ class MyDecoder(qcb.Decoder):
279
+ name, output = "mine", "correction" # or "observables"
280
+ def prepare(self, problem): # once per worker process
281
+ self.H = problem.H
282
+ def decode(self, syndrome):
283
+ ...
284
+ ```
285
+
286
+ A correction that does not reproduce the syndrome counts as a logical failure
287
+ (reported as `invalid`).
288
+
289
+ ## Performance knobs
290
+
291
+ ```python
292
+ cfg = qcb.CertConfig(true_node_budget=200_000, wrong_node_budget=100_000,
293
+ exact_ilp_time_limit=30.0)
294
+ qcb.benchmark(problem, decoders, shots=10_000, config=cfg, num_workers=16,
295
+ target_failures=100) # stop after 100 failures of the first PROBE
296
+ ```
297
+
298
+ No knob can make a certificate wrong; knobs only trade wall-time against the
299
+ `undecided` fraction. The certificate is cheapest at low physical error rates.
300
+ For large circuit-level DEMs at high `p`, expect a growing `undecided` share:
301
+ raise the budgets and seed with Tesseract.
302
+
303
+ Results are identical for any `num_workers`. Workers are started with `fork`
304
+ when the calling process is single-threaded. Otherwise they are started with
305
+ `forkserver`, for example after an in-process run, because scipy's HiGHS
306
+ solver keeps a thread pool alive and forking a process that holds it can
307
+ deadlock the workers. `forkserver` needs picklable decoders (module-level
308
+ functions or classes). In scripts, protect the entry point with
309
+ `if __name__ == "__main__":`.
310
+
311
+ ## Command line
312
+
313
+ ```bash
314
+ qldpc-certificate-benchmark selftest # brute-force validation of the engine
315
+ qldpc-certificate-benchmark demo --p 0.04 --shots 500 # BP+OSD-0 on [[72,12,6]]
316
+ ```
317
+
318
+ ## Citing
319
+
320
+ If you use this library, please cite our paper:
321
+
322
+ > *arXiv reference coming soon.*
323
+
324
+ ## License
325
+
326
+ MIT
@@ -0,0 +1,288 @@
1
+ # qldpc-certificate-benchmark
2
+
3
+ **Certified maximum-likelihood benchmarking for quantum error-correction decoders.**
4
+
5
+ Decoder papers usually compare against other heuristic decoders, because the
6
+ true maximum-likelihood (ML) decoder is intractable to run. This library
7
+ avoids that problem. For every shot it **proves** whether ML decoding would
8
+ succeed or fail. Averaged over shots, this gives a *certified bracket* on the
9
+ ML logical error rate, and any decoder can then be measured against the
10
+ optimum instead of against another heuristic.
11
+
12
+ It works on any independent-error decoding problem:
13
+
14
+ * a **Stim detector error model** or circuit (circuit-level noise),
15
+ * a **parity-check matrix** with its logical operators (code-capacity or
16
+ phenomenological noise),
17
+ * any `(H, L, priors)` triple you build yourself.
18
+
19
+ Here is an example: a rotated surface code, d=5, 5 rounds, circuit-level
20
+ p=0.005, 2000 Stim shots, PyMatching as the decoder under test
21
+ ([`examples/stim_surface_code.py`](examples/stim_surface_code.py)):
22
+
23
+ ```
24
+ Certified ML benchmark -- rotated surface d=5, p=0.005 (m=120, n=1679, k=1)
25
+ shots: 2000 wall: 9.3s verdicts: 1992 success / 8 fail / 0 tie / 0 undecided
26
+ ML LER certified bracket: [4.000e-03, 4.000e-03] (95% CI-widened: [1.992e-03, 7.206e-03])
27
+
28
+ decoder role LER 95% CI recov. intrin. unkn. p(worse than ML)
29
+ pymatching probe 6.500e-03 [3.47e-03, 1.11e-02] 8 5 0 0.113
30
+ bp_osd seed 4.500e-03 [2.06e-03, 8.53e-03] 2 7 0 0.5
31
+ tesseract seed 4.000e-03 [1.73e-03, 7.87e-03] 0 8 0 1
32
+ ```
33
+
34
+ On these shots, ML provably fails on exactly 8 (the bracket is closed).
35
+ PyMatching fails on 13: 5 of them no decoder could have corrected, and 8 are
36
+ shots where ML provably succeeds.
37
+
38
+ ---
39
+
40
+ ## Installation
41
+
42
+ ```bash
43
+ pip install qldpc-certificate-benchmark # core: numpy, scipy, numba, ldpc
44
+ pip install "qldpc-certificate-benchmark[stim]" # + Stim DEM / circuit import
45
+ pip install "qldpc-certificate-benchmark[all]" # + Tesseract beam MLE & exact Simplex MLE
46
+ ```
47
+
48
+ Python ≥ 3.10. The first call JIT-compiles the Numba kernels, which takes
49
+ about 20 s. The compiled kernels are cached for later runs.
50
+
51
+ ## Quick start
52
+
53
+ ### 1. Benchmark your decoder on a code (code-capacity noise)
54
+
55
+ ```python
56
+ import qldpc_certificate_benchmark as qcb
57
+
58
+ Hx, Hz, Lx, Lz = qcb.codes.bb_72_12_6() # [[72,12,6]] bivariate-bicycle code
59
+ problem = qcb.DecodingProblem.from_css_code(Hx, Hz, p=0.04) # X errors; logicals computed for you
60
+
61
+ def my_decoder(syndrome): # any function: syndrome -> correction (length n)
62
+ ... # or syndrome -> observables (length k)
63
+ return correction
64
+
65
+ result = qcb.benchmark(problem, {"mine": my_decoder}, shots=2000)
66
+ print(result)
67
+ ```
68
+
69
+ ### 2. Circuit-level noise from Stim
70
+
71
+ ```python
72
+ import stim, pymatching
73
+ import qldpc_certificate_benchmark as qcb
74
+
75
+ circuit = stim.Circuit.generated("surface_code:rotated_memory_x", distance=5, rounds=5,
76
+ after_clifford_depolarization=0.005,
77
+ before_measure_flip_probability=0.005,
78
+ after_reset_flip_probability=0.005)
79
+ problem = qcb.DecodingProblem.from_stim_circuit(circuit)
80
+ # or: qcb.DecodingProblem.from_dem(circuit.detector_error_model())
81
+ # or: qcb.DecodingProblem.from_dem("path/to/model.dem")
82
+
83
+ def make_matcher(problem): # called once in each worker process
84
+ dem = circuit.detector_error_model(decompose_errors=True)
85
+ return pymatching.Matching.from_detector_error_model(dem).decode
86
+
87
+ result = qcb.benchmark(problem,
88
+ {"pymatching": qcb.FunctionDecoder(factory=make_matcher,
89
+ output="observables")},
90
+ shots=5000)
91
+ ```
92
+
93
+ ### 3. Certify predictions you already computed
94
+
95
+ If you already have a sampling and batch-decoding pipeline, hand over the
96
+ shots and predictions. Nothing is re-sampled.
97
+
98
+ ```python
99
+ dets, obs = circuit.compile_detector_sampler(seed=0).sample(10_000, separate_observables=True)
100
+ pred = matcher.decode_batch(dets) # (shots, k) observables
101
+
102
+ result = qcb.certify_shots(problem, dets, obs, predictions={"pymatching": pred})
103
+ print(result.ml_bracket(), result.ler("pymatching"), result.gap_to_ml("pymatching"))
104
+ ```
105
+
106
+ `predictions` may hold `(shots, k)` observable predictions or `(shots, n)`
107
+ corrections over the columns of `H`.
108
+
109
+ ### 4. Certify a single shot
110
+
111
+ ```python
112
+ cert = qcb.Certifier(problem)
113
+ c = cert.certify(syndrome, true_observables)
114
+ c.verdict # "success" | "fail" | "tie" | "undecided"
115
+ c.ml_correct # True / False / None
116
+ ```
117
+
118
+ ### Your own matrices
119
+
120
+ ```python
121
+ problem = qcb.DecodingProblem(H, L, priors) # H: (m,n), L: (k,n), priors: scalar or (n,)
122
+ ```
123
+
124
+ `H` and `L` may be dense arrays or `scipy.sparse` matrices. Column `j` is one
125
+ independent error mechanism that fires with probability `priors[j]`, flips
126
+ the detectors in `H[:, j]`, and flips the logical observables in `L[:, j]`.
127
+
128
+ ### Where the logical operators come from
129
+
130
+ The certificate compares *logical classes*: the class of an error `e` is
131
+ `L e mod 2`. So it needs `L`, and where `L` comes from depends on the input:
132
+
133
+ * **Stim DEM or circuit.** `L` is the DEM's `L0, L1, ...` observables, as
134
+ defined by the circuit's `OBSERVABLE_INCLUDE`. Nothing is computed; the
135
+ certificate answers exactly the question your Stim benchmark asks.
136
+ * **CSS code, no logicals at hand.** `DecodingProblem.from_css_code(Hx, Hz, p,
137
+ error_type="X")` computes them with GF(2) elimination. A basis of `ker Hx`
138
+ modulo `rowspace Hz` gives the Z logicals, which classify X errors. Both
139
+ matrices are needed: from `Hz` alone you cannot tell a stabilizer
140
+ (in `rowspace Hx`) from a logical operator. `qcb.codes.css_logicals(Hx, Hz)`
141
+ returns the logicals directly.
142
+ * **Your own `L`.** Used as given. Any valid basis gives identical verdicts,
143
+ because another basis, or logicals shifted by stabilizers, only relabels the
144
+ classes. If you pass only a subset of the logicals, the certificate
145
+ answers "does ML predict *these* observables correctly", which is the same
146
+ convention as Stim.
147
+
148
+ ---
149
+
150
+ ## What exactly is certified
151
+
152
+ For a shot with syndrome `s` and true observables `λ`, write the cost of an
153
+ error pattern `e` as `Σ_j e_j·log((1-p_j)/p_j)` (its negative log-likelihood,
154
+ up to a constant). The certifier decides, **with proof**, how the cheapest
155
+ solution of `H e = s` in the true class `L e = λ` compares with the cheapest
156
+ solution in every wrong class:
157
+
158
+ | verdict | meaning |
159
+ |-------------|--------------------------------------------------------------------------|
160
+ | `success` | every wrong-class solution is strictly more expensive, so ML is correct |
161
+ | `fail` | some wrong-class solution is strictly cheaper, so ML is wrong |
162
+ | `tie` | the two optima are equal (within 1e-9), so ML depends on tie-breaking |
163
+ | `undecided` | the search budget ran out before a proof was found |
164
+
165
+ Over `N` shots, `#fail/N ≤ LER_ML ≤ (#fail + #tie + #undecided)/N` holds
166
+ **exactly** on the sampled shots. `result.ml_bracket()` also widens the
167
+ bracket with one-sided Clopper-Pearson intervals to cover sampling error.
168
+
169
+ This is *non-degenerate* ML: the most likely single error pattern, which is
170
+ the target of minimum-weight / MLE decoders such as Tesseract and exact ILP
171
+ decoders. It is not the most likely logical coset. A decoder can
172
+ occasionally beat non-degenerate ML; those shots are reported as `lucky`.
173
+ Code-capacity models with *uniform* priors have many exactly-degenerate
174
+ minima and therefore many `tie`s. Circuit-level DEMs, whose priors are not
175
+ uniform, rarely produce them.
176
+
177
+ **How it is proven.** Each shot runs an exact A* search over the coset with
178
+ admissible heuristics, strengthened by a covering-LP dual bound, adaptive
179
+ parity-cut LPs (Feldman), and exact integer programs (HiGHS). A certified
180
+ cluster decomposition splits shots with independent error regions. Every
181
+ witness produced by an LP/ILP solver or a seeding decoder is re-verified in
182
+ NumPy before it can enter a proof, so a solver bug or a wrong decoder can
183
+ make a shot `undecided` but never flip a verdict. The engine is validated
184
+ against exhaustive enumeration by `qcb.selftest()`. The kernels are compiled
185
+ without `fastmath`, so the strict comparisons keep IEEE semantics.
186
+
187
+ ## Reading the results
188
+
189
+ ```python
190
+ result.ml_bracket() # {'lb', 'ub', 'lb_ci', 'ub_ci', 'uncertified_frac'}
191
+ result.ler("mine") # LER, failures, shots, Clopper-Pearson CI
192
+ result.decomposition("mine")
193
+ # intrinsic -- ML also provably fails: no decoder could fix these
194
+ # recoverable -- ML provably succeeds: the decoder is provably suboptimal here
195
+ # unknown -- ML verdict is tie/undecided
196
+ # lucky -- decoder right where non-degenerate ML provably fails
197
+ result.gap_to_ml("mine") # one-sided exact McNemar vs ML; uncertified shots are
198
+ # resolved AGAINST the claim, so p is certified
199
+ result.compare("mine", "bp_osd") # paired exact McNemar between two decoders
200
+ result.summary() # everything as a dict
201
+ result.save("run1") # run1.json (summary) + run1.npz (per-shot arrays)
202
+ ```
203
+
204
+ `result.verdict`, `result.ok[name]`, and `result.ran[name]` hold the per-shot
205
+ arrays **in shot order**.
206
+
207
+ ## Decoders and roles (why the benchmark stays independent)
208
+
209
+ Every decoder has a role:
210
+
211
+ | role | measured | may tighten the certificate | default for |
212
+ |--------------|:--------:|:---------------------------:|---------------------------------------|
213
+ | `PROBE` | ✓ | ✗ | your decoders |
214
+ | `SEED` | ✓ | ✓ (re-verified) | `BPOSD`, `Tesseract` (via `seeders`) |
215
+ | `CROSSCHECK` | ✓ | ✗ | `Simplex` (exact MLE) |
216
+
217
+ The bracket is built only from `SEED` decoders plus an internal
218
+ decoder-agnostic anchor. A `PROBE`'s answer structurally cannot influence its
219
+ own certificate, so its comparison with ML is independent. Seeders only make
220
+ certification faster, by lowering the `undecided` fraction. By default
221
+ (`seeders="auto"`) BP+OSD seeds, plus Tesseract when it is installed. Seeding
222
+ with Tesseract is strongly recommended on large circuit-level DEMs. Pass
223
+ `seeders=None` to disable seeding.
224
+
225
+ Built-in decoders:
226
+
227
+ * `qcb.BPOSD(max_iter=50, osd_method="osd_cs", osd_order=7, **kw)`: BP+OSD from
228
+ Joschka Roffe's [`ldpc`](https://github.com/quantumgizmos/ldpc) package
229
+ (`ldpc.BpOsdDecoder`). Extra keyword arguments go straight to `ldpc`. Pass
230
+ `role="probe"` to benchmark it rather than seed with it.
231
+ * `qcb.Tesseract(beam=20, beam_climbing=True)`: Google's beam-search MLE
232
+ (`[all]` extra). Keep `beam_climbing=True`: without it, a wider beam gives
233
+ worse answers.
234
+ * `qcb.Simplex(shot_cap=100)`: exact MLE ILP. Pass `crosscheck=True` to
235
+ `benchmark` to verify that it never contradicts a proven verdict.
236
+
237
+ Custom decoders: pass a function, or subclass `qcb.Decoder`:
238
+
239
+ ```python
240
+ class MyDecoder(qcb.Decoder):
241
+ name, output = "mine", "correction" # or "observables"
242
+ def prepare(self, problem): # once per worker process
243
+ self.H = problem.H
244
+ def decode(self, syndrome):
245
+ ...
246
+ ```
247
+
248
+ A correction that does not reproduce the syndrome counts as a logical failure
249
+ (reported as `invalid`).
250
+
251
+ ## Performance knobs
252
+
253
+ ```python
254
+ cfg = qcb.CertConfig(true_node_budget=200_000, wrong_node_budget=100_000,
255
+ exact_ilp_time_limit=30.0)
256
+ qcb.benchmark(problem, decoders, shots=10_000, config=cfg, num_workers=16,
257
+ target_failures=100) # stop after 100 failures of the first PROBE
258
+ ```
259
+
260
+ No knob can make a certificate wrong; knobs only trade wall-time against the
261
+ `undecided` fraction. The certificate is cheapest at low physical error rates.
262
+ For large circuit-level DEMs at high `p`, expect a growing `undecided` share:
263
+ raise the budgets and seed with Tesseract.
264
+
265
+ Results are identical for any `num_workers`. Workers are started with `fork`
266
+ when the calling process is single-threaded. Otherwise they are started with
267
+ `forkserver`, for example after an in-process run, because scipy's HiGHS
268
+ solver keeps a thread pool alive and forking a process that holds it can
269
+ deadlock the workers. `forkserver` needs picklable decoders (module-level
270
+ functions or classes). In scripts, protect the entry point with
271
+ `if __name__ == "__main__":`.
272
+
273
+ ## Command line
274
+
275
+ ```bash
276
+ qldpc-certificate-benchmark selftest # brute-force validation of the engine
277
+ qldpc-certificate-benchmark demo --p 0.04 --shots 500 # BP+OSD-0 on [[72,12,6]]
278
+ ```
279
+
280
+ ## Citing
281
+
282
+ If you use this library, please cite our paper:
283
+
284
+ > *arXiv reference coming soon.*
285
+
286
+ ## License
287
+
288
+ MIT
@@ -0,0 +1,19 @@
1
+ """Certified benchmark on the [[72,12,6]] bivariate-bicycle code, code-capacity noise.
2
+
3
+ Measures two BP+OSD configurations from Roffe's ``ldpc`` package against the
4
+ certified ML bracket:
5
+
6
+ * ``bp+osd0`` -- BP + OSD-0 (standing in for "your decoder", role PROBE)
7
+ * ``bp_osd`` -- BP + OSD-CS order 7, the default seeder (role SEED)
8
+ """
9
+ import qldpc_certificate_benchmark as qcb
10
+
11
+ Hx, Hz, Lx, Lz = qcb.codes.bb_72_12_6()
12
+ problem = qcb.DecodingProblem.from_css_code(Hx, Hz, p=0.04, error_type="X",
13
+ name="[[72,12,6]] X errors, p=0.04")
14
+ mine = qcb.BPOSD(osd_method="osd0", name="bp+osd0", role="probe")
15
+
16
+ if __name__ == "__main__":
17
+ result = qcb.benchmark(problem, [mine], shots=2000, seed=0)
18
+ print(result)
19
+ print(result.compare("bp+osd0", "bp_osd"))