open-standards/wai/wai-quantum/examples/qec_memory/pairs.rs
Transaction Science 86c2a5c393
Some checks failed
WAI breaking changes and the conformance runner / WAI breaking · does this change touch WAI (push) Successful in 23s
WAI examples / WAI examples · does this change touch them (push) Successful in 29s
wai-quantum for JavaScript / does this change touch it (push) Successful in 41s
wai-quantum docs, course and locales / does this change touch it (push) Successful in 45s
wai-audit collector and player integration / does this change touch it (push) Successful in 59s
wai-quantum resource estimation / does this change touch it (push) Successful in 46s
wai-quantum docs, course and locales / the Studio's words in every locale · linux (push) Successful in 21s
wai-quantum wasmQ / does this change touch it (push) Successful in 1m19s
WAI operation catalogue / does this change touch it (push) Successful in 1m20s
wai-quantum for Python / does this change touch it (push) Successful in 40s
wai-quantum error correction and mitigation / does this change touch it (push) Successful in 41s
wai-quantum service / does this change touch it (push) Successful in 41s
wai-quantum extension points / does this change touch it (push) Successful in 42s
wai-quantum pulse and dynamics / does this change touch it (push) Successful in 1m41s
wai-quantum editors / does this change touch it (push) Successful in 2m8s
wai-quantum command line / does this change touch it (push) Successful in 2m11s
wai-quantum other formats / does this change touch it (push) Successful in 2m12s
wai-quantum clippy / does this change touch it (push) Successful in 2m12s
wai-quantum contract / does this change touch it (push) Successful in 2m12s
wai-quantum OpenQASM reader / does this change touch it (push) Successful in 2m13s
wai-quantum studio and desktop / does this change touch it (push) Successful in 53s
wai-quantum wasmQ / the reference modules rebuild to their modules of record · linux (push) Successful in 56s
wai-quantum variational and algorithm library / does this change touch it (push) Successful in 53s
wai-quantum verdicts and repairs / does this change touch it (push) Successful in 59s
wai-sink-android native sink library / does this change touch it (push) Successful in 57s
wai-quantum views in agent hosts / does this change touch it (push) Successful in 59s
WAI Studio / does this change touch it (push) Successful in 55s
wai-sink-swift native sink package / does this change touch it (push) Successful in 1m25s
wai-audit collector and player integration / collector, player integration and the browser end to end · macos (push) Successful in 2m40s
wai-quantum pulse and dynamics / the same bits · macos (push) Successful in 1m46s
wai-audit collector and player integration / collector · linux (push) Successful in 4m6s
wai-quantum wasmQ / profile, engines, reference modules, adversarial, vectors · macos (push) Successful in 3m38s
wai-quantum for Python / python · macos (push) Successful in 3m48s
wai-quantum pulse and dynamics / the same bits · linux-arm64 (push) Successful in 3m59s
wai-quantum editors / editors · macos (push) Successful in 2m28s
wai-quantum pulse and dynamics / reader, simulator and pulse jobs under wasm32-wasip2 (wasmtime) (push) Successful in 4m52s
wai-quantum docs, course and locales / the course and the command line's languages · linux (push) Successful in 5m56s
wai-quantum service / service energy · linux packages (push) Successful in 5m23s
wai-quantum command line / command line · macos (push) Successful in 3m24s
WAI breaking changes and the conformance runner / WAI breaking · wire, record and API against main · linux (push) Successful in 8m10s
wai-quantum wasmQ / interpreter and vectors under wasm32-wasip2 (wasmtime) (push) Successful in 7m52s
wai-quantum contract / the policy is linked, and the deprecations page reads (push) Successful in 10s
wai-quantum service / service · macos (push) Successful in 8m1s
wai-quantum variational and algorithm library / parameters, gradients, optimisers, VQE, QAOA and builders · linux-arm64, release (push) Successful in 7m32s
wai-quantum pulse and dynamics / reader, simulator and pulse jobs · linux, release (push) Successful in 8m36s
wai-quantum editors / editors · linux (push) Successful in 8m4s
wai-quantum extension points / the registry under wasm32-wasip2 (wasmtime) (push) Successful in 8m53s
wai-quantum wasmQ / profile, engines, reference modules, adversarial, vectors · linux (push) Successful in 9m7s
wai-sink-android native sink library / the JNI layer and the platform-free half on the host JVM · linux-arm64 (push) Successful in 8m4s
wai-quantum studio and desktop / desktop app · macos (push) Successful in 7m29s
wai-quantum OpenQASM reader / corpus under wasm32-wasip2 (wasmtime) (push) Successful in 1m18s
repository checks / lockfiles, advisories, tests in CI, workflow hygiene, spec and section references, public text (push) Failing after 12m8s
wai-quantum contract / API compatibility · linux (push) Successful in 5m16s
wai-quantum for JavaScript / javascript · linux (push) Successful in 11m27s
wai-quantum extension points / the registry and the example · linux-arm64 (push) Successful in 12m6s
WAI examples / WAI examples · android-app · linux-arm64 (push) Successful in 13m41s
wai-sink-android native sink library / every ABI, the AAR, and the instrumented tests in a headless emulator · macos (push) Successful in 9m15s
wai-quantum service / service · linux (push) Successful in 12m53s
wai-quantum resource estimation / estimation and calibration · linux, release (push) Successful in 14m1s
wai-quantum verdicts and repairs / verdicts, repairs and the intent set · linux (push) Successful in 3m10s
wai-quantum verdicts and repairs / the intent set and the verdict under wasm32-wasip2 (wasmtime) (push) Successful in 3m22s
wai-quantum variational and algorithm library / parameters, gradients, optimisers, VQE, QAOA and builders · linux, release (push) Successful in 4m10s
wai-quantum error correction and mitigation / codes, decoders, texts and mitigation · linux, release (push) Successful in 14m45s
WAI Studio / the catalogue's actions, the unit tests and the core · linux (push) Successful in 1m9s
WAI Studio / Studio in Chromium and Firefox · macos (push) Successful in 11m39s
wai-quantum extension points / the registry and the example · linux (push) Successful in 15m22s
wai-quantum for Python / python · linux (push) Successful in 15m45s
wai-quantum other formats / the set under wasm32-wasip2 (wasmtime) (push) Successful in 7m23s
wai-quantum clippy / clippy, warnings denied, every feature set · linux (push) Successful in 11m15s
WAI operation catalogue / the catalogue and every door · linux (push) Successful in 17m27s
wai-quantum other formats / readers, writers and the set · linux (push) Successful in 10m16s
wai-quantum views in agent hosts / views · the page and its engine (push) Successful in 5m18s
wai-quantum OpenQASM reader / reader and corpus · linux (push) Successful in 10m1s
wai-quantum studio and desktop / desktop app · linux (push) Successful in 10m56s
WAI examples / WAI examples · the archive, the readiness check, the browser and Apple · macos (push) Successful in 21m33s
wai-quantum views in agent hosts / views · a scripted host (push) Successful in 7m1s
wai-quantum studio and desktop / studio · browser (push) Successful in 13m47s
WAI examples / WAI examples · portable · linux (push) Successful in 24m8s
wai-quantum studio and desktop / desktop app · windows (push) Successful in 17m57s
WAI breaking changes and the conformance runner / WAI breaking and conform · their tests, and the independent verifiers through wai conform · linux (push) Successful in 29m1s
WAI Studio / Studio in WebKit · linux (push) Successful in 16m2s
wai-quantum contract / schemas and the committed tool lists · linux (push) Successful in 25m13s
wai-quantum command line / command line · linux (push) Successful in 28m51s
wai-sink-swift native sink package / libwai slices, desktop tests, simulator tests · macos (push) Successful in 28m15s
wai-quantum docs, course and locales / every command and snippet in the docs · linux (push) Successful in 36m16s
qec_memory: held-out shot windows, every decoder's predictions kept, and paired tests against the published decoders
The surface-code memory example (examples/qec_memory) gains what a paired
comparison with the published decoders needs:

