Skip to content

Commit 0ab806e

Browse files
committed
docs: cite the corrected benchmark numbers publicly
Puts the measured circuit-level results in the README as a table readers can check against `official_benchmark_results.csv`, and records the correction in the CHANGELOG so the supersession is on the public record rather than only in git history. Both findings are stated, including the unflattering one. PyMatching led on throughput at every distance measured; `qector_blossom` and PyMatching returned identical logical-failure counts on identical samples at d=3 and d=5 (1891 and 1596 in 100,000). Neither is generalised beyond the cells shown, per docs/REPRODUCIBILITY_CHECKLIST.md, and the not-quiesced host and `git_tree_dirty: true` provenance are stated alongside. All 14 cited rows were machine-checked against the CSV before commit - throughput, LER and both Wilson bounds - so the README cannot drift from the artifact it points at. The reports were re-rendered with `--from-json` rather than re-measured, so the underlying JSON is byte-identical to the run these figures come from. The only change is presentation: a ~1000x-slower decoder rendered as "0.00x", which reads as zero throughput instead of as the accurate number.
1 parent 649a222 commit 0ab806e

5 files changed

Lines changed: 117 additions & 19 deletions

File tree

CHANGELOG.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,33 @@ has been published. `src/*.rs` is `.gitignore`d, so `git log v0.6.9..HEAD` shows
1212
none of the Rust work recorded here — it is verified by `cargo test` and by
1313
reading the tree.
1414

15+
### Benchmarks — corrected artifacts, and what they say
16+
- **`official_benchmark_results.{json,csv,md,pdf}` were regenerated.** The first
17+
v0.7.0 versions reproduced the defect that got six pre-v0.7.0 artifacts
18+
withdrawn (todo6 A1-03), and additionally stamped `_provenance`'s canned
19+
methodology note — which asserts `ler.estimate_ler_circuit_level` scoring and
20+
`ler.assert_comparable` validation — into files produced by neither. They
21+
reported "LER" as a syndrome-consistency check `(H·ĉ = s)`, which reads 0.000%
22+
for every decoder at every distance because it never asks whether the logical
23+
observable flipped; they timed QECTOR through a native batch call against
24+
PyMatching through a 20-shot Python loop extrapolated to 100,000; and their
25+
LER chart plotted hardcoded analytic curves rather than measurements.
26+
- **What replaced them.** Every row now comes from
27+
`ler.estimate_ler_circuit_level` — one circuit, one decomposed DEM, one
28+
detector/observable sample set per cell, one `decode_batch` resolver for
29+
QECTOR, PyMatching and ldpc alike — and `ler.assert_comparable` gates the rows
30+
before writing. Nothing is extrapolated: 63 cells that exceeded the per-cell
31+
decode budget are recorded as *not measured*, with their probe rate and
32+
projected cost.
33+
- **The numbers, per-cell and not generalised.** `qector_blossom` and PyMatching
34+
2 returned identical logical-failure counts on identical samples at `d = 3`
35+
and `d = 5` (1891 and 1596 in 100,000). On throughput PyMatching led at every
36+
distance measured, consistent with this project's long-standing note that it
37+
leads on plain MWPM. Both statements are in the artifacts and in the README.
38+
- `scripts/run_custom_comparison_benchmark.py` gained `--from-json`, which
39+
re-renders the reports from a stamped artifact without re-measuring, so a
40+
presentation fix cannot silently move the numbers underneath a citation.
41+
1542
### Added — CLI, diagnostics, and ecosystem entry points
1643
- **`qector` CLI** (`decode` / `bench` / `serve`) and **`qector-doctor`**, a
1744
15-check environment diagnostic that reports *why* a backend is unavailable

README.md

Lines changed: 37 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -423,20 +423,46 @@ quiesced**, and its provenance block records `git_tree_dirty: true`. A further
423423
63 cells exceeded the per-cell decode budget and are listed as *not measured*,
424424
carrying their measured probe rate and projected cost — no cell is extrapolated.
425425

