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.
- qldpc_certificate_benchmark-0.1.0/.gitignore +14 -0
- qldpc_certificate_benchmark-0.1.0/LICENSE +21 -0
- qldpc_certificate_benchmark-0.1.0/PKG-INFO +326 -0
- qldpc_certificate_benchmark-0.1.0/README.md +288 -0
- qldpc_certificate_benchmark-0.1.0/examples/code_capacity_bb72.py +19 -0
- qldpc_certificate_benchmark-0.1.0/examples/stim_surface_code.py +27 -0
- qldpc_certificate_benchmark-0.1.0/pyproject.toml +61 -0
- qldpc_certificate_benchmark-0.1.0/src/qldpc_certificate_benchmark/__init__.py +42 -0
- qldpc_certificate_benchmark-0.1.0/src/qldpc_certificate_benchmark/__main__.py +35 -0
- qldpc_certificate_benchmark-0.1.0/src/qldpc_certificate_benchmark/_engine/__init__.py +0 -0
- qldpc_certificate_benchmark-0.1.0/src/qldpc_certificate_benchmark/_engine/clusters.py +195 -0
- qldpc_certificate_benchmark-0.1.0/src/qldpc_certificate_benchmark/_engine/constants.py +42 -0
- qldpc_certificate_benchmark-0.1.0/src/qldpc_certificate_benchmark/_engine/engine.py +154 -0
- qldpc_certificate_benchmark-0.1.0/src/qldpc_certificate_benchmark/_engine/kernels.py +765 -0
- qldpc_certificate_benchmark-0.1.0/src/qldpc_certificate_benchmark/_engine/relaxations.py +407 -0
- qldpc_certificate_benchmark-0.1.0/src/qldpc_certificate_benchmark/_engine/sector.py +123 -0
- qldpc_certificate_benchmark-0.1.0/src/qldpc_certificate_benchmark/_engine/selftest.py +627 -0
- qldpc_certificate_benchmark-0.1.0/src/qldpc_certificate_benchmark/_engine/tesseract.py +113 -0
- qldpc_certificate_benchmark-0.1.0/src/qldpc_certificate_benchmark/_engine/verdicts.py +425 -0
- qldpc_certificate_benchmark-0.1.0/src/qldpc_certificate_benchmark/_version.py +1 -0
- qldpc_certificate_benchmark-0.1.0/src/qldpc_certificate_benchmark/benchmark.py +347 -0
- qldpc_certificate_benchmark-0.1.0/src/qldpc_certificate_benchmark/certifier.py +214 -0
- qldpc_certificate_benchmark-0.1.0/src/qldpc_certificate_benchmark/codes.py +138 -0
- qldpc_certificate_benchmark-0.1.0/src/qldpc_certificate_benchmark/config.py +90 -0
- qldpc_certificate_benchmark-0.1.0/src/qldpc_certificate_benchmark/decoders.py +321 -0
- qldpc_certificate_benchmark-0.1.0/src/qldpc_certificate_benchmark/problem.py +286 -0
- qldpc_certificate_benchmark-0.1.0/src/qldpc_certificate_benchmark/results.py +252 -0
- qldpc_certificate_benchmark-0.1.0/tests/conftest.py +32 -0
- qldpc_certificate_benchmark-0.1.0/tests/test_api.py +109 -0
- qldpc_certificate_benchmark-0.1.0/tests/test_bruteforce.py +61 -0
- qldpc_certificate_benchmark-0.1.0/tests/test_multiprocessing.py +38 -0
- qldpc_certificate_benchmark-0.1.0/tests/test_selftest.py +7 -0
- qldpc_certificate_benchmark-0.1.0/tests/test_stim.py +48 -0
- qldpc_certificate_benchmark-0.1.0/tests/test_worker_errors.py +20 -0
|
@@ -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"))
|