- survey --skip K decodes shots K to K + N of each experiment (K even, so
  every shot keeps its parity and the model that parity picks), and scores
  the published decoders on the same shots: shots a decoder's settings were
  never chosen on.
- survey --predictions DIR now also writes the published decoders'
  predictions on the same shots and the record's own flips, one byte a
  shot, beside this crate's.
- Every receipt records its first shot, and per experiment, for each of
  this crate's decoders against each published decoder, the shots only one
  of the two got wrong.
- qec_memory pairs SET KEY@RECEIPT... tests those counts: the exact
  two-sided McNemar test per distance and over every distance given, the
  fitted rates and Lambda of both, and a paired bootstrap (every fitted
  experiment's shots drawn again with the two decoders' outcomes on a shot
  kept together, each replicate from its own seed) of each distance's
  rate ratio and of the difference in Lambda.

No receipt changes here; the runs that use it follow.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-13 07:31:16 -05:00

271 lines
12 KiB
Rust
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//! This crate's decoder against each published decoder of a record, shot by
//! shot: exact McNemar tests on the shots only one of the two got wrong, and
//! a paired bootstrap of the fitted rates and of `Λ`.
//!
//! ```text
//! qec_memory pairs SET [--replicates N] [--seed S] [--threads T] [email protected]...
//! ```
//!
//! Each `KEY@RECEIPT` names this crate's decoder (`decoder|prior`) in a
//! receipt `survey` sealed with its discordant counts; the receipts' fitted
//! experiments are pooled (a receipt for each distance, say). For every
//! published decoder scored in all of them:
//!
//! - per distance, and over every distance given, the failures of each and
//! the discordant shots `b` (this decoder wrong, the published one right)
//! and `c` (the reverse), with the exact two-sided McNemar p-value: under
//! the null that the two fail alike, each discordant shot is either way
//! with probability one half, `p = min(1, 2·P(X ≤ min(b, c)))`,
//! `X ~ Bin(b + c, 1/2)`;
//! - the fitted `ε` per distance and `Λ` of each, as `survey` fits them;
//! - a paired bootstrap: in each replicate every fitted experiment's shots are
//! drawn again with replacement, the two decoders' outcomes on a shot kept
//! together (the experiment's joint table of right and wrong is all a
//! draw needs), and both are fitted again. The 2.5 and 97.5 percentiles of
//! `Λ_this − Λ_published` and of `ε_this/ε_published` per distance, and the
//! fraction of replicates where this decoder's `Λ` is not above the
//! published one's. Replicate `r` draws from its own generator, seeded
//! from `S` and `r`, so the result does not depend on the threads.
use crate::data::Set;
use serde_json::{Value, json};
use std::collections::BTreeMap;
use wai_quantum::quantum_memory::{RoundPoint, fit_decay, fit_lambda, mean_rate};
/// One fitted experiment, against one published decoder: its series
/// (distance, patch, basis), rounds and shots, and the joint table
/// `[both wrong, only this decoder, only the published one]` (both right
/// is the rest).
#[derive(Clone)]
struct Joint {
series: (u32, String, String),
rounds: u32,
shots: u64,
both: u64,
only_ours: u64,
only_theirs: u64,
}
/// The exact two-sided McNemar p-value for `b` and `c` discordant shots.
pub fn mcnemar(b: u64, c: u64) -> f64 {
let n = b + c;
if n == 0 {
return 1.0;
}
let k = b.min(c);
// P(X ≤ k) as a sum of terms in log space, from the largest:
// ln(C(n, i)/2ⁿ), each from the one before.
let mut terms = Vec::with_capacity(k as usize + 1);
let mut t = -(n as f64) * std::f64::consts::LN_2;
for i in 0..=k {
terms.push(t);
t += ((n - i) as f64).ln() - ((i + 1) as f64).ln();
}
let top = terms.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
let tail = top.exp() * terms.iter().map(|t| (t - top).exp()).sum::<f64>();
(2.0 * tail).min(1.0)
}
/// Fitted ε per distance and Λ from per-experiment failures.
fn fit(joints: &[Joint], errors: impl Fn(&Joint) -> u64) -> (BTreeMap<u32, f64>, Option<f64>) {
let mut groups: BTreeMap<&(u32, String, String), Vec<RoundPoint>> = BTreeMap::new();
for j in joints {
groups.entry(&j.series).or_default().push(RoundPoint { rounds: j.rounds, shots: j.shots, errors: errors(j) });
}
let mut per_d: BTreeMap<u32, Vec<_>> = BTreeMap::new();
for (series, pts) in &groups {
if let Some(f) = fit_decay(pts, 0) {
per_d.entry(series.0).or_default().push(f);
}
}
let mut eps = BTreeMap::new();
let mut lam_in = Vec::new();
for (d, fits) in &per_d {
let m = mean_rate(fits).unwrap();
eps.insert(*d, m.eps);
lam_in.push((*d, m.eps, m.sigma_independent));
}
(eps, fit_lambda(&lam_in).map(|l| l.0))
}
struct SplitMix(u64);
impl SplitMix {
fn next(&mut self) -> u64 {
self.0 = self.0.wrapping_add(0x9e37_79b9_7f4a_7c15);
let mut z = self.0;
z = (z ^ (z >> 30)).wrapping_mul(0xbf58_476d_1ce4_e5b9);
z = (z ^ (z >> 27)).wrapping_mul(0x94d0_49bb_1331_11eb);
z ^ (z >> 31)
}
/// Uniform below `n` (Lemire's multiply, the bias below 2⁻⁴⁰ for the
/// shot counts here).
fn below(&mut self, n: u64) -> u64 {
((u128::from(self.next()) * u128::from(n)) >> 64) as u64
}
}
/// One replicate: every experiment's shots drawn again.
fn redraw(joints: &[Joint], rng: &mut SplitMix) -> Vec<Joint> {
joints
.iter()
.map(|j| {
let (mut both, mut ours, mut theirs) = (0, 0, 0);
let (a, b, c) = (j.both, j.both + j.only_ours, j.both + j.only_ours + j.only_theirs);
for _ in 0..j.shots {
let r = rng.below(j.shots);
if r < a {
both += 1;
} else if r < b {
ours += 1;
} else if r < c {
theirs += 1;
}
}
Joint { both, only_ours: ours, only_theirs: theirs, ..j.clone() }
})
.collect()
}
fn quantile(v: &mut [f64], q: f64) -> f64 {
v.sort_by(|a, b| a.total_cmp(b));
v[((v.len() as f64 - 1.0) * q).round() as usize]
}
pub fn pairs(a: &[String]) {
let set = Set::parse(a.first().map(String::as_str).unwrap_or("")).unwrap_or_else(|| crate::usage());
let (mut replicates, mut seed, mut threads) = (2000usize, 1u64, 8usize);
let mut inputs: Vec<(String, String)> = Vec::new();
let mut i = 1;
while i < a.len() {
match a[i].as_str() {
"--replicates" | "--seed" | "--threads" => {
let v = a.get(i + 1).unwrap_or_else(|| crate::usage());
match a[i].as_str() {
"--replicates" => replicates = v.parse().unwrap(),
"--seed" => seed = v.parse().unwrap(),
_ => threads = v.parse().unwrap(),
}
i += 2;
}
x => {
let (k, p) = x.rsplit_once('@').unwrap_or_else(|| crate::usage());
inputs.push((k.to_string(), p.to_string()));
i += 1;
}
}
}
// Every fitted experiment: this crate's key, its counts and its discordant table.
let mut exps: Vec<(String, Value)> = Vec::new();
for (key, path) in &inputs {
let doc: Value = serde_json::from_str(&std::fs::read_to_string(path).unwrap_or_else(|e| panic!("{path}: {e}"))).unwrap();
assert!(wai_quantum::engine::receipt::verify(&doc["request"], &doc["result"], &doc["receipt"]), "{path}: the receipt does not verify");
for e in doc["result"]["experiments"].as_array().unwrap() {
if set.fitted(e["rounds"].as_u64().unwrap() as u32) {
assert!(!e["discordant"][key.as_str()].is_null(), "{path}: no discordant counts for {key} (sealed before survey kept them?)");
exps.push((key.clone(), e.clone()));
}
}
}
let mut published: Vec<String> = exps[0].1["discordant"][exps[0].0.as_str()].as_object().unwrap().keys().cloned().collect();
published.retain(|p| exps.iter().all(|(k, e)| !e["discordant"][k.as_str()][p.as_str()].is_null()));
let mut out = Vec::new();
for p in &published {
let joints: Vec<Joint> = exps
.iter()
.map(|(k, e)| {
let n = |key: &str| e["errors"][key].as_u64().unwrap();
let d = &e["discordant"][k.as_str()][p.as_str()];
let (b, c) = (d[0].as_u64().unwrap(), d[1].as_u64().unwrap());
let both = n(k) - b;
assert_eq!(both, n(p) - c, "{}: the joint table adds up", e["experiment"]);
Joint {
series: (e["distance"].as_u64().unwrap() as u32, e["patch"].as_str().unwrap().into(), e["basis"].as_str().unwrap().into()),
rounds: e["rounds"].as_u64().unwrap() as u32,
shots: e["shots"].as_u64().unwrap(),
both,
only_ours: b,
only_theirs: c,
}
})
.collect();
let ours = |j: &Joint| j.both + j.only_ours;
let theirs = |j: &Joint| j.both + j.only_theirs;
let mut tests = serde_json::Map::new();
let distances: Vec<u32> = {
let mut v: Vec<u32> = joints.iter().map(|j| j.series.0).collect();
v.sort_unstable();
v.dedup();
v
};
let mut pooled = |label: String, js: Vec<&Joint>| {
let (fo, ft) = (js.iter().map(|j| ours(j)).sum::<u64>(), js.iter().map(|j| theirs(j)).sum::<u64>());
let (b, c) = (js.iter().map(|j| j.only_ours).sum::<u64>(), js.iter().map(|j| j.only_theirs).sum::<u64>());
let shots: u64 = js.iter().map(|j| j.shots).sum();
println!("{p} {label}: {} experiments, {shots} shots; failures {fo} (this) against {ft}; discordant {b} against {c}; McNemar p = {:.3e}", js.len(), mcnemar(b, c));
tests.insert(label, json!({ "experiments": js.len(), "shots": shots, "failures": [fo, ft], "discordant": [b, c], "mcnemar_p": mcnemar(b, c) }));
};
for &d in &distances {
pooled(format!("d{d}"), joints.iter().filter(|j| j.series.0 == d).collect());
}
if distances.len() > 1 {
pooled("all".into(), joints.iter().collect());
}
let (eo, lo) = fit(&joints, ours);
let (et, lt) = fit(&joints, theirs);
println!("{p}: eps this {eo:?} Lambda {lo:?}; eps published {et:?} Lambda {lt:?}");
// The bootstrap, replicates spread over threads, each from its own seed.
let reps: Vec<(BTreeMap<u32, f64>, Option<f64>)> = {
let next = std::sync::atomic::AtomicUsize::new(0);
let slots = std::sync::Mutex::new(vec![None; replicates]);
std::thread::scope(|s| {
for _ in 0..threads.max(1) {
s.spawn(|| {
loop {
let r = next.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
if r >= replicates {
break;
}
let mut rng = SplitMix(seed ^ (r as u64).wrapping_mul(0xd1b5_4a32_d192_ed03));
let js = redraw(&joints, &mut rng);
let (a, la) = fit(&js, ours);
let (b, lb) = fit(&js, theirs);
let ratio: BTreeMap<u32, f64> = a.iter().filter_map(|(d, x)| b.get(d).map(|y| (*d, x / y))).collect();
let dl = la.zip(lb).map(|(x, y)| x - y);
slots.lock().unwrap()[r] = Some((ratio, dl));
}
});
}
});
slots.into_inner().unwrap().into_iter().map(Option::unwrap).collect()
};
let mut boot = serde_json::Map::new();
for &d in &distances {
let mut v: Vec<f64> = reps.iter().filter_map(|r| r.0.get(&d).copied()).collect();
if v.is_empty() {
continue;
}
let above = v.iter().filter(|&&x| x >= 1.0).count() as f64 / v.len() as f64;
let (lo95, hi95) = (quantile(&mut v, 0.025), quantile(&mut v, 0.975));
println!("{p} d{d}: eps this/published, 95% interval [{lo95:.4}, {hi95:.4}], ratio at least one in {:.4} of {} replicates", above, v.len());
boot.insert(format!("eps_ratio_d{d}"), json!({ "p2_5": lo95, "p97_5": hi95, "fraction_at_least_one": above, "replicates": v.len() }));
}
let mut dl: Vec<f64> = reps.iter().filter_map(|r| r.1).collect();
if !dl.is_empty() {
let not_above = dl.iter().filter(|&&x| x <= 0.0).count() as f64 / dl.len() as f64;
let (lo95, hi95) = (quantile(&mut dl, 0.025), quantile(&mut dl, 0.975));
println!("{p}: Lambda this − published, 95% interval [{lo95:.4}, {hi95:.4}], not above zero in {not_above:.4} of {} replicates", dl.len());
boot.insert("lambda_difference".into(), json!({ "p2_5": lo95, "p97_5": hi95, "fraction_not_above_zero": not_above, "replicates": dl.len() }));
}
out.push(json!({
"published": p, "tests": tests,
"eps_this": eo, "lambda_this": lo, "eps_published": et, "lambda_published": lt,
"bootstrap": boot,
}));
}
let doc = json!({
"set": set.name(), "inputs": inputs.iter().map(|(k, p)| format!("{k}@{p}")).collect::<Vec<_>>(),
"replicates": replicates, "seed": seed, "pairs": out,
});
println!("{doc}");
}