426+
Largest shot count measured per cell. Throughput is decode time only; LER is
427+
per shot with a 95% Wilson interval. Every row is one
428+
`estimate_ler_circuit_level` call on the same circuit, DEM and samples.
429+
430+
| d | Decoder | Shots | Throughput (dec/s) | LER | 95% CI |
431+
| ---: | --- | ---: | ---: | ---: | --- |
432+
| 3 | PyMatching 2 | 100,000 | 2,437,651 | 0.01891 | [0.01808, 0.01977] |
433+
| 3 | `qector_blossom` | 100,000 | 245,629 | 0.01891 | [0.01808, 0.01977] |
434+
| 3 | `qector_unionfind` | 100,000 | 825,726 | 0.02210 | [0.02121, 0.02303] |
435+
| 3 | ldpc BP-OSD | 50,000 | 2,259 | 0.01938 | [0.01821, 0.02063] |
436+
| 5 | PyMatching 2 | 100,000 | 249,065 | 0.01596 | [0.01520, 0.01676] |
437+
| 5 | `qector_blossom` | 100,000 | 9,121 | 0.01596 | [0.01520, 0.01676] |
438+
| 5 | `qector_unionfind` | 100,000 | 112,797 | 0.02645 | [0.02547, 0.02746] |
439+
| 7 | PyMatching 2 | 100,000 | 75,767 | 0.01220 | [0.01154, 0.01290] |
440+
| 7 | `qector_unionfind` | 100,000 | 22,643 | 0.02042 | [0.01956, 0.02132] |
441+
| 9 | PyMatching 2 | 100,000 | 29,244 | 0.00878 | [0.00822, 0.00938] |
442+
| 9 | `qector_unionfind` | 100,000 | 4,606 | 0.01732 | [0.01653, 0.01815] |
443+
| 11 | PyMatching 2 | 100,000 | 13,816 | 0.00647 | [0.00599, 0.00699] |
444+
| 13 | PyMatching 2 | 100,000 | 7,218 | 0.00445 | [0.00406, 0.00488] |
445+
| 15 | PyMatching 2 | 100,000 | 6,480 | 0.00314 | [0.00281, 0.00351] |
446+
447+
The full 77-row table, including every `qector_blossom` and ldpc cell that fit
448+
the budget, is in `official_benchmark_results.md`.
449+
426450
Two findings, stated per-cell and not generalised (see
427451
`docs/REPRODUCIBILITY_CHECKLIST.md`):
428452

429-
- At `d = 3` and `d = 5`, `qector_blossom` and PyMatching 2 returned **the same
430-
number of logical failures on the same 100,000 samples** — 1891 and 1596
431-
respectively. On this workload the two agree exactly.
432-
- On throughput, **PyMatching was faster than every QECTOR decoder at every
433-
distance measured here**, by roughly 3× at `d = 3` and by two orders of
434-
magnitude for `qector_blossom` by `d = 11`. This is consistent with the
435-
long-standing note elsewhere in this project that PyMatching leads on plain
436-
MWPM; it is not a regression, and it is not something the artifacts hide.
437-
438-
Do not quote these as a marketing comparison. Regenerate on quiesced hardware,
439-
and state the noise model, before any number here is used in a claim.
453+
- **Accuracy.** At `d = 3` and `d = 5`, `qector_blossom` and PyMatching 2
454+
returned *the same number of logical failures on the same 100,000 samples*
455+
1891 and 1596 respectively, identical to the digit. On this workload the two
456+
agree exactly, which is a checkable claim rather than a rounded one.
457+
- **Throughput.** PyMatching was faster than every QECTOR decoder at every
458+
distance measured here — roughly 3× at `d = 3` against `qector_unionfind`,
459+
and two orders of magnitude against `qector_blossom` by `d = 11`. That is
460+
consistent with the long-standing note elsewhere in this project that
461+
PyMatching leads on plain MWPM. It is not a regression, and the artifacts do
462+
not hide it.
463+
464+
Neither finding generalises beyond the cells above. Regenerate on quiesced
465+
hardware, and state the noise model, before any number here is used in a claim.
440466

