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
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]>
562 lines
28 KiB
Rust
562 lines
28 KiB
Rust
//! Surface-code memory experiments decoded from the processors' own records,
|
||
//! with this crate's decoders, and the published decoders' predictions on the
|
||
//! same shots scored the same way.
|
||
//!
|
||
//! ```text
|
||
//! qec_memory survey SET DIR --decoders mwpm,uf,corr --prior PRIOR [--threads N] [--shots N] [--skip N] [--only SUBSTR] [--predictions DIR] --out FILE.jsonl [--receipt FILE.json]
|
||
//! qec_memory fit SET FILE.jsonl...
|
||
//! qec_memory pairs SET [--replicates N] [--seed S] [--threads T] [email protected]...
|
||
//! ```
|
||
//!
|
||
//! `--shots N --skip K` decodes shots `K` to `K + N` of each experiment (`K`
|
||
//! even), and scores the published decoders on the same shots. With
|
||
//! `--predictions DIR` every decoder's predictions are written there, one
|
||
//! byte a shot, with the published decoders' (`_published_NAME.b8`) and the
|
||
//! record's own flips (`_actual.b8`). A receipt carries, for each of this
|
||
//! crate's decoders against each published one, the shots only one of the
|
||
//! two got wrong; `pairs` tests those (see `pairs.rs`).
|
||
//!
|
||
//! `SET` is `2023` (doi:10.5281/zenodo.6804040, the unpacked archive of
|
||
//! the 2023 distance-3 and distance-5 experiment) or `2024`
|
||
//! (doi:10.5281/zenodo.13273331, the unpacked archive of the 105-qubit
|
||
//! distance-3, 5 and 7 experiment); both are CC BY 4.0, credited in
|
||
//! `tests/data/qec_memory/README.md`. `DIR` is the
|
||
//! directory the archive unpacked into. Priors: `pij` (2023: the models fitted
|
||
//! to the data, each decoding the other half of the shots), `circuit` (2023:
|
||
//! the noisy circuit's model), `si1000` and `rl` (2024: the two published
|
||
//! decoder priors), `fit` (this crate's own fit to the detection events, each
|
||
//! half of the shots decoded with the model fitted to the other half).
|
||
|
||
mod data;
|
||
mod pairs;
|
||
mod receipt;
|
||
|
||
use data::Set;
|
||
use serde_json::{Value, json};
|
||
use std::path::{Path, PathBuf};
|
||
use std::time::Instant;
|
||
use wai_quantum::quantum_frame::ErrorModel;
|
||
use wai_quantum::quantum_match::{CorrelatedMatching, MatchingGraph};
|
||
use wai_quantum::quantum_memory::{BeliefMatcher, CorrMatcher, DecayFit, Matcher, MlDecoder, RoundPoint, fit_decay, fit_lambda, mean_rate, predict};
|
||
use wai_quantum::quantum_memory_stream::{Inner, ParallelWindows, Plan};
|
||
use wai_quantum::quantum_memory_tn::{SparseMl, TnDecoder};
|
||
use wai_quantum::quantum_uf::UnionFind;
|
||
|
||
fn usage() -> ! {
|
||
eprintln!("usage: qec_memory survey SET DIR --decoders LIST --prior PRIOR [--threads N] [--shots N] [--skip N] [--only SUBSTR] [--predictions DIR] --out FILE.jsonl [--receipt FILE.json]\n qec_memory fit SET FILE.jsonl...\n qec_memory pairs SET [--replicates N] [--seed S] [--threads T] [email protected]...");
|
||
std::process::exit(2)
|
||
}
|
||
|
||
fn main() {
|
||
let a: Vec<String> = std::env::args().collect();
|
||
match a.get(1).map(String::as_str) {
|
||
Some("survey") => survey(&a[2..]),
|
||
Some("fit") => fit(&a[2..]),
|
||
Some("pairs") => pairs::pairs(&a[2..]),
|
||
_ => usage(),
|
||
}
|
||
}
|
||
|
||
/// One decoder, built for one model.
|
||
enum Dec {
|
||
Mwpm(Matcher),
|
||
Uf(UnionFind),
|
||
Corr0(CorrelatedMatching),
|
||
Corr(CorrMatcher),
|
||
Bm(Box<BeliefMatcher>),
|
||
Ml(MlDecoder),
|
||
Tn(Box<TnDecoder>),
|
||
Pw(Box<ParallelWindows>),
|
||
Sp(Box<SparseMl>),
|
||
}
|
||
|
||
/// The largest table the exact decoder may use, in bits of index.
|
||
const ML_BITS: u32 = 22;
|
||
|
||
impl Dec {
|
||
fn build(name: &str, m: &ErrorModel, distance: u32) -> Result<Dec, String> {
|
||
Ok(match name {
|
||
// ufw, mwpmw: parallel windows (commit = buffer = gap = d) of union-find or exact matching;
|
||
// ufw<B>, mwpmw<B>: the same with buffer = gap = B rounds.
|
||
w if w.starts_with("ufw") || w.starts_with("mwpmw") => {
|
||
let (inner, rest) = if let Some(r) = w.strip_prefix("ufw") { (Inner::UnionFind, r) } else { (Inner::Matching, &w[5..]) };
|
||
let mut plan = Plan::of_distance(distance);
|
||
if !rest.is_empty() {
|
||
let b: u32 = rest.parse().map_err(|_| format!("{w}: a buffer in rounds"))?;
|
||
plan.buffer = b;
|
||
plan.gap = b;
|
||
}
|
||
Dec::Pw(Box::new(ParallelWindows::from_model(m, plan, inner)?))
|
||
}
|
||
"mwpm" => Dec::Mwpm(Matcher::from_model(m).expect("a graphlike model")),
|
||
"uf" => Dec::Uf(UnionFind::new(&MatchingGraph::from_model(m).expect("a graphlike model"))),
|
||
"corr0" => Dec::Corr0(CorrelatedMatching::from_model(m).expect("a graphlike model")),
|
||
"corr" => Dec::Corr(CorrMatcher::from_model(m).expect("a graphlike model")),
|
||
"corr2" => {
|
||
let mut c = CorrMatcher::from_model(m).expect("a graphlike model");
|
||
c.always_second = true;
|
||
Dec::Corr(c)
|
||
}
|
||
b if b.starts_with("bm") => {
|
||
// bm[ITERATIONS][h]: h returns propagation's own answer when it explains the syndrome.
|
||
let hard = b.ends_with('h');
|
||
let digits = b.trim_start_matches("bm").trim_end_matches('h');
|
||
let mut d = BeliefMatcher::from_model(m, if digits.is_empty() { 20 } else { digits.parse().unwrap() }).expect("a graphlike model");
|
||
d.trust_converged = hard;
|
||
Dec::Bm(Box::new(d))
|
||
}
|
||
"ml" => Dec::Ml(MlDecoder::from_model(m, ML_BITS)?),
|
||
// tn<CHI>[e<K>]: the boundary matrix-product state at bond CHI, singular values
|
||
// below 10^-K of each cut's largest dropped (K = 14 when not given).
|
||
// sp<CHI>[e<K>]: the same elimination with the table kept sparse, cut to its CHI
|
||
// largest entries, entries below 10^-K of the largest dropped at each close (K = 12).
|
||
// A trailing b on either: the truncation weighed by belief propagation's posteriors
|
||
// (20 sweeps) instead of the priors.
|
||
t if t.starts_with("sp") => {
|
||
let (body, bp) = t.strip_suffix('b').map_or((t, false), |x| (x, true));
|
||
let (chi, k) = body[2..].split_once('e').unwrap_or((&body[2..], "12"));
|
||
let bad = |_| format!("{t}: sp, a number of entries, and e with an exponent");
|
||
let mut d = SparseMl::from_model(m, chi.parse().map_err(bad)?)?;
|
||
d.cutoff = format!("1e-{}", k.parse::<u32>().map_err(bad)?).parse().unwrap();
|
||
if bp {
|
||
d.use_belief_propagation(m, 20)?;
|
||
}
|
||
Dec::Sp(Box::new(d))
|
||
}
|
||
t if t.starts_with("tn") => {
|
||
let (body, bp) = t.strip_suffix('b').map_or((t, false), |x| (x, true));
|
||
let (chi, k) = body[2..].split_once('e').unwrap_or((&body[2..], "14"));
|
||
let bad = |_| format!("{t}: tn, a bond dimension, and e with an exponent");
|
||
let mut d = TnDecoder::from_model(m, chi.parse().map_err(bad)?)?;
|
||
// Read as decimal text, correctly rounded: the same double everywhere.
|
||
d.cutoff = format!("1e-{}", k.parse::<u32>().map_err(bad)?).parse().unwrap();
|
||
if bp {
|
||
d.use_belief_propagation(m, 20)?;
|
||
}
|
||
Dec::Tn(Box::new(d))
|
||
}
|
||
other => panic!("no decoder {other}"),
|
||
})
|
||
}
|
||
fn describe(&self) -> Value {
|
||
match self {
|
||
Dec::Ml(d) => json!({ "width": d.width(), "mechanisms": d.mechanisms(), "work_per_shot": d.work_per_shot() }),
|
||
Dec::Bm(b) => json!({ "iterations": b.iterations, "trust_converged": b.trust_converged }),
|
||
Dec::Pw(p) => {
|
||
let (a, b) = p.windows();
|
||
json!({ "plan": format!("{:?}", p.plan()), "layer_a": a.len(), "layer_b": b.len(), "rounds_decoded": p.rounds_decoded(), "rounds": p.rounds() })
|
||
}
|
||
Dec::Sp(t) => json!({ "chi": t.chi, "cutoff": t.cutoff, "width": t.width(), "mechanisms": t.mechanisms() }),
|
||
Dec::Tn(t) => json!({ "chi": t.chi, "cutoff": t.cutoff, "width": t.width(), "mechanisms": t.mechanisms(), "layout": format!("{:?}", t.layout()), "mean_span": t.mean_span() }),
|
||
_ => Value::Null,
|
||
}
|
||
}
|
||
}
|
||
|
||
enum Work {
|
||
M(wai_quantum::quantum_memory::MatchWork),
|
||
U(wai_quantum::quantum_uf::Workspace),
|
||
C(wai_quantum::quantum_memory::CorrWork),
|
||
B(Box<wai_quantum::quantum_memory::BeliefWork>),
|
||
L(wai_quantum::quantum_memory::MlWork),
|
||
T(Box<wai_quantum::quantum_memory_tn::TnWork>),
|
||
P(Box<wai_quantum::quantum_memory_stream::StreamWork>),
|
||
S(Box<wai_quantum::quantum_memory_tn::SparseWork>),
|
||
N,
|
||
}
|
||
|
||
impl Dec {
|
||
fn work(&self) -> Work {
|
||
match self {
|
||
Dec::Mwpm(m) => Work::M(m.workspace()),
|
||
Dec::Uf(u) => Work::U(u.workspace()),
|
||
Dec::Corr0(_) => Work::N,
|
||
Dec::Corr(c) => Work::C(c.workspace()),
|
||
Dec::Bm(b) => Work::B(Box::new(b.workspace())),
|
||
Dec::Ml(l) => Work::L(l.workspace()),
|
||
Dec::Tn(t) => Work::T(Box::new(t.workspace())),
|
||
Dec::Pw(p) => Work::P(Box::new(p.workspace())),
|
||
Dec::Sp(t) => Work::S(Box::new(t.workspace())),
|
||
}
|
||
}
|
||
fn decode(&self, w: &mut Work, fired: &[u32]) -> Result<u64, String> {
|
||
match (self, w) {
|
||
(Dec::Mwpm(m), Work::M(ws)) => m.decode(fired, ws).map_err(|e| format!("{e:?}")),
|
||
(Dec::Uf(u), Work::U(ws)) => u.decode(fired, ws).map_err(|e| format!("{e:?}")),
|
||
(Dec::Corr0(c), _) => c.decode(fired).map_err(|e| format!("{e:?}")),
|
||
(Dec::Corr(c), Work::C(ws)) => c.decode(fired, ws).map_err(|e| format!("{e:?}")),
|
||
(Dec::Bm(b), Work::B(ws)) => b.decode(fired, ws).map_err(|e| format!("{e:?}")),
|
||
(Dec::Ml(l), Work::L(ws)) => l.decode(fired, ws),
|
||
(Dec::Tn(t), Work::T(ws)) => {
|
||
let r = t.decode(fired, ws);
|
||
note(ws.last_forced, ws.last_max_bond as u64);
|
||
r
|
||
}
|
||
(Dec::Pw(p), Work::P(ws)) => p.decode(fired, ws).map_err(|e| format!("{e:?}")),
|
||
(Dec::Sp(t), Work::S(ws)) => {
|
||
let r = t.decode(fired, ws);
|
||
note(ws.last_forced, ws.last_max_entries as u64);
|
||
r
|
||
}
|
||
_ => unreachable!(),
|
||
}
|
||
}
|
||
}
|
||
|
||
/// Over one decoder's run: shots whose truncation dropped every explanation of
|
||
/// some detector (closed against its bit), and the largest bond or table.
|
||
static FORCED_SHOTS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
|
||
static LARGEST: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
|
||
|
||
fn note(forced: u32, size: u64) {
|
||
use std::sync::atomic::Ordering::Relaxed;
|
||
if forced > 0 {
|
||
FORCED_SHOTS.fetch_add(1, Relaxed);
|
||
}
|
||
LARGEST.fetch_max(size, Relaxed);
|
||
}
|
||
|
||
struct Args {
|
||
decoders: Vec<String>,
|
||
prior: String,
|
||
threads: usize,
|
||
shots: Option<usize>,
|
||
only: Vec<String>,
|
||
out: PathBuf,
|
||
receipt: Option<PathBuf>,
|
||
/// Fit priors to every shot of the record, even when fewer are decoded.
|
||
fit_all: bool,
|
||
/// Write each decoder's predictions here, `b8`, one file an experiment.
|
||
predictions: Option<PathBuf>,
|
||
/// The first shot decoded (shots before it are left out); even.
|
||
skip: usize,
|
||
}
|
||
|
||
fn parse_args(a: &[String]) -> Args {
|
||
let mut r = Args { decoders: vec!["mwpm".into()], prior: String::new(), threads: 8, shots: None, only: Vec::new(), out: PathBuf::new(), receipt: None, fit_all: false, predictions: None, skip: 0 };
|
||
let mut i = 0;
|
||
while i < a.len() {
|
||
if a[i] == "--fit-all" {
|
||
r.fit_all = true;
|
||
i += 1;
|
||
continue;
|
||
}
|
||
let v = a.get(i + 1).cloned().unwrap_or_else(|| usage());
|
||
match a[i].as_str() {
|
||
"--decoders" => r.decoders = v.split(',').map(str::to_string).collect(),
|
||
"--prior" => r.prior = v,
|
||
"--threads" => r.threads = v.parse().unwrap(),
|
||
"--shots" => r.shots = Some(v.parse().unwrap()),
|
||
"--only" => r.only = v.split(',').map(str::to_string).collect(),
|
||
"--out" => r.out = PathBuf::from(v),
|
||
"--receipt" => r.receipt = Some(PathBuf::from(v)),
|
||
"--predictions" => r.predictions = Some(PathBuf::from(v)),
|
||
"--skip" => r.skip = v.parse().unwrap_or_else(|_| usage()),
|
||
_ => usage(),
|
||
}
|
||
i += 2;
|
||
}
|
||
if r.prior.is_empty() || r.out.as_os_str().is_empty() {
|
||
usage();
|
||
}
|
||
if r.skip % 2 == 1 {
|
||
// A shot's parity picks its model (the 2023 fitted models, and the
|
||
// fits here, are cross-fitted by parity), so a selection keeps it.
|
||
eprintln!("--skip: an even number of shots");
|
||
std::process::exit(2);
|
||
}
|
||
r
|
||
}
|
||
|
||
/// One experiment's rows: each decoder's, then each published decoder's.
|
||
fn survey_one(set: Set, e: &data::Experiment, args: &Args) -> Vec<Value> {
|
||
let mut rows = Vec::new();
|
||
let all = data::shots(set, e);
|
||
let skip = args.skip;
|
||
let shots = match (skip, args.shots) {
|
||
(0, None) => all.clone(),
|
||
(0, Some(n)) => all.first(n),
|
||
(k, n) => all.select(|s| s >= k && n.is_none_or(|n| s < k + n)),
|
||
};
|
||
let (events, rate) = shots.event_rate();
|
||
let base = json!({
|
||
"set": set.name(), "experiment": e.name, "distance": e.distance, "basis": e.basis, "rounds": e.rounds, "patch": e.patch,
|
||
"detectors": shots.detectors(), "shots": shots.len(), "events": events, "event_rate": rate, "shots_blake3": hex(&shots.digest()),
|
||
});
|
||
let published = data::published(set, e, skip, shots.len());
|
||
let wrong = |pred: &[u64], s: usize| pred[s] != shots.flips(s);
|
||
if let Some(dir) = &args.predictions {
|
||
// The record's own flips and each published decoder's predictions on
|
||
// the same shots, beside this crate's, so every pair can be compared
|
||
// shot by shot.
|
||
std::fs::create_dir_all(dir).unwrap();
|
||
let stem = e.name.replace('/', "_");
|
||
let actual: Vec<u8> = (0..shots.len()).map(|s| shots.flips(s) as u8).collect();
|
||
std::fs::write(dir.join(format!("{stem}_actual.b8")), actual).unwrap();
|
||
for (name, pred) in &published {
|
||
std::fs::write(dir.join(format!("{stem}_published_{name}.b8")), pred.iter().map(|&f| f as u8).collect::<Vec<u8>>()).unwrap();
|
||
}
|
||
}
|
||
// Fitted priors are cross-fitted by shot parity, so a decoded shot is
|
||
// never among the shots its model was fitted to.
|
||
let models = data::models(set, e, &args.prior, if args.fit_all { &all } else { &shots });
|
||
for dname in &args.decoders {
|
||
let t0 = Instant::now();
|
||
let decs: Result<Vec<Dec>, String> = models.iter().map(|m| Dec::build(dname, m, e.distance)).collect();
|
||
let decs = match decs {
|
||
Ok(d) => d,
|
||
Err(why) => {
|
||
eprintln!("{} {dname}: not run: {why}", e.name);
|
||
continue;
|
||
}
|
||
};
|
||
let build_s = t0.elapsed().as_secs_f64();
|
||
FORCED_SHOTS.store(0, std::sync::atomic::Ordering::Relaxed);
|
||
LARGEST.store(0, std::sync::atomic::Ordering::Relaxed);
|
||
let cpu0 = receipt::cpu_ns();
|
||
let t1 = Instant::now();
|
||
// Shot `s` of the selection is shot `skip + s` of the record, whose
|
||
// parity picks its model (`skip` is even).
|
||
let pr = predict(&shots, args.threads, || decs.iter().map(Dec::work).collect::<Vec<_>>(), |w, s, fired| {
|
||
let k = (skip + s) % decs.len();
|
||
decs[k].decode(&mut w[k], fired)
|
||
})
|
||
.unwrap_or_else(|(s, err)| panic!("{} shot {s}: {err}", e.name));
|
||
let wall = t1.elapsed().as_secs_f64();
|
||
if matches!(decs[0], Dec::Tn(_)) {
|
||
// Where the decomposition time went, summed over every thread since the process began.
|
||
let (ph, rot) = wai_quantum::quantum_svd::phases();
|
||
eprintln!("svd phases so far (all threads): {ph:?}, {rot} rotations, {} Jacobi fallbacks", wai_quantum::quantum_memory_tn::jacobi_fallbacks());
|
||
}
|
||
// The process's CPU time over the decode, all threads: the decoder's
|
||
// cost a shot whatever else shares the host.
|
||
let cpu_per_shot = match (cpu0, receipt::cpu_ns()) {
|
||
(Some(a), Some(b)) => Some(b.saturating_sub(a) as f64 / shots.len() as f64),
|
||
_ => None,
|
||
};
|
||
let errors = pr.errors(&shots);
|
||
if let Some(dir) = &args.predictions {
|
||
std::fs::create_dir_all(dir).unwrap();
|
||
let bytes: Vec<u8> = pr.flips.iter().map(|&f| f as u8).collect();
|
||
std::fs::write(dir.join(format!("{}_{}_{dname}.b8", e.name.replace('/', "_"), args.prior)), bytes).unwrap();
|
||
}
|
||
let mut t = pr.nanos.clone();
|
||
t.sort_unstable();
|
||
let q = |f: f64| t[((t.len() as f64 - 1.0) * f).round() as usize];
|
||
let mean = t.iter().sum::<u64>() as f64 / t.len() as f64;
|
||
let mut row = base.clone();
|
||
let extra = json!({
|
||
"decoder": dname, "prior": args.prior, "errors": errors, "predictions_blake3": hex(&pr.digest()),
|
||
"ns_mean": mean, "ns_p50": q(0.5), "ns_p99": q(0.99), "ns_max": t[t.len() - 1],
|
||
"ns_per_round_mean": mean / f64::from(e.rounds), "build_s": build_s, "wall_s": wall, "threads": args.threads,
|
||
"cpu_ns_per_shot": cpu_per_shot,
|
||
"decoder_shape": decs[0].describe(),
|
||
});
|
||
for (k, v) in extra.as_object().unwrap() {
|
||
row[k] = v.clone();
|
||
}
|
||
// Against each published decoder on the same shots: the shots only
|
||
// this decoder got wrong, and the shots only the published one did.
|
||
let mut discordant = serde_json::Map::new();
|
||
for (name, pred) in &published {
|
||
let only_ours = (0..shots.len()).filter(|&s| wrong(&pr.flips, s) && !wrong(pred, s)).count();
|
||
let only_theirs = (0..shots.len()).filter(|&s| !wrong(&pr.flips, s) && wrong(pred, s)).count();
|
||
discordant.insert(format!("published:{name}|published"), json!([only_ours, only_theirs]));
|
||
}
|
||
row["discordant"] = Value::Object(discordant);
|
||
if matches!(decs[0], Dec::Tn(_) | Dec::Sp(_)) {
|
||
row["decoder_shape"]["forced_shots"] = json!(FORCED_SHOTS.load(std::sync::atomic::Ordering::Relaxed));
|
||
row["decoder_shape"]["largest"] = json!(LARGEST.load(std::sync::atomic::Ordering::Relaxed));
|
||
}
|
||
eprintln!("{} {} {}: {} errors of {} ({:.4}), {:.1} µs/shot mean, {:.2} µs/round, {:.1}s {}", e.name, dname, args.prior, errors, shots.len(), errors as f64 / shots.len() as f64, mean / 1e3, mean / 1e3 / f64::from(e.rounds), wall, row["decoder_shape"]);
|
||
rows.push(row);
|
||
}
|
||
for (name, pred) in &published {
|
||
let mut row = base.clone();
|
||
row["decoder"] = json!(format!("published:{name}"));
|
||
row["prior"] = json!("published");
|
||
row["errors"] = json!((0..shots.len()).filter(|&s| wrong(pred, s)).count());
|
||
rows.push(row);
|
||
}
|
||
rows
|
||
}
|
||
|
||
fn survey(a: &[String]) {
|
||
if a.len() < 2 {
|
||
usage();
|
||
}
|
||
let set = Set::parse(&a[0]).unwrap_or_else(|| usage());
|
||
let dir = Path::new(&a[1]);
|
||
let args = parse_args(&a[2..]);
|
||
let mut exps = data::list(set, dir);
|
||
if !args.only.is_empty() {
|
||
exps.retain(|e| args.only.iter().any(|o| e.name.contains(o.as_str())));
|
||
}
|
||
eprintln!("{} experiments", exps.len());
|
||
let mut out = std::fs::File::create(&args.out).unwrap();
|
||
use std::io::Write;
|
||
let (rows, wall, energy) = receipt::metered(|| {
|
||
let mut all = Vec::new();
|
||
for e in &exps {
|
||
let rows = survey_one(set, e, &args);
|
||
for row in &rows {
|
||
writeln!(out, "{row}").unwrap();
|
||
}
|
||
all.push(rows);
|
||
}
|
||
all
|
||
});
|
||
eprintln!("done in {wall:.1}s");
|
||
if let Some(path) = &args.receipt {
|
||
let experiments: Vec<Value> = rows.iter().map(|r| compact(r)).collect();
|
||
let flat: Vec<Value> = rows.into_iter().flatten().collect();
|
||
let (fits, _) = fit_rows(set, &flat);
|
||
let request = json!({
|
||
"experiment": set.citation(),
|
||
"source": set.source(),
|
||
"set": set.name(),
|
||
"prior": args.prior,
|
||
"decoders": args.decoders,
|
||
"shots_per_experiment": args.shots,
|
||
"first_shot": args.skip,
|
||
"experiments": exps.iter().map(|e| e.name.clone()).collect::<Vec<_>>(),
|
||
"fit": set.fit_rule(),
|
||
"method": {
|
||
"records": "detection_events.b8 and the actual observable flips, read as published; each experiment's shots hashed (shots_blake3: quantum_memory::Shots::digest)",
|
||
"decoders": "quantum_memory: mwpm = Matcher (exact minimum-weight perfect matching), corr = CorrMatcher, bm = BeliefMatcher (sum-product propagation, then matching), ml = MlDecoder (exact maximum likelihood); quantum_uf: uf; quantum_match: corr0; quantum_memory_tn: sp<chi> = SparseMl, tn<chi> = TnDecoder (e<K>: cutoff 10^-K; b: weighed by belief propagation); quantum_memory_stream: ufw, mwpmw = ParallelWindows",
|
||
"published_decoders": "each published prediction file scored against the same shots",
|
||
"shots": "shots first_shot to first_shot + shots_per_experiment of each experiment (all of them when not given)",
|
||
"discordant": "per experiment, for each of this crate's decoders against each published decoder: [shots only this crate's decoder got wrong, shots only the published one got wrong]",
|
||
},
|
||
"threads": args.threads,
|
||
"fit_priors_to_every_shot": args.fit_all,
|
||
"source_commit": std::env::var("QEC_SOURCE_COMMIT").ok(),
|
||
"host": host(),
|
||
});
|
||
let shapes: serde_json::Map<String, Value> = flat.iter().filter(|r| !r["decoder_shape"].is_null()).map(|r| (format!("{}|{}", r["decoder"].as_str().unwrap(), r["experiment"].as_str().unwrap()), r["decoder_shape"].clone())).collect();
|
||
let result = json!({
|
||
"experiments": experiments,
|
||
"fits": fits,
|
||
"decoder_shapes": shapes,
|
||
"wall_seconds": wall,
|
||
"energy_figure": energy,
|
||
"peak_rss_bytes": receipt::peak_rss_bytes(),
|
||
});
|
||
receipt::seal(request, result, path);
|
||
eprintln!("sealed {}", path.display());
|
||
}
|
||
}
|
||
|
||
/// One experiment's rows as the receipt holds them: the experiment once,
|
||
/// then per decoder (`name|prior`) its errors, and for this crate's decoders
|
||
/// the predictions' digest, the mean and 99th-percentile nanoseconds a shot,
|
||
/// and against each published decoder the shots only one of the two got
|
||
/// wrong (`[this crate's, the published one's]`).
|
||
fn compact(rows: &[Value]) -> Value {
|
||
let r0 = &rows[0];
|
||
let mut errors = serde_json::Map::new();
|
||
let mut digests = serde_json::Map::new();
|
||
let mut ns = serde_json::Map::new();
|
||
let mut discordant = serde_json::Map::new();
|
||
for r in rows {
|
||
let key = format!("{}|{}", r["decoder"].as_str().unwrap(), r["prior"].as_str().unwrap());
|
||
errors.insert(key.clone(), r["errors"].clone());
|
||
if !r["predictions_blake3"].is_null() {
|
||
digests.insert(key.clone(), r["predictions_blake3"].clone());
|
||
let cpu = r["cpu_ns_per_shot"].as_f64().map(|c| c.round() as u64);
|
||
// The wall time of the whole decode over every thread, a shot.
|
||
let wall = r["wall_s"].as_f64().map(|w| (w * 1e9 / r["shots"].as_f64().unwrap()).round() as u64);
|
||
ns.insert(key.clone(), json!([r["ns_mean"].as_f64().unwrap().round() as u64, r["ns_p99"], cpu, wall]));
|
||
if r["discordant"].as_object().is_some_and(|d| !d.is_empty()) {
|
||
discordant.insert(key, r["discordant"].clone());
|
||
}
|
||
}
|
||
}
|
||
json!({
|
||
"experiment": r0["experiment"], "distance": r0["distance"], "basis": r0["basis"], "rounds": r0["rounds"], "patch": r0["patch"],
|
||
"detectors": r0["detectors"], "shots": r0["shots"], "event_rate": r0["event_rate"], "shots_blake3": r0["shots_blake3"],
|
||
"errors": errors, "predictions_blake3": digests, "ns_mean_p99": ns, "discordant": discordant,
|
||
})
|
||
}
|
||
|
||
/// A receipt's experiments as flat rows (`decoder`, `prior`, `distance`,
|
||
/// `patch`, `basis`, `rounds`, `shots`, `errors`), for [`fit_rows`].
|
||
fn flatten(doc: &Value) -> Vec<Value> {
|
||
let mut out = Vec::new();
|
||
for e in doc["result"]["experiments"].as_array().unwrap() {
|
||
for (key, n) in e["errors"].as_object().unwrap() {
|
||
let (dec, prior) = key.split_once('|').unwrap();
|
||
out.push(json!({ "decoder": dec, "prior": prior, "distance": e["distance"], "patch": e["patch"], "basis": e["basis"], "rounds": e["rounds"], "shots": e["shots"], "errors": n }));
|
||
}
|
||
}
|
||
out
|
||
}
|
||
|
||
/// What the receipt says about the machine.
|
||
fn host() -> Value {
|
||
json!({ "os": std::env::consts::OS, "arch": std::env::consts::ARCH, "cores": std::thread::available_parallelism().map(|n| n.get()).unwrap_or(0) })
|
||
}
|
||
|
||
fn hex(b: &[u8]) -> String {
|
||
b.iter().map(|x| format!("{x:02x}")).collect()
|
||
}
|
||
|
||
/// Every (decoder, prior) in the rows: per (distance, patch, basis) a decay
|
||
/// over the rounds the set's publication fits, the mean per distance, and Λ.
|
||
/// The fits as JSON, and as text.
|
||
fn fit_rows(set: Set, rows: &[Value]) -> (Value, String) {
|
||
use std::fmt::Write;
|
||
let mut text = String::new();
|
||
let mut out = serde_json::Map::new();
|
||
let mut keys: Vec<(String, String)> = rows.iter().map(|r| (r["decoder"].as_str().unwrap().to_string(), r["prior"].as_str().unwrap().to_string())).collect();
|
||
keys.sort();
|
||
keys.dedup();
|
||
for (dec, prior) in keys {
|
||
let mut groups: std::collections::BTreeMap<(u64, String, String), Vec<RoundPoint>> = std::collections::BTreeMap::new();
|
||
for r in rows.iter().filter(|r| r["decoder"] == dec.as_str() && r["prior"] == prior.as_str() && set.fitted(r["rounds"].as_u64().unwrap() as u32)) {
|
||
groups
|
||
.entry((r["distance"].as_u64().unwrap(), r["patch"].as_str().unwrap().to_string(), r["basis"].as_str().unwrap().to_string()))
|
||
.or_default()
|
||
.push(RoundPoint { rounds: r["rounds"].as_u64().unwrap() as u32, shots: r["shots"].as_u64().unwrap(), errors: r["errors"].as_u64().unwrap() });
|
||
}
|
||
let mut per_d: std::collections::BTreeMap<u64, Vec<DecayFit>> = std::collections::BTreeMap::new();
|
||
let mut members = Vec::new();
|
||
for ((d, patch, basis), pts) in &groups {
|
||
if let Some(f) = fit_decay(pts, 0) {
|
||
let _ = writeln!(text, " {dec:>40} {prior:>9} d{d} {patch:>8} {basis}: eps {:.5}% ± {:.5}% (stat {:.5}%), chi2/dof {:.2}, {} points", 100.0 * f.eps, 100.0 * f.sigma, 100.0 * f.sigma_stat, f.chi2_per_dof, f.points);
|
||
members.push(json!({ "distance": d, "patch": patch, "basis": basis, "eps": f.eps, "sigma": f.sigma, "sigma_stat": f.sigma_stat, "prefactor": f.prefactor, "chi2_per_dof": f.chi2_per_dof, "points": f.points }));
|
||
per_d.entry(*d).or_default().push(f);
|
||
}
|
||
}
|
||
let mut lam_in = Vec::new();
|
||
let mut dist = serde_json::Map::new();
|
||
for (d, fits) in &per_d {
|
||
let m = mean_rate(fits).unwrap();
|
||
let _ = writeln!(text, "{dec:>42} {prior:>9} d{d}: eps {:.4}% (σ mean {:.4}%, indep {:.4}%, rms {:.4}%), {} fits", 100.0 * m.eps, 100.0 * m.sigma_mean, 100.0 * m.sigma_independent, 100.0 * m.sigma_rms, m.members);
|
||
dist.insert(format!("d{d}"), json!({ "eps": m.eps, "sigma_mean": m.sigma_mean, "sigma_independent": m.sigma_independent, "sigma_rms": m.sigma_rms, "fits": m.members }));
|
||
lam_in.push((*d as u32, m.eps, m.sigma_independent));
|
||
}
|
||
let lambda = fit_lambda(&lam_in);
|
||
if let Some((l, s)) = lambda {
|
||
let _ = writeln!(text, "{dec:>42} {prior:>9} Lambda {l:.4} ± {s:.4}");
|
||
}
|
||
out.insert(format!("{dec}|{prior}"), json!({ "decoder": dec, "prior": prior, "members": members, "distances": dist, "lambda": lambda.map(|(l, s)| json!({ "value": l, "sigma": s })) }));
|
||
}
|
||
(Value::Object(out), text)
|
||
}
|
||
|
||
/// Fit the rows of the files given.
|
||
fn fit(a: &[String]) {
|
||
let set = Set::parse(&a[0]).unwrap_or_else(|| usage());
|
||
let mut rows: Vec<Value> = Vec::new();
|
||
for f in &a[1..] {
|
||
let t = std::fs::read_to_string(f).unwrap();
|
||
if f.ends_with(".json") {
|
||
let doc: Value = serde_json::from_str(&t).unwrap();
|
||
rows.extend(flatten(&doc));
|
||
} else {
|
||
rows.extend(t.lines().map(|l| serde_json::from_str::<Value>(l).unwrap()));
|
||
}
|
||
}
|
||
print!("{}", fit_rows(set, &rows).1);
|
||
}
|