wai-quantum 0.3.32: union-find decoding in near-linear time #155
14 changed files with 934 additions and 14 deletions
2
wai/wai-quantum-serve/Cargo.lock
generated
2
wai/wai-quantum-serve/Cargo.lock
generated
|
|
@ -589,7 +589,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "wai-quantum"
|
||||
version = "0.3.31"
|
||||
version = "0.3.32"
|
||||
dependencies = [
|
||||
"blake3",
|
||||
"ed25519-dalek",
|
||||
|
|
|
|||
2
wai/wai-quantum/Cargo.lock
generated
2
wai/wai-quantum/Cargo.lock
generated
|
|
@ -415,7 +415,7 @@ checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
|
|||
|
||||
[[package]]
|
||||
name = "wai-quantum"
|
||||
version = "0.3.31"
|
||||
version = "0.3.32"
|
||||
dependencies = [
|
||||
"blake3",
|
||||
"ed25519-dalek",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "wai-quantum"
|
||||
version = "0.3.31"
|
||||
version = "0.3.32"
|
||||
edition = "2024"
|
||||
rust-version = "1.97"
|
||||
license = "Apache-2.0"
|
||||
|
|
@ -74,6 +74,8 @@ quantum_ldpc = ["quantum_frame"]
|
|||
# Quantum-inspired optimisation: simulated bifurcation (ballistic, discrete,
|
||||
# heated), simulated annealing and an exact referee, on Ising and Max-Cut.
|
||||
quantum_qio = []
|
||||
# Union-find decoding in near-linear time over the matching graph.
|
||||
quantum_uf = ["quantum_frame"]
|
||||
# Mixed states and the observables that distinguish quantum theory from a classical
|
||||
# account: density matrices, the partial trace, exact Pauli expectations, CHSH.
|
||||
quantum_info = ["quantum"]
|
||||
|
|
@ -113,7 +115,7 @@ quantum_phasor_meter = ["_signing"]
|
|||
|
||||
# Everything.
|
||||
full = [
|
||||
"quantum", "quantum_toolchain", "quantum_mps", "quantum_stabilizer", "quantum_pauli", "quantum_spd", "quantum_spd_receipt", "quantum_bptn", "quantum_resource", "quantum_resource_receipt", "quantum_frame", "quantum_tn", "quantum_gbs", "quantum_sv", "quantum_arch", "quantum_ldpc", "quantum_qio", "quantum_qir", "quantum_info", "quantum_qudit", "quantum_comm", "quantum_source", "quantum_channel",
|
||||
"quantum", "quantum_toolchain", "quantum_mps", "quantum_stabilizer", "quantum_pauli", "quantum_spd", "quantum_spd_receipt", "quantum_bptn", "quantum_resource", "quantum_resource_receipt", "quantum_frame", "quantum_tn", "quantum_gbs", "quantum_sv", "quantum_arch", "quantum_ldpc", "quantum_qio", "quantum_uf", "quantum_qir", "quantum_info", "quantum_qudit", "quantum_comm", "quantum_source", "quantum_channel",
|
||||
"quantum_receipt", "quantum_ops", "quantum_cal", "quantum_mitigate", "quantum_qec",
|
||||
"quantum_control", "quantum_noise", "quantum_compile", "quantum_atom", "quantum_qbom",
|
||||
"quantum_vml", "quantum_phasor", "quantum_qfhrr", "quantum_kernel", "quantum_qdata",
|
||||
|
|
@ -124,6 +126,10 @@ full = [
|
|||
name = "qio_gset"
|
||||
required-features = ["quantum_qio"]
|
||||
|
||||
[[example]]
|
||||
name = "uf_bench"
|
||||
required-features = ["quantum_uf"]
|
||||
|
||||
[[example]]
|
||||
name = "ldpc_memory"
|
||||
required-features = ["quantum_ldpc"]
|
||||
|
|
|
|||
|
|
@ -65,6 +65,7 @@ never creates information.
|
|||
| `quantum_resource` | **fault-tolerant resource estimation**: from a program's logical counts (qubits, T, Toffoli and rotation gates, measurements) to code distance, T-state factories found by exhaustive search, physical qubits and runtime, on six reference qubits and three codes, with the qubits-versus-time frontier. Reproduces the published worked examples to their printed precision |
|
||||
| `quantum_resource_receipt` | a signed, energy-accounted receipt over an estimate, re-checkable bit for bit on any machine |
|
||||
| `quantum_qio` | **quantum-inspired optimisation**: Ising and Max-Cut problems (G-set reader, spin-glass generator), simulated bifurcation in its ballistic, discrete and heated forms (batched eight trials per pass, bit-identical to one at a time), simulated annealing, single-flip descent, an exact Gray-code referee up to 34 spins, and step-to-solution. Reaches the best-known cut on seven G-set instances |
|
||||
| `quantum_uf` | **union-find decoding in near-linear time** over the same matching graph: event-driven cluster growth on integer weights (a radix heap orders the fill events), merges, and peeling. Measured against exact matching on the same shots: within 0.92–1.18× the failures, up to 77× faster at `d = 11` |
|
||||
| `quantum_ldpc` | **qLDPC memory at circuit level**: bivariate-bicycle codes from their two polynomials (`k` by rank, logical bases), the depth-8 syndrome cycle with circuit-level noise, its detector error model, and BP-OSD decoding (min-sum with adaptive scaling, ordered statistics with a combination sweep). The error model has the same columns as the source's own decoding matrices, and on identical syndromes the decoder makes the source's decisions shot for shot |
|
||||
| `quantum_arch` | **architecture-level estimation**: yoked surface-code storage (row and grid parity checks over patches, hot and cold), magic-state cultivation read off the published simulation curves, CCZ factories, and a clock set by whichever is slowest of lattice surgery, the magic-state supply and the control loop. Assembles whole-machine estimates; its storage search matches the source's footprint tool layout for layout |
|
||||
| `quantum_receipt` | signed, energy-accounted receipt over a reconstruction |
|
||||
|
|
@ -176,6 +177,20 @@ in one pass. On the G-set Max-Cut benchmark the best-known cut is reached on
|
|||
G1, G2, G3, G11, G14, G22 and G43. On G55 the best found is 10,292 against
|
||||
10,299. The module documents each setting and success rate, and the bound
|
||||
that sparse graphs put on the time step.
|
||||
## Fast decoding
|
||||
|
||||
```sh
|
||||
wai-quantum qec -d 9 --p 0.005 --shots 20000 --decoder uf # union-find
|
||||
wai-quantum qec -d 9 --p 0.005 --shots 20000 --decoder exact # exact matching, same shots
|
||||
```
|
||||
|
||||
A fault-tolerant machine must decode each round of syndromes before the next
|
||||
arrives. `quantum_uf` is the union-find decoder over the matching graph that
|
||||
exact matching uses. Clusters grow from the fired detectors and merge as edges
|
||||
fill, then each cluster's spanning tree is peeled to a correction. Its time per
|
||||
shot grows linearly with the number of fired detectors. On the same shots it
|
||||
fails between 0.92 and 1.18 times as often as exact matching. The module
|
||||
tabulates both decoders by distance and noise, with the time each takes.
|
||||
|
||||
## qLDPC memory
|
||||
|
||||
|
|
|
|||
46
wai/wai-quantum/examples/uf_bench.rs
Normal file
46
wai/wai-quantum/examples/uf_bench.rs
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
//! Union-find against exact matching on rotated surface-code memory
|
||||
//! experiments: logical failures on the same shots, and time per shot.
|
||||
//!
|
||||
//! ```text
|
||||
//! cargo run --release --example uf_bench --features quantum_uf -- P SHOTS D [D …] [--uf-only]
|
||||
//! ```
|
||||
use wai_quantum::quantum_frame::{error_model, parse, sample, surface_code_memory};
|
||||
use wai_quantum::quantum_match::MatchingGraph;
|
||||
use wai_quantum::quantum_uf::UnionFind;
|
||||
|
||||
fn main() {
|
||||
let a: Vec<String> = std::env::args().collect();
|
||||
let p: f64 = a[1].parse().unwrap();
|
||||
let shots: usize = a[2].parse().unwrap();
|
||||
let uf_only = a.iter().any(|x| x == "--uf-only");
|
||||
for d in a[3..].iter().filter(|x| !x.starts_with("--")).map(|x| x.parse::<u32>().unwrap()) {
|
||||
let c = parse(&surface_code_memory(d, d, p)).unwrap();
|
||||
let g = MatchingGraph::from_model(&error_model(&c).unwrap()).unwrap();
|
||||
let det = sample(&c, shots, 7);
|
||||
let uf = UnionFind::new(&g);
|
||||
let mut ws = uf.workspace();
|
||||
let shots_fired: Vec<Vec<u32>> = (0..shots).map(|s| det.fired(s)).collect();
|
||||
let t0 = std::time::Instant::now();
|
||||
let mut uf_fail = 0;
|
||||
for (s, f) in shots_fired.iter().enumerate() {
|
||||
uf_fail += u64::from(uf.decode(f, &mut ws).unwrap() != det.flips(s));
|
||||
}
|
||||
let uf_us = t0.elapsed().as_secs_f64() * 1e6 / shots as f64;
|
||||
let mean_fired = shots_fired.iter().map(Vec::len).sum::<usize>() as f64 / shots as f64;
|
||||
if uf_only {
|
||||
println!("d={d:>2} p={p} shots={shots} fired/shot {mean_fired:.1} uf {uf_fail} ({uf_us:.2} us/shot)");
|
||||
continue;
|
||||
}
|
||||
let t1 = std::time::Instant::now();
|
||||
let mut mw_fail = 0;
|
||||
for (s, f) in shots_fired.iter().enumerate() {
|
||||
mw_fail += u64::from(g.decode(f).unwrap() != det.flips(s));
|
||||
}
|
||||
let mw_us = t1.elapsed().as_secs_f64() * 1e6 / shots as f64;
|
||||
println!(
|
||||
"d={d:>2} p={p} shots={shots} fired/shot {mean_fired:.1} uf {uf_fail} ({uf_us:.2} us/shot) exact {mw_fail} ({mw_us:.1} us/shot) ratio {:.2} speedup {:.0}x",
|
||||
uf_fail as f64 / mw_fail.max(1) as f64,
|
||||
mw_us / uf_us
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -113,6 +113,9 @@ pub mod quantum_ldpc;
|
|||
#[cfg(feature = "quantum_qio")]
|
||||
pub mod quantum_qio;
|
||||
|
||||
#[cfg(feature = "quantum_uf")]
|
||||
pub mod quantum_uf;
|
||||
|
||||
#[cfg(feature = "quantum_frame")]
|
||||
pub mod quantum_match;
|
||||
|
||||
|
|
|
|||
|
|
@ -50,8 +50,9 @@
|
|||
//!
|
||||
//! The decoder is exact first and fast second. At small distances it decodes
|
||||
//! in microseconds. At `d = 9` a fired detector's boundary cap still spans
|
||||
//! much of the lattice, so a shot costs about half a millisecond. A
|
||||
//! region-growing matcher is the step after this one. Sampling is not the
|
||||
//! much of the lattice, so a shot costs about half a millisecond. For speed
|
||||
//! over the same graph, `quantum_uf` decodes in time linear in the fired
|
||||
//! detectors, at a measured cost in accuracy. Sampling is not the
|
||||
//! bottleneck: the frame simulator draws over a million `d = 9` shots per second.
|
||||
|
||||
use crate::quantum_frame::{Detections, ErrorModel};
|
||||
|
|
@ -239,6 +240,16 @@ impl MatchingGraph {
|
|||
Ok(out)
|
||||
}
|
||||
|
||||
/// Detectors in the graph (the boundary is node `detectors()`).
|
||||
pub fn detectors(&self) -> usize {
|
||||
self.detectors
|
||||
}
|
||||
|
||||
/// Neighbours of node `u`: `(node, integer weight, observable mask)`.
|
||||
pub(crate) fn neighbours(&self, u: usize) -> &[(u32, i64, u64)] {
|
||||
&self.adj[u]
|
||||
}
|
||||
|
||||
/// Decode every shot; returns how many predictions missed the actual
|
||||
/// observable flips.
|
||||
pub fn failures(&self, shots: &Detections) -> Result<u64, MatchError> {
|
||||
|
|
|
|||
814
wai/wai-quantum/src/quantum_uf.rs
Normal file
814
wai/wai-quantum/src/quantum_uf.rs
Normal file
|
|
@ -0,0 +1,814 @@
|
|||
//! Union-find DECODING in near-linear time — `wai.quantum.uf`.
|
||||
//!
|
||||
//! A fault-tolerant machine must decode every round of syndromes before the
|
||||
//! next arrives, so a decoder's latency is part of the hardware budget.
|
||||
//! Exact minimum-weight matching (`quantum_match`) is the accuracy reference,
|
||||
//! but it pays for exactness with shortest-path searches that span much of the
|
||||
//! lattice. The union-find decoder trades a little accuracy for time that
|
||||
//! grows almost linearly with the number of fired detectors.
|
||||
//!
|
||||
//! Over the same matching graph, a decode runs in three steps:
|
||||
//!
|
||||
//! 1. **Grow.** Every fired detector starts a cluster. Each odd cluster (an
|
||||
//! odd number of fired detectors, not touching the boundary) grows all its
|
||||
//! frontier edges at once. An edge between two growing clusters fills at
|
||||
//! twice the speed. Growth runs on the graph's integer weights:
|
||||
//! - each round advances by the time the next edge needs to fill;
|
||||
//! - a double-speed edge rounds that time up, at a cost of at most one
|
||||
//! unit in 16,384 of a log-likelihood.
|
||||
//! 2. **Merge.** A filled edge joins the two clusters it connects. A cluster
|
||||
//! that reaches the boundary stops growing. Growth stops when every
|
||||
//! cluster is even or holds the boundary.
|
||||
//! 3. **Peel.** Each cluster's filled edges are walked as a spanning tree,
|
||||
//! rooted at the boundary when the cluster holds it. Leaves are peeled
|
||||
//! inward: a node left with odd parity takes the edge to its parent into
|
||||
//! the correction. The prediction is the observable parity of that
|
||||
//! correction.
|
||||
//!
|
||||
//! Every step is integer and ordered, so a decode is the same on every
|
||||
//! machine. A [`Workspace`] is reused across shots and reset in time
|
||||
//! proportional to what the last shot touched, not to the graph's size.
|
||||
//!
|
||||
//! Growth is event-driven. Each frontier edge has a scheduled fill time, and
|
||||
//! a merge reschedules only the frontier of a side whose growth changed. The
|
||||
//! events sit in a radix heap: fill times never run backwards, so each event
|
||||
//! costs amortised constant work. Time per shot then grows linearly with the
|
||||
//! number of fired detectors, at about 1.1–1.5 µs per fired detector from
|
||||
//! `d = 9` to `d = 21`.
|
||||
//!
|
||||
//! # Checked
|
||||
//!
|
||||
//! - **Single faults.** Every single edge of the graph is corrected exactly.
|
||||
//! - **Two growth schedules.** The event-driven schedule agrees with the
|
||||
//! round-by-round one: on 2,000 shots at each of three distances, at most
|
||||
//! four decisions differ, from ties at equal fill times.
|
||||
//! - **Logical rates.** Measured against exact matching on the *same* shots
|
||||
//! (rotated surface-code memory, `d` rounds, 20,000 shots; failures, then
|
||||
//! the time per shot):
|
||||
//!
|
||||
//! | `p` | d | union-find | exact matching |
|
||||
//! |-------|---|-----------------|-----------------|
|
||||
//! | 0.003 | 5 | 67 (4.4 µs) | 60 (23 µs) |
|
||||
//! | 0.003 | 9 | 10 (53 µs) | 10 (1.1 ms) |
|
||||
//! | 0.005 | 5 | 280 (7.3 µs) | 265 (46 µs) |
|
||||
//! | 0.005 | 7 | 203 (27 µs) | 203 (416 µs) |
|
||||
//! | 0.005 | 9 | 167 (56 µs) | 142 (2.0 ms) |
|
||||
//! | 0.007 | 5 | 727 (12 µs) | 683 (79 µs) |
|
||||
//! | 0.007 | 9 | 760 (97 µs) | 665 (3.2 ms) |
|
||||
//!
|
||||
//! Union-find fails between 0.92 and 1.18 times as often as exact matching.
|
||||
//! At `d = 11, p = 0.005` it is 77 times faster. At `p = 0.007` its failures
|
||||
//! stop falling with distance while matching's do not yet: its threshold
|
||||
//! sits a little lower.
|
||||
//!
|
||||
//! # Honest boundaries
|
||||
//!
|
||||
//! - **Approximate.** Union-find is not minimum weight. It fails on some
|
||||
//! shots that exact matching decodes, and its threshold sits below
|
||||
//! matching's.
|
||||
//! - **Graphlike models only**, as for matching. Hyperedges are refused when
|
||||
//! the graph is built.
|
||||
|
||||
use crate::quantum_frame::Detections;
|
||||
use crate::quantum_match::{MatchError, MatchingGraph};
|
||||
|
||||
/// A monotone priority queue: keys never fall below the last key popped.
|
||||
/// Bucket `i` holds keys whose highest bit differing from that key is
|
||||
/// `i − 1`, so each entry moves down at most 64 times. Equal keys pop in a
|
||||
/// fixed order (last pushed first), so the order is deterministic.
|
||||
#[derive(Clone, Debug)]
|
||||
struct RadixHeap {
|
||||
last: u64,
|
||||
buckets: Vec<Vec<(u64, u32, u32)>>,
|
||||
}
|
||||
|
||||
impl Default for RadixHeap {
|
||||
fn default() -> RadixHeap {
|
||||
RadixHeap { last: 0, buckets: vec![Vec::new(); 65] }
|
||||
}
|
||||
}
|
||||
|
||||
impl RadixHeap {
|
||||
fn bucket(&self, key: u64) -> usize {
|
||||
(64 - (key ^ self.last).leading_zeros()) as usize
|
||||
}
|
||||
fn push(&mut self, key: u64, e: u32, version: u32) {
|
||||
debug_assert!(key >= self.last);
|
||||
let b = self.bucket(key);
|
||||
self.buckets[b].push((key, e, version));
|
||||
}
|
||||
fn pop(&mut self) -> Option<(u64, u32, u32)> {
|
||||
if self.buckets[0].is_empty() {
|
||||
let i = (1..65).find(|&i| !self.buckets[i].is_empty())?;
|
||||
let moved = core::mem::take(&mut self.buckets[i]);
|
||||
self.last = moved.iter().map(|m| m.0).min().expect("non-empty");
|
||||
for m in &moved {
|
||||
let b = self.bucket(m.0);
|
||||
self.buckets[b].push(*m);
|
||||
}
|
||||
// Keep the emptied bucket's allocation.
|
||||
let mut moved = moved;
|
||||
moved.clear();
|
||||
if self.buckets[i].is_empty() {
|
||||
self.buckets[i] = moved;
|
||||
}
|
||||
}
|
||||
self.buckets[0].pop()
|
||||
}
|
||||
fn clear(&mut self) {
|
||||
for b in &mut self.buckets {
|
||||
b.clear();
|
||||
}
|
||||
self.last = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/// The union-find decoder over a matching graph.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct UnionFind {
|
||||
/// Nodes: the detectors, then the boundary.
|
||||
nodes: usize,
|
||||
/// Adjacency in compressed rows: neighbour and edge id per entry.
|
||||
start: Vec<usize>,
|
||||
nbr: Vec<u32>,
|
||||
edge: Vec<u32>,
|
||||
/// Per edge: its ends, its doubled weight (growth is counted in half
|
||||
/// units), and its observables.
|
||||
ends: Vec<(u32, u32)>,
|
||||
cap: Vec<i64>,
|
||||
obs: Vec<u64>,
|
||||
}
|
||||
|
||||
/// Per-shot state, reused across decodes and reset in time proportional to
|
||||
/// what the last shot touched.
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct Workspace {
|
||||
parent: Vec<u32>,
|
||||
/// Root data: odd parity, holds the boundary, members, frontier edges.
|
||||
odd: Vec<bool>,
|
||||
boundary: Vec<bool>,
|
||||
members: Vec<Vec<u32>>,
|
||||
frontier: Vec<Vec<u32>>,
|
||||
in_use: Vec<bool>,
|
||||
touched: Vec<u32>,
|
||||
// Edge state: growth at `tau`, speed since then, version, filled.
|
||||
growth: Vec<i64>,
|
||||
tau: Vec<i64>,
|
||||
speed: Vec<i64>,
|
||||
version: Vec<u32>,
|
||||
full: Vec<bool>,
|
||||
edge_in_use: Vec<bool>,
|
||||
edges_touched: Vec<u32>,
|
||||
events: RadixHeap,
|
||||
defect: Vec<bool>,
|
||||
// Peeling scratch.
|
||||
order: Vec<u32>,
|
||||
via: Vec<u32>,
|
||||
seen: Vec<bool>,
|
||||
}
|
||||
|
||||
impl UnionFind {
|
||||
/// Build the decoder from a matching graph.
|
||||
pub fn new(g: &MatchingGraph) -> UnionFind {
|
||||
let nodes = g.detectors() + 1;
|
||||
let mut ids: std::collections::BTreeMap<(u32, u32), u32> = std::collections::BTreeMap::new();
|
||||
let (mut ends, mut cap, mut obs) = (Vec::new(), Vec::new(), Vec::new());
|
||||
let mut start = vec![0];
|
||||
let (mut nbr, mut edge) = (Vec::new(), Vec::new());
|
||||
for u in 0..nodes {
|
||||
for &(v, w, o) in g.neighbours(u) {
|
||||
let key = ((u as u32).min(v), (u as u32).max(v));
|
||||
let id = *ids.entry(key).or_insert_with(|| {
|
||||
ends.push(key);
|
||||
cap.push(2 * w.max(0));
|
||||
obs.push(o);
|
||||
(ends.len() - 1) as u32
|
||||
});
|
||||
nbr.push(v);
|
||||
edge.push(id);
|
||||
}
|
||||
start.push(nbr.len());
|
||||
}
|
||||
UnionFind { nodes, start, nbr, edge, ends, cap, obs }
|
||||
}
|
||||
|
||||
/// A workspace sized for this decoder.
|
||||
pub fn workspace(&self) -> Workspace {
|
||||
let (n, e) = (self.nodes, self.ends.len());
|
||||
Workspace {
|
||||
parent: (0..n as u32).collect(),
|
||||
odd: vec![false; n],
|
||||
boundary: vec![false; n],
|
||||
members: vec![Vec::new(); n],
|
||||
frontier: vec![Vec::new(); n],
|
||||
in_use: vec![false; n],
|
||||
touched: Vec::new(),
|
||||
growth: vec![0; e],
|
||||
tau: vec![0; e],
|
||||
speed: vec![0; e],
|
||||
version: vec![0; e],
|
||||
full: vec![false; e],
|
||||
edge_in_use: vec![false; e],
|
||||
edges_touched: Vec::new(),
|
||||
events: RadixHeap::default(),
|
||||
defect: vec![false; n],
|
||||
order: Vec::new(),
|
||||
via: vec![u32::MAX; n],
|
||||
seen: vec![false; n],
|
||||
}
|
||||
}
|
||||
|
||||
fn find(ws: &mut Workspace, mut u: u32) -> u32 {
|
||||
while ws.parent[u as usize] != u {
|
||||
let p = ws.parent[u as usize];
|
||||
ws.parent[u as usize] = ws.parent[p as usize];
|
||||
u = ws.parent[u as usize];
|
||||
}
|
||||
u
|
||||
}
|
||||
|
||||
fn growing(ws: &Workspace, root: u32) -> bool {
|
||||
ws.odd[root as usize] && !ws.boundary[root as usize]
|
||||
}
|
||||
|
||||
fn touch(&self, ws: &mut Workspace, u: u32) {
|
||||
let i = u as usize;
|
||||
if ws.in_use[i] {
|
||||
return;
|
||||
}
|
||||
ws.in_use[i] = true;
|
||||
ws.touched.push(u);
|
||||
ws.parent[i] = u;
|
||||
ws.odd[i] = false;
|
||||
ws.boundary[i] = i == self.nodes - 1;
|
||||
ws.members[i].clear();
|
||||
ws.members[i].push(u);
|
||||
ws.defect[i] = false;
|
||||
ws.frontier[i].clear();
|
||||
// The boundary never grows: its edges are reached from the other side.
|
||||
if !ws.boundary[i] {
|
||||
ws.frontier[i].extend_from_slice(&self.edge[self.start[i]..self.start[i + 1]]);
|
||||
}
|
||||
}
|
||||
|
||||
/// Bring edge `e` up to time `now`, give it the speed its ends now set,
|
||||
/// and schedule when it fills. An unchanged speed keeps its scheduled
|
||||
/// event. Returns whether the edge still belongs on a frontier (it is
|
||||
/// neither filled nor inside one cluster).
|
||||
fn reschedule(&self, ws: &mut Workspace, e: u32, now: i64) -> bool {
|
||||
let ei = e as usize;
|
||||
if !ws.edge_in_use[ei] {
|
||||
ws.edge_in_use[ei] = true;
|
||||
ws.edges_touched.push(e);
|
||||
ws.growth[ei] = 0;
|
||||
ws.tau[ei] = now;
|
||||
ws.speed[ei] = 0;
|
||||
ws.version[ei] = 0;
|
||||
ws.full[ei] = false;
|
||||
}
|
||||
if ws.full[ei] {
|
||||
return false;
|
||||
}
|
||||
let (a, b) = self.ends[ei];
|
||||
let ra = if ws.in_use[a as usize] { Some(Self::find(ws, a)) } else { None };
|
||||
let rb = if ws.in_use[b as usize] { Some(Self::find(ws, b)) } else { None };
|
||||
let internal = ra.is_some() && ra == rb;
|
||||
let speed = if internal { 0 } else { i64::from(ra.is_some_and(|r| Self::growing(ws, r))) + i64::from(rb.is_some_and(|r| Self::growing(ws, r))) };
|
||||
if speed == ws.speed[ei] && ws.version[ei] != 0 {
|
||||
return !internal;
|
||||
}
|
||||
ws.growth[ei] += ws.speed[ei] * (now - ws.tau[ei]);
|
||||
ws.tau[ei] = now;
|
||||
ws.version[ei] = ws.version[ei].wrapping_add(1).max(1);
|
||||
ws.speed[ei] = speed;
|
||||
if speed > 0 {
|
||||
let rem = self.cap[ei] - ws.growth[ei];
|
||||
let at = if rem <= 0 { now } else { now + (rem + speed - 1) / speed };
|
||||
ws.events.push(at as u64, e, ws.version[ei]);
|
||||
}
|
||||
!internal
|
||||
}
|
||||
|
||||
/// Reschedule every edge on `root`'s frontier, dropping those that are
|
||||
/// filled or inside the cluster.
|
||||
fn reschedule_frontier(&self, ws: &mut Workspace, root: u32, now: i64) {
|
||||
let mut list = core::mem::take(&mut ws.frontier[root as usize]);
|
||||
list.retain(|&e| self.reschedule(ws, e, now));
|
||||
ws.frontier[root as usize] = list;
|
||||
}
|
||||
|
||||
fn reset(ws: &mut Workspace) {
|
||||
for &u in &ws.touched {
|
||||
let i = u as usize;
|
||||
ws.in_use[i] = false;
|
||||
ws.parent[i] = u;
|
||||
ws.members[i].clear();
|
||||
ws.frontier[i].clear();
|
||||
ws.defect[i] = false;
|
||||
ws.seen[i] = false;
|
||||
ws.via[i] = u32::MAX;
|
||||
}
|
||||
ws.touched.clear();
|
||||
for &e in &ws.edges_touched {
|
||||
ws.edge_in_use[e as usize] = false;
|
||||
}
|
||||
ws.edges_touched.clear();
|
||||
ws.events.clear();
|
||||
}
|
||||
|
||||
/// Predict which observables flipped, from the detectors that fired.
|
||||
pub fn decode(&self, fired: &[u32], ws: &mut Workspace) -> Result<u64, MatchError> {
|
||||
Self::reset(ws);
|
||||
for &d in fired {
|
||||
self.touch(ws, d);
|
||||
ws.odd[d as usize] ^= true;
|
||||
ws.defect[d as usize] ^= true;
|
||||
}
|
||||
for &d in fired {
|
||||
if Self::growing(ws, d) {
|
||||
self.reschedule_frontier(ws, d, 0);
|
||||
}
|
||||
}
|
||||
while let Some((at, e, ver)) = ws.events.pop() {
|
||||
let at = at as i64;
|
||||
let ei = e as usize;
|
||||
if ws.full[ei] || ws.version[ei] != ver {
|
||||
continue;
|
||||
}
|
||||
ws.full[ei] = true;
|
||||
ws.growth[ei] = self.cap[ei];
|
||||
ws.speed[ei] = 0;
|
||||
let (a, b) = self.ends[ei];
|
||||
self.touch(ws, a);
|
||||
self.touch(ws, b);
|
||||
let (ra, rb) = (Self::find(ws, a), Self::find(ws, b));
|
||||
if ra == rb {
|
||||
continue;
|
||||
}
|
||||
let (ga, gb) = (Self::growing(ws, ra), Self::growing(ws, rb));
|
||||
// Merge, smaller into larger.
|
||||
let (big, small) = if ws.members[ra as usize].len() >= ws.members[rb as usize].len() { (ra, rb) } else { (rb, ra) };
|
||||
ws.parent[small as usize] = big;
|
||||
let moved = core::mem::take(&mut ws.members[small as usize]);
|
||||
ws.members[big as usize].extend(moved);
|
||||
ws.odd[big as usize] ^= ws.odd[small as usize];
|
||||
ws.boundary[big as usize] |= ws.boundary[small as usize];
|
||||
let g = Self::growing(ws, big);
|
||||
// Only a side whose growth changed moves its edges' speeds.
|
||||
let mut fs = core::mem::take(&mut ws.frontier[small as usize]);
|
||||
let (g_big, g_small) = if big == ra { (ga, gb) } else { (gb, ga) };
|
||||
if g_big != g {
|
||||
self.reschedule_frontier(ws, big, at);
|
||||
}
|
||||
if g_small != g {
|
||||
fs.retain(|&f| self.reschedule(ws, f, at));
|
||||
}
|
||||
ws.frontier[big as usize].extend(fs);
|
||||
}
|
||||
for k in 0..ws.touched.len() {
|
||||
let u = ws.touched[k];
|
||||
if Self::find(ws, u) == u && Self::growing(ws, u) {
|
||||
return Err(MatchError::Unmatchable);
|
||||
}
|
||||
}
|
||||
Ok(self.peel(ws))
|
||||
}
|
||||
|
||||
/// Peel every cluster's spanning tree of filled edges, leaves first.
|
||||
fn peel(&self, ws: &mut Workspace) -> u64 {
|
||||
let boundary = (self.nodes - 1) as u32;
|
||||
let mut out = 0u64;
|
||||
let mut roots: Vec<u32> = Vec::new();
|
||||
for k in 0..ws.touched.len() {
|
||||
let u = ws.touched[k];
|
||||
if Self::find(ws, u) == u {
|
||||
roots.push(u);
|
||||
}
|
||||
}
|
||||
roots.sort_unstable();
|
||||
for r in roots {
|
||||
let start_node = if ws.boundary[r as usize] { boundary } else { r };
|
||||
ws.order.clear();
|
||||
ws.order.push(start_node);
|
||||
ws.seen[start_node as usize] = true;
|
||||
let mut head = 0;
|
||||
while head < ws.order.len() {
|
||||
let u = ws.order[head] as usize;
|
||||
head += 1;
|
||||
for k in self.start[u]..self.start[u + 1] {
|
||||
let (v, e) = (self.nbr[k] as usize, self.edge[k] as usize);
|
||||
if !ws.in_use[v] || ws.seen[v] || !ws.edge_in_use[e] || !ws.full[e] {
|
||||
continue;
|
||||
}
|
||||
if Self::find(ws, v as u32) != r {
|
||||
continue;
|
||||
}
|
||||
ws.seen[v] = true;
|
||||
ws.via[v] = e as u32;
|
||||
ws.order.push(v as u32);
|
||||
}
|
||||
}
|
||||
for idx in (1..ws.order.len()).rev() {
|
||||
let u = ws.order[idx];
|
||||
if ws.defect[u as usize] {
|
||||
let e = ws.via[u as usize] as usize;
|
||||
let (a, b) = self.ends[e];
|
||||
let parent = if a == u { b } else { a };
|
||||
out ^= self.obs[e];
|
||||
ws.defect[u as usize] = false;
|
||||
ws.defect[parent as usize] ^= true;
|
||||
}
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// Decode every shot; returns how many predictions missed the actual
|
||||
/// observable flips.
|
||||
pub fn failures(&self, shots: &Detections) -> Result<u64, MatchError> {
|
||||
let mut ws = self.workspace();
|
||||
let mut n = 0;
|
||||
for s in 0..shots.shots {
|
||||
if self.decode(&shots.fired(s), &mut ws)? != shots.flips(s) {
|
||||
n += 1;
|
||||
}
|
||||
}
|
||||
Ok(n)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// The round-based form: every odd cluster advances by the next fill time
|
||||
/// each round. The event-driven decoder must agree with it.
|
||||
#[derive(Clone, Debug)]
|
||||
struct RoundBased {
|
||||
/// Nodes: the detectors, then the boundary.
|
||||
nodes: usize,
|
||||
/// Adjacency in compressed rows: neighbour, weight, observables, edge id.
|
||||
start: Vec<usize>,
|
||||
nbr: Vec<u32>,
|
||||
weight: Vec<i64>,
|
||||
obs: Vec<u64>,
|
||||
edge: Vec<u32>,
|
||||
edges: usize,
|
||||
}
|
||||
|
||||
/// Per-shot state, reused across decodes.
|
||||
#[derive(Clone, Debug, Default)]
|
||||
struct RoundWorkspace {
|
||||
parent: Vec<u32>,
|
||||
/// Root data: odd parity, holds the boundary, members.
|
||||
odd: Vec<bool>,
|
||||
boundary: Vec<bool>,
|
||||
members: Vec<Vec<u32>>,
|
||||
in_use: Vec<bool>,
|
||||
touched: Vec<u32>,
|
||||
growth: Vec<i64>,
|
||||
grown: Vec<u32>,
|
||||
defect: Vec<bool>,
|
||||
// Peeling scratch.
|
||||
order: Vec<u32>,
|
||||
via: Vec<usize>,
|
||||
seen: Vec<bool>,
|
||||
}
|
||||
|
||||
impl RoundBased {
|
||||
/// Build the decoder from a matching graph.
|
||||
pub fn new(g: &MatchingGraph) -> RoundBased {
|
||||
let nodes = g.detectors() + 1;
|
||||
let mut ids: std::collections::BTreeMap<(u32, u32), u32> = std::collections::BTreeMap::new();
|
||||
let mut start = vec![0];
|
||||
let (mut nbr, mut weight, mut obs, mut edge) = (Vec::new(), Vec::new(), Vec::new(), Vec::new());
|
||||
for u in 0..nodes {
|
||||
for &(v, w, o) in g.neighbours(u) {
|
||||
let key = ((u as u32).min(v), (u as u32).max(v));
|
||||
let next = ids.len() as u32;
|
||||
let id = *ids.entry(key).or_insert(next);
|
||||
nbr.push(v);
|
||||
weight.push(w.max(0));
|
||||
obs.push(o);
|
||||
edge.push(id);
|
||||
}
|
||||
start.push(nbr.len());
|
||||
}
|
||||
RoundBased { nodes, start, nbr, weight, obs, edge, edges: ids.len() }
|
||||
}
|
||||
|
||||
/// A workspace sized for this decoder.
|
||||
pub fn workspace(&self) -> RoundWorkspace {
|
||||
RoundWorkspace {
|
||||
parent: (0..self.nodes as u32).collect(),
|
||||
odd: vec![false; self.nodes],
|
||||
boundary: vec![false; self.nodes],
|
||||
members: vec![Vec::new(); self.nodes],
|
||||
in_use: vec![false; self.nodes],
|
||||
touched: Vec::new(),
|
||||
growth: vec![0; self.edges],
|
||||
grown: Vec::new(),
|
||||
defect: vec![false; self.nodes],
|
||||
order: Vec::new(),
|
||||
via: vec![usize::MAX; self.nodes],
|
||||
seen: vec![false; self.nodes],
|
||||
}
|
||||
}
|
||||
|
||||
fn find(ws: &mut RoundWorkspace, mut u: u32) -> u32 {
|
||||
while ws.parent[u as usize] != u {
|
||||
let p = ws.parent[u as usize];
|
||||
ws.parent[u as usize] = ws.parent[p as usize];
|
||||
u = ws.parent[u as usize];
|
||||
}
|
||||
u
|
||||
}
|
||||
|
||||
fn touch(&self, ws: &mut RoundWorkspace, u: u32) {
|
||||
let i = u as usize;
|
||||
if !ws.in_use[i] {
|
||||
ws.in_use[i] = true;
|
||||
ws.touched.push(u);
|
||||
ws.parent[i] = u;
|
||||
ws.odd[i] = false;
|
||||
ws.boundary[i] = i == self.nodes - 1;
|
||||
ws.members[i].clear();
|
||||
ws.members[i].push(u);
|
||||
ws.defect[i] = false;
|
||||
}
|
||||
}
|
||||
|
||||
fn union(ws: &mut RoundWorkspace, a: u32, b: u32) {
|
||||
let (mut ra, mut rb) = (Self::find(ws, a), Self::find(ws, b));
|
||||
if ra == rb {
|
||||
return;
|
||||
}
|
||||
if ws.members[ra as usize].len() < ws.members[rb as usize].len() {
|
||||
core::mem::swap(&mut ra, &mut rb);
|
||||
}
|
||||
ws.parent[rb as usize] = ra;
|
||||
let moved = core::mem::take(&mut ws.members[rb as usize]);
|
||||
ws.members[ra as usize].extend(moved);
|
||||
ws.odd[ra as usize] ^= ws.odd[rb as usize];
|
||||
ws.boundary[ra as usize] |= ws.boundary[rb as usize];
|
||||
}
|
||||
|
||||
fn reset(ws: &mut RoundWorkspace) {
|
||||
for &u in &ws.touched {
|
||||
let i = u as usize;
|
||||
ws.in_use[i] = false;
|
||||
ws.parent[i] = u;
|
||||
ws.members[i].clear();
|
||||
ws.defect[i] = false;
|
||||
ws.seen[i] = false;
|
||||
ws.via[i] = usize::MAX;
|
||||
}
|
||||
ws.touched.clear();
|
||||
for &e in &ws.grown {
|
||||
ws.growth[e as usize] = 0;
|
||||
}
|
||||
ws.grown.clear();
|
||||
}
|
||||
|
||||
/// Predict which observables flipped, from the detectors that fired.
|
||||
pub fn decode(&self, fired: &[u32], ws: &mut RoundWorkspace) -> Result<u64, MatchError> {
|
||||
Self::reset(ws);
|
||||
for &d in fired {
|
||||
self.touch(ws, d);
|
||||
ws.odd[d as usize] ^= true;
|
||||
ws.defect[d as usize] ^= true;
|
||||
}
|
||||
let mut roots: Vec<u32> = Vec::new();
|
||||
let mut full: Vec<(u32, u32)> = Vec::new();
|
||||
loop {
|
||||
roots.clear();
|
||||
for k in 0..ws.touched.len() {
|
||||
let u = ws.touched[k];
|
||||
if Self::find(ws, u) == u && ws.odd[u as usize] && !ws.boundary[u as usize] {
|
||||
roots.push(u);
|
||||
}
|
||||
}
|
||||
if roots.is_empty() {
|
||||
break;
|
||||
}
|
||||
roots.sort_unstable();
|
||||
// The next edge to fill sets the step.
|
||||
let mut step = i64::MAX;
|
||||
for &r in &roots {
|
||||
for m in 0..ws.members[r as usize].len() {
|
||||
let u = ws.members[r as usize][m] as usize;
|
||||
for k in self.start[u]..self.start[u + 1] {
|
||||
let v = self.nbr[k];
|
||||
let rv = if ws.in_use[v as usize] { Self::find(ws, v) } else { v };
|
||||
if ws.in_use[v as usize] && rv == r {
|
||||
continue;
|
||||
}
|
||||
let rem = self.weight[k] - ws.growth[self.edge[k] as usize];
|
||||
if rem <= 0 {
|
||||
step = 0;
|
||||
continue;
|
||||
}
|
||||
let speed = if ws.in_use[v as usize] && ws.odd[rv as usize] && !ws.boundary[rv as usize] { 2 } else { 1 };
|
||||
step = step.min((rem + speed - 1) / speed);
|
||||
}
|
||||
}
|
||||
}
|
||||
if step == i64::MAX {
|
||||
return Err(MatchError::Unmatchable);
|
||||
}
|
||||
full.clear();
|
||||
for &r in &roots {
|
||||
for m in 0..ws.members[r as usize].len() {
|
||||
let u = ws.members[r as usize][m];
|
||||
for k in self.start[u as usize]..self.start[u as usize + 1] {
|
||||
let v = self.nbr[k];
|
||||
if ws.in_use[v as usize] && Self::find(ws, v) == r {
|
||||
continue;
|
||||
}
|
||||
let e = self.edge[k] as usize;
|
||||
if ws.growth[e] < self.weight[k] {
|
||||
if ws.growth[e] == 0 {
|
||||
ws.grown.push(e as u32);
|
||||
}
|
||||
ws.growth[e] = (ws.growth[e] + step).min(self.weight[k]);
|
||||
}
|
||||
if ws.growth[e] >= self.weight[k] {
|
||||
full.push((u, v));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for &(u, v) in &full {
|
||||
self.touch(ws, v);
|
||||
Self::union(ws, u, v);
|
||||
}
|
||||
}
|
||||
Ok(self.peel(ws))
|
||||
}
|
||||
|
||||
/// Peel every cluster's spanning tree of filled edges, leaves first.
|
||||
fn peel(&self, ws: &mut RoundWorkspace) -> u64 {
|
||||
let boundary = (self.nodes - 1) as u32;
|
||||
let mut out = 0u64;
|
||||
// Roots, the boundary's cluster first so it is rooted there.
|
||||
let mut roots: Vec<u32> = Vec::new();
|
||||
for k in 0..ws.touched.len() {
|
||||
let u = ws.touched[k];
|
||||
if Self::find(ws, u) == u {
|
||||
roots.push(u);
|
||||
}
|
||||
}
|
||||
roots.sort_unstable();
|
||||
for r in roots {
|
||||
let start_node = if ws.boundary[r as usize] { boundary } else { r };
|
||||
ws.order.clear();
|
||||
ws.order.push(start_node);
|
||||
ws.seen[start_node as usize] = true;
|
||||
let mut head = 0;
|
||||
while head < ws.order.len() {
|
||||
let u = ws.order[head] as usize;
|
||||
head += 1;
|
||||
for k in self.start[u]..self.start[u + 1] {
|
||||
let v = self.nbr[k] as usize;
|
||||
if !ws.in_use[v] || ws.seen[v] || ws.growth[self.edge[k] as usize] < self.weight[k] {
|
||||
continue;
|
||||
}
|
||||
if Self::find(ws, v as u32) != r {
|
||||
continue;
|
||||
}
|
||||
ws.seen[v] = true;
|
||||
ws.via[v] = k;
|
||||
ws.order.push(v as u32);
|
||||
}
|
||||
}
|
||||
for idx in (1..ws.order.len()).rev() {
|
||||
let u = ws.order[idx] as usize;
|
||||
if ws.defect[u] {
|
||||
let k = ws.via[u];
|
||||
// The edge stored at `k` runs from the parent to `u`.
|
||||
let parent = self.edge_source(k);
|
||||
out ^= self.obs[k];
|
||||
ws.defect[u] = false;
|
||||
ws.defect[parent] ^= true;
|
||||
}
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// The node whose adjacency row holds entry `k`.
|
||||
fn edge_source(&self, k: usize) -> usize {
|
||||
self.start.partition_point(|&s| s <= k) - 1
|
||||
}
|
||||
}
|
||||
|
||||
use crate::quantum_frame::{error_model, parse, sample, surface_code_memory};
|
||||
|
||||
fn graph(d: u32, p: f64) -> (crate::quantum_frame::Circuit, MatchingGraph) {
|
||||
let c = parse(&surface_code_memory(d, d, p)).unwrap();
|
||||
let g = MatchingGraph::from_model(&error_model(&c).unwrap()).unwrap();
|
||||
(c, g)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn every_single_fault_is_corrected() {
|
||||
for d in [3, 5] {
|
||||
let (_, g) = graph(d, 0.003);
|
||||
let uf = UnionFind::new(&g);
|
||||
let mut ws = uf.workspace();
|
||||
let b = g.detectors() as u32;
|
||||
let mut checked = 0;
|
||||
for u in 0..=g.detectors() {
|
||||
for &(v, _, o) in g.neighbours(u) {
|
||||
if (u as u32) < v {
|
||||
let fired: Vec<u32> = [u as u32, v].into_iter().filter(|&x| x != b).collect();
|
||||
assert_eq!(uf.decode(&fired, &mut ws).unwrap(), o, "d={d} edge ({u},{v})");
|
||||
checked += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
assert!(checked > 100);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_radix_heap_pops_in_order() {
|
||||
let mut h = RadixHeap::default();
|
||||
let mut rng = 0x1234_5678u64;
|
||||
let mut keys = Vec::new();
|
||||
let mut last = 0u64;
|
||||
for round in 0..200 {
|
||||
for _ in 0..(round % 7) {
|
||||
rng = rng.wrapping_mul(6364136223846793005).wrapping_add(1442695040888963407);
|
||||
let k = last + (rng >> 40) % 1000;
|
||||
h.push(k, round, 0);
|
||||
keys.push(k);
|
||||
}
|
||||
if let Some((k, _, _)) = h.pop() {
|
||||
keys.sort_unstable();
|
||||
assert_eq!(k, keys.remove(0));
|
||||
last = k;
|
||||
}
|
||||
}
|
||||
while let Some((k, _, _)) = h.pop() {
|
||||
keys.sort_unstable();
|
||||
assert_eq!(k, keys.remove(0));
|
||||
}
|
||||
assert!(keys.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_fired_detectors_means_no_flip() {
|
||||
let (_, g) = graph(3, 0.003);
|
||||
let uf = UnionFind::new(&g);
|
||||
assert_eq!(uf.decode(&[], &mut uf.workspace()).unwrap(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn it_tracks_exact_matching_and_distance_helps() {
|
||||
// Same shots for both decoders.
|
||||
let mut rates = Vec::new();
|
||||
for d in [3, 5, 7] {
|
||||
let (c, g) = graph(d, 0.004);
|
||||
let det = sample(&c, 6_000, 11);
|
||||
let uf = UnionFind::new(&g).failures(&det).unwrap();
|
||||
let mw = g.failures(&det).unwrap();
|
||||
// Approximate, never far: within a factor of two of exact here.
|
||||
assert!(uf >= mw / 2 && uf <= 2 * mw + 10, "d={d}: uf {uf}, mwpm {mw}");
|
||||
rates.push(uf);
|
||||
}
|
||||
assert!(rates[0] > rates[1] && rates[1] > rates[2], "{rates:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_reused_workspace_changes_nothing() {
|
||||
let (c, g) = graph(5, 0.006);
|
||||
let uf = UnionFind::new(&g);
|
||||
let det = sample(&c, 300, 4);
|
||||
let mut shared = uf.workspace();
|
||||
for s in 0..300 {
|
||||
let a = uf.decode(&det.fired(s), &mut shared).unwrap();
|
||||
let b = uf.decode(&det.fired(s), &mut uf.workspace()).unwrap();
|
||||
assert_eq!(a, b, "shot {s}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn event_driven_growth_matches_rounds() {
|
||||
for (d, p) in [(5, 0.006), (7, 0.004), (9, 0.005)] {
|
||||
let (c, g) = graph(d, p);
|
||||
let det = sample(&c, 2000, 13);
|
||||
let (ev, rb) = (UnionFind::new(&g), RoundBased::new(&g));
|
||||
let (mut we, mut wr) = (ev.workspace(), rb.workspace());
|
||||
let mut differ = 0;
|
||||
for s in 0..2000 {
|
||||
let f = det.fired(s);
|
||||
if ev.decode(&f, &mut we).unwrap() != rb.decode(&f, &mut wr).unwrap() {
|
||||
differ += 1;
|
||||
}
|
||||
}
|
||||
// Ties at equal fill times may resolve in a different order.
|
||||
assert!(differ <= 4, "d={d}: {differ} of 2000 differ");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -22,7 +22,8 @@
|
|||
feature = "quantum_sv",
|
||||
feature = "quantum_arch",
|
||||
feature = "quantum_ldpc",
|
||||
feature = "quantum_qio"
|
||||
feature = "quantum_qio",
|
||||
feature = "quantum_uf"
|
||||
))]
|
||||
|
||||
use wai_quantum::*;
|
||||
|
|
@ -393,3 +394,18 @@ fn quantum_inspired_optimisation() {
|
|||
v.extend(g.spins.iter().map(|&s| f64::from(s)));
|
||||
assert_eq!(digest(&v), 0xc01a_7afa_39c0_c487);
|
||||
}
|
||||
|
||||
/// Union-find decoding: event-driven growth on the radix heap, merges and
|
||||
/// peeling, over a sampled surface-code memory, with a reused workspace.
|
||||
#[test]
|
||||
fn union_find_decoding() {
|
||||
use quantum_frame::{error_model, parse, sample, surface_code_memory};
|
||||
let c = parse(&surface_code_memory(7, 7, 0.006)).unwrap();
|
||||
let g = quantum_match::MatchingGraph::from_model(&error_model(&c).unwrap()).unwrap();
|
||||
let uf = quantum_uf::UnionFind::new(&g);
|
||||
let mut ws = uf.workspace();
|
||||
let det = sample(&c, 512, 21);
|
||||
let v: Vec<f64> = (0..512).map(|s| uf.decode(&det.fired(s), &mut ws).unwrap() as f64).collect();
|
||||
assert!(v.iter().any(|&x| x != 0.0), "the sample must include flips");
|
||||
assert_eq!(digest(&v), 0xe43c_2ad4_9244_3118);
|
||||
}
|
||||
|
|
|
|||
2
wai/wai-rs/Cargo.lock
generated
2
wai/wai-rs/Cargo.lock
generated
|
|
@ -3900,7 +3900,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "wai-quantum"
|
||||
version = "0.3.31"
|
||||
version = "0.3.32"
|
||||
dependencies = [
|
||||
"blake3",
|
||||
"ed25519-dalek",
|
||||
|
|
|
|||
|
|
@ -355,7 +355,7 @@ quantum_resource = ["dep:wai-quantum", "wai-quantum/quantum_resource"]
|
|||
quantum_resource_receipt = ["quantum_resource", "wai-quantum/quantum_resource_receipt"]
|
||||
# Noisy Clifford circuits at QEC scale: frame sampling, detector error models,
|
||||
# exact matching, measured code models.
|
||||
quantum_frame = ["dep:wai-quantum", "wai-quantum/quantum_frame"]
|
||||
quantum_frame = ["dep:wai-quantum", "wai-quantum/quantum_frame", "wai-quantum/quantum_uf"]
|
||||
# Tensor-network contraction: hyper-optimised trees, slicing, circuit amplitudes.
|
||||
quantum_tn = ["dep:wai-quantum", "wai-quantum/quantum_tn"]
|
||||
# Gaussian boson sampling: hafnians, torontonians, exact probabilities, samples, checks.
|
||||
|
|
|
|||
|
|
@ -157,7 +157,7 @@ fn usage() -> ! {
|
|||
wai-quantum estimate --qubits N [--ccz N] [--t N] [--rotations N] [--depth N] [--measurements N] ...\n \
|
||||
Qubits: us-e3, us-e4, ns-e3, ns-e4, meas-e4, meas-e6. A measured code: --prefactor A --threshold P.\n\n\
|
||||
ERROR CORRECTION AT CIRCUIT LEVEL:\n \
|
||||
wai-quantum qec [-d D] [--rounds R] [--p P] [--shots N] [--seed S] [--json]\n \
|
||||
wai-quantum qec [-d D] [--rounds R] [--p P] [--shots N] [--seed S] [--decoder exact|uf] [--json]\n \
|
||||
wai-quantum qec --file CIRCUIT [--shots N] (the ecosystem's circuit text)\n \
|
||||
wai-quantum qec [--circuit | --dem] [-d D | --file CIRCUIT]\n \
|
||||
wai-quantum qec --fit [--shots N] (measure a and p* of the code model)\n\n\
|
||||
|
|
@ -786,11 +786,17 @@ fn main() {
|
|||
}
|
||||
let g = MatchingGraph::from_model(&model).unwrap_or_else(|e| die(&format!("matching graph: {e:?}")));
|
||||
let s = sample(&c, shots, seed);
|
||||
let failures = g.failures(&s).unwrap_or_else(|e| die(&format!("decode: {e:?}")));
|
||||
let decoder = opt(args, "--decoder", "").unwrap_or_else(|| "exact".into());
|
||||
let failures = match decoder.as_str() {
|
||||
"exact" => g.failures(&s),
|
||||
"uf" => wai::quantum_uf::UnionFind::new(&g).failures(&s),
|
||||
_ => die("--decoder: exact or uf"),
|
||||
}
|
||||
.unwrap_or_else(|e| die(&format!("decode: {e:?}")));
|
||||
let rate = failures as f64 / shots as f64;
|
||||
if json {
|
||||
println!(
|
||||
"{{\"detectors\":{},\"failures\":{failures},\"mechanisms\":{},\"observables\":{},\"qubits\":{},\"seed\":{seed},\"shots\":{shots}}}",
|
||||
"{{\"decoder\":\"{decoder}\",\"detectors\":{},\"failures\":{failures},\"mechanisms\":{},\"observables\":{},\"qubits\":{},\"seed\":{seed},\"shots\":{shots}}}",
|
||||
model.detectors,
|
||||
model.mechanisms.len(),
|
||||
model.observables,
|
||||
|
|
@ -799,7 +805,7 @@ fn main() {
|
|||
} else {
|
||||
println!("circuit {} qubits, {} detectors, {} observables", c.qubits, model.detectors, model.observables);
|
||||
println!("model {} error mechanisms", model.mechanisms.len());
|
||||
println!("failures {failures} of {shots} shots ({rate:.3e} per shot)");
|
||||
println!("failures {failures} of {shots} shots ({rate:.3e} per shot, {decoder} decoder)");
|
||||
if opt(args, "--file", "").is_none() {
|
||||
let per_round = wai::quantum_match::MemoryPoint { distance: d, rounds, p, shots: shots as u64, failures }.per_round();
|
||||
println!("per round {per_round:.3e} (d = {d}, {rounds} rounds, p = {p})");
|
||||
|
|
|
|||
|
|
@ -636,6 +636,9 @@ pub use wai_quantum::quantum_frame;
|
|||
#[cfg(feature = "quantum_frame")]
|
||||
pub use wai_quantum::quantum_match;
|
||||
|
||||
#[cfg(feature = "quantum_frame")]
|
||||
pub use wai_quantum::quantum_uf;
|
||||
|
||||
#[cfg(feature = "quantum_tn")]
|
||||
pub use wai_quantum::quantum_tn;
|
||||
|
||||
|
|
|
|||
2
wai/wai-web/Cargo.lock
generated
2
wai/wai-web/Cargo.lock
generated
|
|
@ -734,7 +734,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "wai-quantum"
|
||||
version = "0.3.31"
|
||||
version = "0.3.32"
|
||||
dependencies = [
|
||||
"blake3",
|
||||
"ed25519-dalek",
|
||||
|
|
|
|||
Loading…
Reference in a new issue