441467
### Published, citable evidence
442468

official_benchmark_results.md

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,26 +16,26 @@ Every row below is one `ler.estimate_ler_circuit_level` measurement: the same St
1616
| PyMatching v2 (C++) | 3 | 1,000 | 13 | 0.01300 | [0.00761, 0.02211] | 1,949,317.9 | 1.00x |
1717
| QECTOR Sparse Blossom (CPU) | 3 | 1,000 | 13 | 0.01300 | [0.00761, 0.02211] | 154,268.6 | 0.08x |
1818
| QECTOR Union-Find (CPU) | 3 | 1,000 | 16 | 0.01600 | [0.00987, 0.02583] | 222,321.0 | 0.11x |
19-
| ldpc BP-OSD | 3 | 1,000 | 12 | 0.01200 | [0.00688, 0.02086] | 2,266.7 | 0.00x |
19+
| ldpc BP-OSD | 3 | 1,000 | 12 | 0.01200 | [0.00688, 0.02086] | 2,266.7 | 0.001x |
2020
| PyMatching v2 (C++) | 3 | 5,000 | 84 | 0.01680 | [0.01359, 0.02075] | 1,865,810.9 | 1.00x |
2121
| QECTOR Sparse Blossom (CPU) | 3 | 5,000 | 84 | 0.01680 | [0.01359, 0.02075] | 236,337.3 | 0.13x |
2222
| QECTOR Union-Find (CPU) | 3 | 5,000 | 105 | 0.02100 | [0.01738, 0.02536] | 481,023.6 | 0.26x |
23-
| ldpc BP-OSD | 3 | 5,000 | 87 | 0.01740 | [0.01413, 0.02141] | 2,285.6 | 0.00x |
23+
| ldpc BP-OSD | 3 | 5,000 | 87 | 0.01740 | [0.01413, 0.02141] | 2,285.6 | 0.001x |
2424
| PyMatching v2 (C++) | 3 | 10,000 | 202 | 0.02020 | [0.01762, 0.02315] | 1,848,941.5 | 1.00x |
2525
| QECTOR Sparse Blossom (CPU) | 3 | 10,000 | 202 | 0.02020 | [0.01762, 0.02315] | 248,615.8 | 0.13x |
2626
| QECTOR Union-Find (CPU) | 3 | 10,000 | 233 | 0.02330 | [0.02052, 0.02644] | 571,931.9 | 0.31x |
27-
| ldpc BP-OSD | 3 | 10,000 | 199 | 0.01990 | [0.01734, 0.02283] | 2,329.7 | 0.00x |
27+
| ldpc BP-OSD | 3 | 10,000 | 199 | 0.01990 | [0.01734, 0.02283] | 2,329.7 | 0.001x |
2828
| PyMatching v2 (C++) | 3 | 50,000 | 960 | 0.01920 | [0.01803, 0.02044] | 2,234,836.6 | 1.00x |
2929
| QECTOR Sparse Blossom (CPU) | 3 | 50,000 | 960 | 0.01920 | [0.01803, 0.02044] | 250,396.0 | 0.11x |
3030
| QECTOR Union-Find (CPU) | 3 | 50,000 | 1128 | 0.02256 | [0.02129, 0.02390] | 812,805.9 | 0.36x |
31-
| ldpc BP-OSD | 3 | 50,000 | 969 | 0.01938 | [0.01821, 0.02063] | 2,259.1 | 0.00x |
31+
| ldpc BP-OSD | 3 | 50,000 | 969 | 0.01938 | [0.01821, 0.02063] | 2,259.1 | 0.001x |
3232
| PyMatching v2 (C++) | 3 | 100,000 | 1891 | 0.01891 | [0.01808, 0.01977] | 2,437,651.0 | 1.00x |
3333
| QECTOR Sparse Blossom (CPU) | 3 | 100,000 | 1891 | 0.01891 | [0.01808, 0.01977] | 245,628.9 | 0.10x |
3434
| QECTOR Union-Find (CPU) | 3 | 100,000 | 2210 | 0.02210 | [0.02121, 0.02303] | 825,725.6 | 0.34x |
3535
| PyMatching v2 (C++) | 5 | 1,000 | 19 | 0.01900 | [0.01220, 0.02948] | 266,410.9 | 1.00x |
3636
| QECTOR Sparse Blossom (CPU) | 5 | 1,000 | 19 | 0.01900 | [0.01220, 0.02948] | 8,690.2 | 0.03x |
3737
| QECTOR Union-Find (CPU) | 5 | 1,000 | 26 | 0.02600 | [0.01780, 0.03782] | 79,887.5 | 0.30x |
38-
| ldpc BP-OSD | 5 | 1,000 | 21 | 0.02100 | [0.01378, 0.03189] | 87.8 | |
38+
| ldpc BP-OSD | 5 | 1,000 | 21 | 0.02100 | [0.01378, 0.03189] | 87.8 | 0x |
3939
| PyMatching v2 (C++) | 5 | 5,000 | 73 | 0.01460 | [0.01163, 0.01832] | 286,109.9 | 1.00x |
4040
| QECTOR Sparse Blossom (CPU) | 5 | 5,000 | 73 | 0.01460 | [0.01163, 0.01832] | 9,018.3 | 0.03x |
4141
| QECTOR Union-Find (CPU) | 5 | 5,000 | 148 | 0.02960 | [0.02525, 0.03467] | 91,362.6 | 0.32x |
@@ -73,7 +73,7 @@ Every row below is one `ler.estimate_ler_circuit_level` measurement: the same St
7373
| PyMatching v2 (C++) | 9 | 100,000 | 878 | 0.00878 | [0.00822, 0.00938] | 29,244.1 | 1.00x |
7474
| QECTOR Union-Find (CPU) | 9 | 100,000 | 1732 | 0.01732 | [0.01653, 0.01815] | 4,606.0 | 0.16x |
7575
| PyMatching v2 (C++) | 11 | 1,000 | 7 | 0.00700 | [0.00339, 0.01438] | 11,490.2 | 1.00x |
76-
| QECTOR Sparse Blossom (CPU) | 11 | 1,000 | 7 | 0.00700 | [0.00339, 0.01438] | 84.2 | 0.01x |
76+
| QECTOR Sparse Blossom (CPU) | 11 | 1,000 | 7 | 0.00700 | [0.00339, 0.01438] | 84.2 | 0.007x |
7777
| QECTOR Union-Find (CPU) | 11 | 1,000 | 13 | 0.01300 | [0.00761, 0.02211] | 367.3 | 0.03x |
7878
| PyMatching v2 (C++) | 11 | 5,000 | 24 | 0.00480 | [0.00323, 0.00713] | 12,516.9 | 1.00x |
7979
| PyMatching v2 (C++) | 11 | 10,000 | 77 | 0.00770 | [0.00617, 0.00961] | 13,693.2 | 1.00x |

