From e76acbe9e50d15ce6b22177566e789cda0b3aff9 Mon Sep 17 00:00:00 2001 From: Transaction Science Date: Mon, 5 Oct 2026 07:46:12 -0400 Subject: [PATCH 1/2] wai-quantum 0.3.32: union-find decoding in near-linear time A fault-tolerant machine must decode each syndrome round before the next arrives. Exact matching (quantum_match) is the accuracy reference; quantum_uf is the fast decoder over the same matching graph: - Grow: every fired detector starts a cluster; odd clusters (not on the boundary) grow all frontier edges at once, on the graph's integer weights, an edge between two growing clusters at double speed. - Merge: a filled edge joins its clusters; a cluster on the boundary stops. - Peel: each cluster's filled edges as a spanning tree, rooted at the boundary when it holds it, peeled leaves-first into a correction. - Growth is event-driven: each frontier edge carries a fill time, and a merge reschedules only the frontier of a side whose growth changed. Fill times never run backwards, so events sit in a radix heap at amortised O(1). Profiling put 45% of the time in a binary heap before; the radix heap halved the time per shot at d = 21. Time now grows linearly with fired detectors, about 1.1-1.5 us each from d = 9 to d = 21. - A reusable workspace resets in time proportional to the last shot. Checked: - every single edge of d = 3 and d = 5 graphs is corrected exactly; - the event-driven schedule agrees with a round-by-round one (kept in the tests) on 2,000 shots at each of three distances, up to tie order; - on the same 20,000 shots, union-find fails 0.92-1.18x as often as exact matching (d = 3-9, p = 0.003-0.007), 77x faster at d = 11; its threshold sits a little lower (at p = 0.007 its failures stop falling with distance). CLI: wai-quantum qec --decoder uf|exact. Example: uf_bench. Pinned bit for bit natively and under wasm32-wasip2 (17 pins); CLI output byte-identical. Co-Authored-By: Claude Opus 5.5 --- wai/wai-quantum-serve/Cargo.lock | 2 +- wai/wai-quantum/Cargo.lock | 2 +- wai/wai-quantum/Cargo.toml | 10 +- wai/wai-quantum/README.md | 15 + wai/wai-quantum/examples/uf_bench.rs | 46 ++ wai/wai-quantum/src/lib.rs | 3 + wai/wai-quantum/src/quantum_match.rs | 15 +- wai/wai-quantum/src/quantum_uf.rs | 814 ++++++++++++++++++++++++++ wai/wai-quantum/tests/bit_identity.rs | 16 +- wai/wai-rs/Cargo.lock | 2 +- wai/wai-rs/Cargo.toml | 2 +- wai/wai-rs/src/bin/wai_quantum.rs | 14 +- wai/wai-rs/src/lib.rs | 3 + wai/wai-web/Cargo.lock | 2 +- 14 files changed, 932 insertions(+), 14 deletions(-) create mode 100644 wai/wai-quantum/examples/uf_bench.rs create mode 100644 wai/wai-quantum/src/quantum_uf.rs diff --git a/wai/wai-quantum-serve/Cargo.lock b/wai/wai-quantum-serve/Cargo.lock index 72b29a56e..1609f71cb 100644 --- a/wai/wai-quantum-serve/Cargo.lock +++ b/wai/wai-quantum-serve/Cargo.lock @@ -589,7 +589,7 @@ dependencies = [ [[package]] name = "wai-quantum" -version = "0.3.31" +version = "0.3.32" dependencies = [ "blake3", "ed25519-dalek", diff --git a/wai/wai-quantum/Cargo.lock b/wai/wai-quantum/Cargo.lock index 47e0fe263..102e54b7b 100644 --- a/wai/wai-quantum/Cargo.lock +++ b/wai/wai-quantum/Cargo.lock @@ -415,7 +415,7 @@ checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" [[package]] name = "wai-quantum" -version = "0.3.31" +version = "0.3.32" dependencies = [ "blake3", "ed25519-dalek", diff --git a/wai/wai-quantum/Cargo.toml b/wai/wai-quantum/Cargo.toml index 80bf17974..b0cef1cd2 100644 --- a/wai/wai-quantum/Cargo.toml +++ b/wai/wai-quantum/Cargo.toml @@ -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"] diff --git a/wai/wai-quantum/README.md b/wai/wai-quantum/README.md index cb9eaa45f..17fa6757b 100644 --- a/wai/wai-quantum/README.md +++ b/wai/wai-quantum/README.md @@ -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 diff --git a/wai/wai-quantum/examples/uf_bench.rs b/wai/wai-quantum/examples/uf_bench.rs new file mode 100644 index 000000000..a99bc2952 --- /dev/null +++ b/wai/wai-quantum/examples/uf_bench.rs @@ -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 = 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::().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> = (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::() 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 + ); + } +} diff --git a/wai/wai-quantum/src/lib.rs b/wai/wai-quantum/src/lib.rs index 85a7d16e4..a8bee44d5 100644 --- a/wai/wai-quantum/src/lib.rs +++ b/wai/wai-quantum/src/lib.rs @@ -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; diff --git a/wai/wai-quantum/src/quantum_match.rs b/wai/wai-quantum/src/quantum_match.rs index cde7fdcda..38f2156ad 100644 --- a/wai/wai-quantum/src/quantum_match.rs +++ b/wai/wai-quantum/src/quantum_match.rs @@ -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 { diff --git a/wai/wai-quantum/src/quantum_uf.rs b/wai/wai-quantum/src/quantum_uf.rs new file mode 100644 index 000000000..01250f8fa --- /dev/null +++ b/wai/wai-quantum/src/quantum_uf.rs @@ -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>, +} + +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, + nbr: Vec, + edge: Vec, + /// Per edge: its ends, its doubled weight (growth is counted in half + /// units), and its observables. + ends: Vec<(u32, u32)>, + cap: Vec, + obs: Vec, +} + +/// 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, + /// Root data: odd parity, holds the boundary, members, frontier edges. + odd: Vec, + boundary: Vec, + members: Vec>, + frontier: Vec>, + in_use: Vec, + touched: Vec, + // Edge state: growth at `tau`, speed since then, version, filled. + growth: Vec, + tau: Vec, + speed: Vec, + version: Vec, + full: Vec, + edge_in_use: Vec, + edges_touched: Vec, + events: RadixHeap, + defect: Vec, + // Peeling scratch. + order: Vec, + via: Vec, + seen: Vec, +} + +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 { + 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 = 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 { + 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, + nbr: Vec, + weight: Vec, + obs: Vec, + edge: Vec, + edges: usize, + } + + /// Per-shot state, reused across decodes. + #[derive(Clone, Debug, Default)] + struct RoundWorkspace { + parent: Vec, + /// Root data: odd parity, holds the boundary, members. + odd: Vec, + boundary: Vec, + members: Vec>, + in_use: Vec, + touched: Vec, + growth: Vec, + grown: Vec, + defect: Vec, + // Peeling scratch. + order: Vec, + via: Vec, + seen: Vec, + } + + 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 { + 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 = 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 = 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 = [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, 20_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"); + } + } +} diff --git a/wai/wai-quantum/tests/bit_identity.rs b/wai/wai-quantum/tests/bit_identity.rs index 9a66a7910..ef1b9f225 100644 --- a/wai/wai-quantum/tests/bit_identity.rs +++ b/wai/wai-quantum/tests/bit_identity.rs @@ -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::*; @@ -392,4 +393,17 @@ fn quantum_inspired_optimisation() { v.push(g.energy); 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 = (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); } diff --git a/wai/wai-rs/Cargo.lock b/wai/wai-rs/Cargo.lock index aadbabb04..7662723de 100644 --- a/wai/wai-rs/Cargo.lock +++ b/wai/wai-rs/Cargo.lock @@ -3900,7 +3900,7 @@ dependencies = [ [[package]] name = "wai-quantum" -version = "0.3.31" +version = "0.3.32" dependencies = [ "blake3", "ed25519-dalek", diff --git a/wai/wai-rs/Cargo.toml b/wai/wai-rs/Cargo.toml index ac0aec25b..c4cb5890a 100644 --- a/wai/wai-rs/Cargo.toml +++ b/wai/wai-rs/Cargo.toml @@ -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. diff --git a/wai/wai-rs/src/bin/wai_quantum.rs b/wai/wai-rs/src/bin/wai_quantum.rs index d80cf5949..9d1d070f8 100644 --- a/wai/wai-rs/src/bin/wai_quantum.rs +++ b/wai/wai-rs/src/bin/wai_quantum.rs @@ -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})"); diff --git a/wai/wai-rs/src/lib.rs b/wai/wai-rs/src/lib.rs index e372519fe..86e2c78a9 100644 --- a/wai/wai-rs/src/lib.rs +++ b/wai/wai-rs/src/lib.rs @@ -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; diff --git a/wai/wai-web/Cargo.lock b/wai/wai-web/Cargo.lock index 321aa28d8..954f3a2f3 100644 --- a/wai/wai-web/Cargo.lock +++ b/wai/wai-web/Cargo.lock @@ -734,7 +734,7 @@ dependencies = [ [[package]] name = "wai-quantum" -version = "0.3.31" +version = "0.3.32" dependencies = [ "blake3", "ed25519-dalek", -- 2.54.0 (Apple Git-157) From 79dd50d20bf50f3d02d2be9ab66ab9f3fd0485f3 Mon Sep 17 00:00:00 2001 From: Transaction Science Date: Mon, 5 Oct 2026 09:07:52 -0400 Subject: [PATCH 2/2] wai-quantum 0.3.32: close the optimisation pin's test function The restack onto 0.3.31 left tests/bit_identity.rs with an unclosed function, so the integration tests did not compile (the linux energy-receipts job). The fix was staged after the commit by mistake. Also trims the union-find vs exact matching comparison to 6,000 shots per distance (about 25 s in debug). Co-Authored-By: Claude Opus 5.5 --- wai/wai-quantum/src/quantum_uf.rs | 2 +- wai/wai-quantum/tests/bit_identity.rs | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/wai/wai-quantum/src/quantum_uf.rs b/wai/wai-quantum/src/quantum_uf.rs index 01250f8fa..d0687841c 100644 --- a/wai/wai-quantum/src/quantum_uf.rs +++ b/wai/wai-quantum/src/quantum_uf.rs @@ -770,7 +770,7 @@ mod tests { let mut rates = Vec::new(); for d in [3, 5, 7] { let (c, g) = graph(d, 0.004); - let det = sample(&c, 20_000, 11); + 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. diff --git a/wai/wai-quantum/tests/bit_identity.rs b/wai/wai-quantum/tests/bit_identity.rs index ef1b9f225..a192b995c 100644 --- a/wai/wai-quantum/tests/bit_identity.rs +++ b/wai/wai-quantum/tests/bit_identity.rs @@ -393,6 +393,8 @@ fn quantum_inspired_optimisation() { v.push(g.energy); 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] -- 2.54.0 (Apple Git-157)