diff --git a/wai/wai-quantum/src/quantum_noisy.rs b/wai/wai-quantum/src/quantum_noisy.rs index e55c97e1b..b395f78f7 100644 --- a/wai/wai-quantum/src/quantum_noisy.rs +++ b/wai/wai-quantum/src/quantum_noisy.rs @@ -1213,21 +1213,45 @@ mod tests { /// Threads, the state limit and the order branches run in change nothing: /// below 16 qubits (many trees) and above (one tree, replays). + /// + /// The reference run is pinned by its bits, the same on every target, so a + /// failure says whether the reference itself moved or only a variant. From + /// 16 qubits the run is one tree on the state vector's own threads, which + /// split a pass only from 2¹⁸ amplitudes a thread; the 16-qubit case forces + /// the split paths too, where it ran them before that threshold. #[test] fn branching_runs_depend_on_neither_threads_nor_the_state_limit() { + use crate::quantum_sv::with_forced_threads; let noise = NoiseModel { depol1: 0.02, depol2: 0.05, damping: 0.01, dephasing: 0.01, readout: 0.02 }; - for (n, shots) in [(6u32, 300u64), (16, 40)] { + for (n, shots, pin) in [(6u32, 300u64, 0xf1f4_2a95_7ae8_7a7e_u64), (16, 40, 0x5e90_2d74_edf2_7819)] { let g = random_circuit(n, 3, 9); let a = branching_trajectories(&g, n, &noise, &observables(n), shots, 3, 64, 1).unwrap(); + assert_eq!(run_digest(&a), pin, "n {n}: the reference run is not the pinned one: {:?}", a.expectations); for (states, threads) in [(1usize, 1usize), (2, 5), (64, 5)] { let b = branching_trajectories(&g, n, &noise, &observables(n), shots, 3, states, threads).unwrap(); // Replays redo gates, so only the work differs. assert_eq!((&b.expectations, &b.counts, b.shots, b.histories), (&a.expectations, &a.counts, a.shots, a.histories), "n {n}: {states} states, {threads} threads"); assert!(b.gate_applications >= a.gate_applications); } + // (A target without threads has no split paths to force.) + if n >= 16 && !cfg!(target_arch = "wasm32") { + for (states, sv_threads) in [(1usize, 2usize), (64, 4)] { + let b = with_forced_threads(Some(sv_threads), || branching_trajectories(&g, n, &noise, &observables(n), shots, 3, states, 1).unwrap()); + assert_eq!(run_digest(&b), pin, "n {n}: {states} states, the state vector on {sv_threads} threads: {:?}", b.expectations); + } + } } } + /// A run's estimates, counts, shots and histories, as one FNV-1a digest of + /// their bits. + fn run_digest(r: &BranchingRun) -> u64 { + let mut v: Vec = r.expectations.iter().flat_map(|e| [e.mean.to_bits(), e.stderr.to_bits()]).collect(); + v.extend(r.counts.iter().flat_map(|&(x, c)| [x, c])); + v.extend([r.shots, r.histories]); + v.iter().fold(0xcbf2_9ce4_8422_2325u64, |h, &w| (h ^ w).wrapping_mul(0x0000_0100_0000_01b3)) + } + /// With rare errors most shots share the error-free history. #[test] fn rare_errors_share_the_work() { diff --git a/wai/wai-quantum/src/quantum_sv.rs b/wai/wai-quantum/src/quantum_sv.rs index 148865f54..00e2b3d62 100644 --- a/wai/wai-quantum/src/quantum_sv.rs +++ b/wai/wai-quantum/src/quantum_sv.rs @@ -287,6 +287,16 @@ thread_local! { static ONE_THREAD: core::cell::Cell = const { core::cell::Cell::new(false) }; } +/// Tests: run `f` with this thread's passes split `threads` ways (`None`: as +/// the machine would), so callers' tests can take the split paths at small sizes. +#[cfg(test)] +pub(crate) fn with_forced_threads(threads: Option, f: impl FnOnce() -> R) -> R { + let before = FORCE_THREADS.with(|t| t.replace(threads)); + let r = f(); + FORCE_THREADS.with(|t| t.set(before)); + r +} + /// Run `f` with this thread's state-vector kernels on this thread alone. For /// callers that run many small states in parallel; the result is the same /// bits either way.