official_benchmark_results.pdf

-6 Bytes
Binary file not shown.

scripts/run_custom_comparison_benchmark.py

Lines changed: 47 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,18 @@
8484
PROBE_SHOTS = 256
8585

8686

87+
def _fmt_speedup(sp) -> str:
88+
"""Format a throughput ratio without collapsing small ones to '0.00x'.
89+
90+
A decoder ~1000x slower than the reference has a ratio of 0.001; printed with
91+
two decimals that becomes '0.00x', which reads as zero throughput rather than
92+
as the (accurate, unflattering) number it is.
93+
"""
94+
if sp is None:
95+
return "—"
96+
return f"{sp:.2f}x" if sp >= 0.01 else f"{sp:.3g}x"
97+
98+
8799
def run_grid(distances, shot_list, p, seed, budget_s):
88100
"""Measure every feasible (distance, decoder, shots) cell. Never extrapolate."""
89101
rows: list[dict] = []
@@ -236,7 +248,7 @@ def export_markdown(path: Path, rows, skipped, prov, p, budget_s):
236248
md.append(
237249
f"| {r['decoder']} | {r['distance']} | {r['shots']:,} | {r['errors']} | "
238250
f"{r['ler']:.5f} | [{r['ci95_lo']:.5f}, {r['ci95_hi']:.5f}] | "
239-
f"{r['decodes_per_s']:,.1f} | {f'{sp:.2f}x' if sp else '—'} |"
251+
f"{r['decodes_per_s']:,.1f} | {_fmt_speedup(sp)} |"
240252
)
241253

242254
if skipped:
@@ -471,7 +483,7 @@ def export_pdf(path: Path, rows, skipped, prov, charts, out_dir: Path, p: float)
471483
Paragraph(f"{r['ler']:.5f}", cellb),
472484
Paragraph(f"[{r['ci95_lo']:.4f}, {r['ci95_hi']:.4f}]", cell),
473485
Paragraph(f"{r['decodes_per_s']:,.0f}", cellb),
474-
Paragraph(f"{sp:.2f}x" if sp else "—", cell),
486+
Paragraph(_fmt_speedup(sp), cell),
475487
])
476488
t = Table(data, colWidths=[138, 22, 48, 38, 52, 94, 60, 40], repeatRows=1)
477489
t.setStyle(TableStyle([
@@ -525,6 +537,12 @@ def main():
525537
"skipped rather than extrapolated.",
526538
)
527539
ap.add_argument("--out-dir", default=".")
540+
ap.add_argument(
541+
"--from-json", metavar="PATH",
542+
help="re-render CSV/Markdown/PDF/charts from an existing stamped artifact "
543+
"instead of measuring. Use for presentation fixes so the numbers stay "
544+
"byte-identical to what has already been cited.",
545+
)
528546
args = ap.parse_args()
529547

530548
distances = [int(x) for x in args.distances.split(",") if x.strip()]
@@ -536,6 +554,33 @@ def main():
536554
print(f" distances={distances} shots={shot_list} p={args.p} seed={args.seed}")
537555
print(f" per-cell decode budget: {args.budget_seconds:g}s (no extrapolation)")
538556

557+
if args.from_json:
558+
# Re-render the reports from an existing stamped artifact without
559+
# re-measuring. A presentation fix must not silently change the numbers
560+
# underneath it: re-running would produce a different (equally valid)
561+
# sample, and any figure already quoted elsewhere would quietly stop
562+
# matching the artifact it cites.
563+
src = Path(args.from_json).resolve()
564+
rows, prov = _provenance.load_artifact(src)
565+
if prov is None:
566+
print(f"{src} carries no provenance block - refusing to re-render it.")
567+
return 1
568+
skipped = prov.get("parameters", {}).get("skipped_cells", [])
569+
p = prov.get("parameters", {}).get("physical_error_rate", args.p)
570+
budget = prov.get("parameters", {}).get("per_cell_budget_seconds", args.budget_seconds)
571+
assert_comparable(rows)
572+
print(f"Re-rendering {len(rows)} measured / {len(skipped)} skipped cells from {src.name}")
573+
export_csv(out_dir / "official_benchmark_results.csv", rows)
574+
export_markdown(
575+
out_dir / "official_benchmark_results.md", rows, skipped, prov, p, budget
576+
)
577+
charts = generate_charts(rows, out_dir, p)
578+
export_pdf(
579+
out_dir / "official_benchmark_results.pdf", rows, skipped, prov, charts, out_dir, p
580+
)
581+
print("\nDone - measurements untouched, reports re-rendered.")
582+
return 0
583+
539584
t0 = time.perf_counter()
540585
rows, skipped = run_grid(distances, shot_list, args.p, args.seed, args.budget_seconds)
541586
elapsed = time.perf_counter() - t0

0 commit comments

Comments
 (0)