Some checks failed
wai-quantum on every processor / emulated · armv7-unknown-linux-gnueabihf (pull_request) Successful in 1m35s
wai-quantum on every processor / emulated · aarch64-unknown-linux-gnu (pull_request) Successful in 2m56s
wai-quantum on every processor / emulated · loongarch64-unknown-linux-gnu (pull_request) Successful in 3m12s
wai-quantum on every processor / emulated · powerpc64-unknown-linux-gnu (pull_request) Successful in 4m43s
wai-quantum on every processor / emulated · powerpc64le-unknown-linux-gnu (pull_request) Successful in 5m13s
wai-quantum on every processor / emulated · riscv64gc-unknown-linux-gnu (pull_request) Successful in 3m28s
wai-quantum WebGPU member / member · DX12 and Vulkan (cbio-windows) (pull_request) Successful in 12m9s
wai-quantum on every processor / emulated · s390x-unknown-linux-gnu (pull_request) Successful in 3m26s
wai-quantum on every processor / emulated · sparc64-unknown-linux-gnu (pull_request) Successful in 5m24s
wai-web reach builds / wai-web reach · does this change touch it (pull_request) Successful in 4s
wai-quantum on every processor / emulated · i686-unknown-linux-gnu (pull_request) Successful in 3m33s
wai-web reach builds / wai-web reach builds · the package of record, every output as recorded (pull_request) Successful in 3m5s
wai-web reach builds / wai-web reach builds · this tree's wai-web, every rung on its engine line (pull_request) Successful in 4m31s
repository checks / lockfiles, tests in CI, workflow hygiene, spec and section references, public text (pull_request) Has been cancelled
standards workspaces build + test / which workspaces this change touches (pull_request) Has been cancelled
standards workspaces build + test / macOS energy backends (pull_request) Has been cancelled
WAI conformance / does this change touch it (pull_request) Has been cancelled
WAI conformance / cargo-class corpora · linux (pull_request) Has been cancelled
WAI conformance / cargo-class corpora · linux-arm64 (pull_request) Has been cancelled
WAI conformance / cargo-class corpora · macos (pull_request) Has been cancelled
WAI conformance / quantum core and corpus under wasm32-wasip2 (wasmtime) (pull_request) Has been cancelled
WAI conformance / v2 vocabulary · manifests validate as stated (pull_request) Has been cancelled
WAI conformance / independent verifiers · claims, JWP, key discovery and integer payloads (Python) (pull_request) Has been cancelled
WAI conformance / delivery telemetry · CMCD and CMSD against an independent library (pull_request) Has been cancelled
WAI conformance / cross-arch byte-equality · linux (pull_request) Has been cancelled
WAI conformance / cross-arch byte-equality · linux-arm64 (pull_request) Has been cancelled
WAI conformance / cross-arch byte-equality · macos (pull_request) Has been cancelled
WAI conformance / codec wrappers · fuzz smoke, text decode limits, C entry points, provenance, the semantic path (pull_request) Has been cancelled
WAI conformance / C2PA manifest emission · signed, read back, validated (pull_request) Has been cancelled
WAI energy receipts / does this change touch it (pull_request) Has been cancelled
WAI energy receipts / energy receipts · linux (pull_request) Has been cancelled
WAI energy receipts / energy receipts · linux-arm64 (pull_request) Has been cancelled
WAI energy receipts / energy receipts · macos (pull_request) Has been cancelled
WAI JPEG-AI whole decode (pinned inputs, required) / does this change touch it (pull_request) Has been cancelled
WAI JPEG-AI whole decode (pinned inputs, required) / fetch pinned JPEG-AI inputs → whole decode + pixel hash, required (pull_request) Has been cancelled
WAI neural sink gates / does this change touch it (pull_request) Has been cancelled
WAI neural sink gates / model-free refusals of the neural sink (pull_request) Has been cancelled
wai-pack / wai-pack · does this change touch it (pull_request) Has been cancelled
wai-pack / wai-pack · linux (pull_request) Has been cancelled
wai-pack / wai-pack · linux-arm64 (pull_request) Has been cancelled
wai-pack / wai-pack · macos (pull_request) Has been cancelled
wai-pack / wai-pack · independent verifier (Python) (pull_request) Has been cancelled
wai-pack / wai-pack · the floor through a media prober (pull_request) Has been cancelled
wai-pack / wai-pack · dependency licences (pull_request) Has been cancelled
wai-quantum CUDA member / does this change touch it (pull_request) Has been cancelled
wai-quantum CUDA member / member · CUDA (jetson-hub) (pull_request) Has been cancelled
wai-quantum CUDA member / member · CUDA absent (linux) (pull_request) Has been cancelled
wai-quantum dataflow member / does this change touch it (pull_request) Has been cancelled
wai-quantum dataflow member / member · dataflow device, simulated (pci 1e52:b140) (pull_request) Has been cancelled
wai-quantum dataflow member / member · dataflow device, simulated (pci 1e52:401e) (pull_request) Has been cancelled
wai-quantum dataflow member / member · dataflow device absent (macos) (pull_request) Has been cancelled
wai-quantum signal processor / does this change touch it (pull_request) Has been cancelled
wai-quantum signal processor / processor and member · signal processor, emulated (pull_request) Has been cancelled
wai-quantum FPGA member / does this change touch it (pull_request) Has been cancelled
wai-quantum FPGA member / member · FPGA datapath, simulated (macos) (pull_request) Has been cancelled
wai-quantum FPGA member / member · FPGA simulation absent (linux) (pull_request) Has been cancelled
wai-quantum HIP member / does this change touch it (pull_request) Has been cancelled
wai-quantum HIP member / member · HIP (framework-128gb) (pull_request) Has been cancelled
wai-quantum HIP member / member · HIP absent (linux-arm64) (pull_request) Has been cancelled
wai-quantum Metal member / does this change touch it (pull_request) Has been cancelled
wai-quantum Metal member / member · Metal (macos) (pull_request) Has been cancelled
wai-quantum Metal member / member · Metal absent (linux) (pull_request) Has been cancelled
wai-quantum npu-cores member / does this change touch it (pull_request) Has been cancelled
wai-quantum npu-cores member / member · npu-cores, neural processor (framework-128gb silicon) (pull_request) Has been cancelled
wai-quantum npu-cores member / member · npu-cores absent (linux-arm64) (pull_request) Has been cancelled
wai-quantum neural-processor member / does this change touch it (pull_request) Has been cancelled
wai-quantum neural-processor member / member · neural processor (macos) (pull_request) Has been cancelled
wai-quantum neural-processor member / member · neural processor absent (linux) (pull_request) Has been cancelled
wai-quantum OpenCL member / does this change touch it (pull_request) Has been cancelled
wai-quantum OpenCL member / member · OpenCL GPU and CPU (jetson-hub) (pull_request) Has been cancelled
wai-quantum OpenCL member / member · OpenCL GPU (framework-128gb) (pull_request) Has been cancelled
wai-quantum OpenCL member / member · OpenCL GPU (macos) (pull_request) Has been cancelled
wai-quantum OpenCL member / member · OpenCL absent (linux) (pull_request) Has been cancelled
wai-quantum-qpu / does this change touch it (pull_request) Has been cancelled
wai-quantum-qpu / clients against their contracts (linux) (pull_request) Has been cancelled
wai-quantum service / does this change touch it (pull_request) Has been cancelled
wai-quantum service / service · linux (pull_request) Has been cancelled
wai-quantum service / service · macos (pull_request) Has been cancelled
wai-quantum matrix-extension member / does this change touch it (pull_request) Has been cancelled
wai-quantum matrix-extension member / member · matrix extension, emulated (aarch64 Linux) (pull_request) Has been cancelled
wai-quantum matrix-extension member / member · matrix extension absent (macos) (pull_request) Has been cancelled
wai-quantum tensor-engine member / does this change touch it (pull_request) Has been cancelled
wai-quantum tensor-engine member / member · tensor engine, the kernel toolchain's simulator (linux) (pull_request) Has been cancelled
wai-quantum tensor-engine member / member · tensor engine absent (linux-arm64) (pull_request) Has been cancelled
wai-quantum portable tensor program / does this change touch it (pull_request) Has been cancelled
wai-quantum portable tensor program / tensor program · CPU, mutants, GPU builds (pull_request) Has been cancelled
wai-quantum portable tensor program / tensor program · GPUs (jetson-hub) (pull_request) Has been cancelled
wai-quantum portable tensor program / tensor program · integrated GPU (framework-128gb) (pull_request) Has been cancelled
wai-quantum portable tensor program / tensor program · Metal and the arm64 CPU (macos) (pull_request) Has been cancelled
wai-quantum CPU vector units / does this change touch it (pull_request) Has been cancelled
wai-quantum CPU vector units / member · vector unit, rvv 1024-bit (riscv64, emulated) (pull_request) Has been cancelled
wai-quantum CPU vector units / member · vector unit, sve 128-bit (aarch64, emulated) (pull_request) Has been cancelled
wai-quantum CPU vector units / member · vector unit, rvv 128-bit (riscv64, emulated) (pull_request) Has been cancelled
wai-quantum CPU vector units / member · vector unit, sve 2048-bit (aarch64, emulated) (pull_request) Has been cancelled
wai-quantum CPU vector units / member · vector unit, avx2 (x86_64 silicon) (pull_request) Has been cancelled
wai-quantum CPU vector units / member · vector unit, sve 512-bit (aarch64, emulated) (pull_request) Has been cancelled
wai-quantum CPU vector units / member · vector units, avx2 and avx512 (framework-128gb silicon) (pull_request) Has been cancelled
wai-quantum CPU vector units / member · vector unit absent (macos) (pull_request) Has been cancelled
wai-quantum WebGPU member / does this change touch it (pull_request) Has been cancelled
wai-quantum WebGPU member / shader checks · browser build · wasip2 inertness · MSRV (pull_request) Has been cancelled
wai-quantum WebGPU member / member · software Vulkan (linux) (pull_request) Has been cancelled
wai-quantum WebGPU member / member · Metal (macos) (pull_request) Has been cancelled
wai-quantum WebGPU member / member · discrete GPU, Vulkan (jetson-hub) (pull_request) Has been cancelled
wai-quantum WebGPU member / member · integrated GPU, Vulkan (framework-128gb) (pull_request) Has been cancelled
wai-quantum WebGPU member / member · browser WebGPU (macos) (pull_request) Has been cancelled
WAI mbt2018_mean integer decode (regenerated vectors) / does this change touch it (pull_request) Has been cancelled
WAI mbt2018_mean integer decode (regenerated vectors) / regenerate mbt2018_mean integer vectors → Rust decode, required (pull_request) Has been cancelled
wai-web wasm32 / does this change touch it (pull_request) Has been cancelled
wai-web wasm32 / wai-web wasm32 build · corpus, menu and writers (pull_request) Has been cancelled
quantum_tableau (new, in full): Clifford circuits on up to 32768 qubits, the Aaronson-Gottesman tableau packed qubit by qubit with u32 indices. Tableau::support gives the affine subspace the computational-basis outcomes are uniform over (Gaussian elimination with signs tracked); a Sampler draws each shot as a random combination of its basis on the measured qubits. Tableau::expectation gives the exact +-1/0 of a Pauli string from the destabilizers. At 10,000 qubits: 100k gates in 49 ms, the subspace in 353 ms, 1000 full-width shots in 154 ms. Against the dense state vector on 40 random circuits: the outcome set, its uniform probabilities and 800 expectation values agree. The service lowers Clifford circuits to it (quarter-turn rotations, half-turn controlled phases), answers counts and expectations up to 32768 qubits, and caps a counts map's size. Live: a 2000-qubit GHZ with a phase flip reads <X^2000> = -1 in 260 ms. 25 service tests. Co-Authored-By: Claude Opus 5.5 <[email protected]>
751 lines
27 KiB
Rust
751 lines
27 KiB
Rust
//! Wide stabilizer simulation — `wai.quantum.tableau`.
|
||
//!
|
||
//! A Clifford circuit on `n` qubits, held as the Aaronson–Gottesman tableau
|
||
//! (`n` destabilizers and `n` stabilizers, each a signed Pauli string), packed
|
||
//! qubit by qubit: the X bits of one qubit across all `2n` rows are one run of
|
||
//! words, and so are its Z bits. A gate then touches one or two such runs, a
|
||
//! few word operations per 64 rows, so a circuit costs `O(gates · n / 64)`.
|
||
//!
|
||
//! Two readouts, both exact:
|
||
//!
|
||
//! - **Outcomes** ([`Tableau::support`]). The computational-basis outcomes of a
|
||
//! stabilizer state are uniform over an affine subspace `x₀ + span(v₁…v_k)`
|
||
//! of `GF(2)^n`. Gaussian elimination on the stabilizers (with their signs
|
||
//! tracked through every product) leaves `k` generators with independent X
|
||
//! parts and `n − k` that are `±Z`-strings; each of the latter is one linear
|
||
//! constraint `z·x = r` on the outcomes, and the constraints' solutions are
|
||
//! the subspace. It is found once, in `O(n³/64)`; a shot is then `x₀` plus a
|
||
//! random combination of the `v`, `O(k · m / 64)` for `m` measured qubits
|
||
//! ([`Sampler`]).
|
||
//! - **Expectation values** ([`Tableau::expectation`]). For a Pauli string `P`,
|
||
//! `⟨P⟩` is `0` if `P` anticommutes with a stabilizer; otherwise `P` is, up to
|
||
//! sign, the product of the stabilizers whose destabilizers anticommute with
|
||
//! it, and the sign of that product is `⟨P⟩`. `O(n²/64)`.
|
||
//!
|
||
//! Signs follow Aaronson and Gottesman's `g` (the power of `i` when two Pauli
|
||
//! letters multiply), evaluated a word at a time.
|
||
|
||
/// The most qubits a tableau holds: the tableau is `4n²` bits, so 32768
|
||
/// qubits take 512 MiB, and the outcome subspace about as much again.
|
||
pub const MAX_QUBITS: u32 = 1 << 15;
|
||
|
||
/// One Clifford gate.
|
||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||
pub enum Clifford {
|
||
H(u32),
|
||
S(u32),
|
||
Sdg(u32),
|
||
X(u32),
|
||
Y(u32),
|
||
Z(u32),
|
||
/// √X.
|
||
SX(u32),
|
||
/// √X†.
|
||
SXdg(u32),
|
||
/// `CX(control, target)`.
|
||
CX(u32, u32),
|
||
/// `CY(control, target)`.
|
||
CY(u32, u32),
|
||
CZ(u32, u32),
|
||
Swap(u32, u32),
|
||
}
|
||
|
||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||
pub enum TableauError {
|
||
TooManyQubits(u32),
|
||
QubitOutOfRange { qubit: u32, n: u32 },
|
||
/// A two-qubit gate or a Pauli string naming one qubit twice.
|
||
RepeatedQubit(u32),
|
||
/// A Pauli factor that is not `X`, `Y`, `Z` or `I`.
|
||
BadPauli(char),
|
||
}
|
||
|
||
impl std::fmt::Display for TableauError {
|
||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||
match self {
|
||
TableauError::TooManyQubits(n) => write!(f, "{n} qubits; a tableau holds at most {MAX_QUBITS}"),
|
||
TableauError::QubitOutOfRange { qubit, n } => write!(f, "qubit {qubit} of a {n}-qubit tableau"),
|
||
TableauError::RepeatedQubit(q) => write!(f, "qubit {q} named twice"),
|
||
TableauError::BadPauli(c) => write!(f, "{c:?} is not a Pauli factor"),
|
||
}
|
||
}
|
||
}
|
||
|
||
impl std::error::Error for TableauError {}
|
||
|
||
/// The tableau of a stabilizer state: rows `0..n` the destabilizers, rows
|
||
/// `n..2n` the stabilizers, packed qubit by qubit.
|
||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||
pub struct Tableau {
|
||
n: usize,
|
||
/// Words per qubit's run of `2n` row bits.
|
||
rw: usize,
|
||
xs: Vec<u64>,
|
||
zs: Vec<u64>,
|
||
/// The rows' signs, `2n` bits.
|
||
rs: Vec<u64>,
|
||
}
|
||
|
||
fn words(bits: usize) -> usize {
|
||
bits.div_ceil(64)
|
||
}
|
||
|
||
fn get(v: &[u64], i: usize) -> bool {
|
||
v[i / 64] >> (i % 64) & 1 == 1
|
||
}
|
||
|
||
fn set(v: &mut [u64], i: usize) {
|
||
v[i / 64] |= 1 << (i % 64);
|
||
}
|
||
|
||
fn splitmix(s: &mut u64) -> u64 {
|
||
*s = s.wrapping_add(0x9E37_79B9_7F4A_7C15);
|
||
let mut z = *s;
|
||
z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
|
||
z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
|
||
z ^ (z >> 31)
|
||
}
|
||
|
||
/// Rows held one after another, `wn` words of X then of Z each, with signs.
|
||
struct Rows {
|
||
wn: usize,
|
||
x: Vec<u64>,
|
||
z: Vec<u64>,
|
||
r: Vec<bool>,
|
||
}
|
||
|
||
impl Rows {
|
||
fn new(count: usize, n: usize) -> Rows {
|
||
let wn = words(n);
|
||
Rows { wn, x: vec![0; count * wn], z: vec![0; count * wn], r: vec![false; count] }
|
||
}
|
||
|
||
fn xr(&self, i: usize) -> &[u64] {
|
||
&self.x[i * self.wn..(i + 1) * self.wn]
|
||
}
|
||
|
||
fn zr(&self, i: usize) -> &[u64] {
|
||
&self.z[i * self.wn..(i + 1) * self.wn]
|
||
}
|
||
|
||
fn swap(&mut self, a: usize, b: usize) {
|
||
if a == b {
|
||
return;
|
||
}
|
||
for w in 0..self.wn {
|
||
self.x.swap(a * self.wn + w, b * self.wn + w);
|
||
self.z.swap(a * self.wn + w, b * self.wn + w);
|
||
}
|
||
self.r.swap(a, b);
|
||
}
|
||
|
||
/// Row `h` ← row `i` · row `h`, sign included (the rows commute).
|
||
fn mul_into(&mut self, h: usize, i: usize) {
|
||
let (wn, mut acc) = (self.wn, 0i64);
|
||
for w in 0..wn {
|
||
let (x1, z1) = (self.x[i * wn + w], self.z[i * wn + w]);
|
||
let (x2, z2) = (self.x[h * wn + w], self.z[h * wn + w]);
|
||
acc += phase_word(x1, z1, x2, z2);
|
||
self.x[h * wn + w] = x1 ^ x2;
|
||
self.z[h * wn + w] = z1 ^ z2;
|
||
}
|
||
let total = (2 * self.r[h] as i64 + 2 * self.r[i] as i64 + acc).rem_euclid(4);
|
||
self.r[h] = total == 2;
|
||
}
|
||
}
|
||
|
||
/// `Σ g(x1, z1, x2, z2)` over one word's 64 qubits: the power of `i` the
|
||
/// product of the two Paulis' letters carries.
|
||
fn phase_word(x1: u64, z1: u64, x2: u64, z2: u64) -> i64 {
|
||
let y1 = x1 & z1;
|
||
let xo = x1 & !z1;
|
||
let zo = !x1 & z1;
|
||
let plus = (y1 & z2 & !x2) | (xo & z2 & x2) | (zo & x2 & !z2);
|
||
let minus = (y1 & x2 & !z2) | (xo & z2 & !x2) | (zo & x2 & z2);
|
||
plus.count_ones() as i64 - minus.count_ones() as i64
|
||
}
|
||
|
||
impl Tableau {
|
||
/// `|0…0⟩` on `n` qubits.
|
||
pub fn new(n: u32) -> Result<Tableau, TableauError> {
|
||
if n > MAX_QUBITS {
|
||
return Err(TableauError::TooManyQubits(n));
|
||
}
|
||
let n = n as usize;
|
||
let rw = words(2 * n);
|
||
let mut t = Tableau { n, rw, xs: vec![0; n * rw], zs: vec![0; n * rw], rs: vec![0; rw] };
|
||
for q in 0..n {
|
||
// Destabilizer q is X_q; stabilizer q is Z_q.
|
||
set(&mut t.xs[q * rw..(q + 1) * rw], q);
|
||
set(&mut t.zs[q * rw..(q + 1) * rw], n + q);
|
||
}
|
||
Ok(t)
|
||
}
|
||
|
||
/// The circuit `ops` on `n` qubits from `|0…0⟩`.
|
||
pub fn run(n: u32, ops: &[Clifford]) -> Result<Tableau, TableauError> {
|
||
let mut t = Tableau::new(n)?;
|
||
for &op in ops {
|
||
t.apply(op)?;
|
||
}
|
||
Ok(t)
|
||
}
|
||
|
||
pub fn qubits(&self) -> u32 {
|
||
self.n as u32
|
||
}
|
||
|
||
fn check(&self, q: u32) -> Result<usize, TableauError> {
|
||
if (q as usize) < self.n { Ok(q as usize) } else { Err(TableauError::QubitOutOfRange { qubit: q, n: self.n as u32 }) }
|
||
}
|
||
|
||
fn pair(&self, a: u32, b: u32) -> Result<(usize, usize), TableauError> {
|
||
let (a, b) = (self.check(a)?, self.check(b)?);
|
||
if a == b {
|
||
return Err(TableauError::RepeatedQubit(a as u32));
|
||
}
|
||
Ok((a, b))
|
||
}
|
||
|
||
fn h(&mut self, a: usize) {
|
||
let rw = self.rw;
|
||
for w in 0..rw {
|
||
let (x, z) = (self.xs[a * rw + w], self.zs[a * rw + w]);
|
||
self.rs[w] ^= x & z;
|
||
self.xs[a * rw + w] = z;
|
||
self.zs[a * rw + w] = x;
|
||
}
|
||
}
|
||
|
||
fn s(&mut self, a: usize) {
|
||
let rw = self.rw;
|
||
for w in 0..rw {
|
||
let (x, z) = (self.xs[a * rw + w], self.zs[a * rw + w]);
|
||
self.rs[w] ^= x & z;
|
||
self.zs[a * rw + w] = z ^ x;
|
||
}
|
||
}
|
||
|
||
fn sdg(&mut self, a: usize) {
|
||
let rw = self.rw;
|
||
for w in 0..rw {
|
||
let (x, z) = (self.xs[a * rw + w], self.zs[a * rw + w]);
|
||
self.rs[w] ^= x & !z;
|
||
self.zs[a * rw + w] = z ^ x;
|
||
}
|
||
}
|
||
|
||
fn cx(&mut self, a: usize, b: usize) {
|
||
let rw = self.rw;
|
||
for w in 0..rw {
|
||
let (xa, za, xb, zb) = (self.xs[a * rw + w], self.zs[a * rw + w], self.xs[b * rw + w], self.zs[b * rw + w]);
|
||
self.rs[w] ^= xa & zb & !(xb ^ za);
|
||
self.xs[b * rw + w] = xb ^ xa;
|
||
self.zs[a * rw + w] = za ^ zb;
|
||
}
|
||
}
|
||
|
||
/// Applies one gate.
|
||
pub fn apply(&mut self, op: Clifford) -> Result<(), TableauError> {
|
||
let rw = self.rw;
|
||
match op {
|
||
Clifford::H(a) => {
|
||
let a = self.check(a)?;
|
||
self.h(a);
|
||
}
|
||
Clifford::S(a) => {
|
||
let a = self.check(a)?;
|
||
self.s(a);
|
||
}
|
||
Clifford::Sdg(a) => {
|
||
let a = self.check(a)?;
|
||
self.sdg(a);
|
||
}
|
||
Clifford::X(a) => {
|
||
let a = self.check(a)?;
|
||
for w in 0..rw {
|
||
self.rs[w] ^= self.zs[a * rw + w];
|
||
}
|
||
}
|
||
Clifford::Z(a) => {
|
||
let a = self.check(a)?;
|
||
for w in 0..rw {
|
||
self.rs[w] ^= self.xs[a * rw + w];
|
||
}
|
||
}
|
||
Clifford::Y(a) => {
|
||
let a = self.check(a)?;
|
||
for w in 0..rw {
|
||
self.rs[w] ^= self.xs[a * rw + w] ^ self.zs[a * rw + w];
|
||
}
|
||
}
|
||
Clifford::SX(a) => {
|
||
let a = self.check(a)?;
|
||
self.h(a);
|
||
self.s(a);
|
||
self.h(a);
|
||
}
|
||
Clifford::SXdg(a) => {
|
||
let a = self.check(a)?;
|
||
self.h(a);
|
||
self.sdg(a);
|
||
self.h(a);
|
||
}
|
||
Clifford::CX(a, b) => {
|
||
let (a, b) = self.pair(a, b)?;
|
||
self.cx(a, b);
|
||
}
|
||
Clifford::CZ(a, b) => {
|
||
let (a, b) = self.pair(a, b)?;
|
||
self.h(b);
|
||
self.cx(a, b);
|
||
self.h(b);
|
||
}
|
||
Clifford::CY(a, b) => {
|
||
let (a, b) = self.pair(a, b)?;
|
||
self.sdg(b);
|
||
self.cx(a, b);
|
||
self.s(b);
|
||
}
|
||
Clifford::Swap(a, b) => {
|
||
let (a, b) = self.pair(a, b)?;
|
||
for w in 0..rw {
|
||
self.xs.swap(a * rw + w, b * rw + w);
|
||
self.zs.swap(a * rw + w, b * rw + w);
|
||
}
|
||
}
|
||
}
|
||
Ok(())
|
||
}
|
||
|
||
/// Rows `from..to` of the tableau, held row by row.
|
||
fn rows(&self, from: usize, to: usize) -> Rows {
|
||
let (n, rw) = (self.n, self.rw);
|
||
let mut out = Rows::new(to - from, n);
|
||
let wn = out.wn;
|
||
for q in 0..n {
|
||
for (bits, dst) in [(&self.xs, &mut out.x), (&self.zs, &mut out.z)] {
|
||
let col = &bits[q * rw..(q + 1) * rw];
|
||
for (w, &bits_w) in col.iter().enumerate().take(to.div_ceil(64)).skip(from / 64) {
|
||
let mut word = bits_w;
|
||
while word != 0 {
|
||
let row = w * 64 + word.trailing_zeros() as usize;
|
||
word &= word - 1;
|
||
if (from..to).contains(&row) {
|
||
dst[(row - from) * wn + q / 64] |= 1 << (q % 64);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
for row in from..to {
|
||
out.r[row - from] = get(&self.rs, row);
|
||
}
|
||
out
|
||
}
|
||
|
||
/// `⟨ψ|P|ψ⟩` for the Pauli string `P` (`(qubit, 'X'|'Y'|'Z'|'I')`):
|
||
/// `1`, `−1`, or `0`.
|
||
pub fn expectation(&self, pauli: &[(u32, char)]) -> Result<i8, TableauError> {
|
||
let n = self.n;
|
||
let wn = words(n);
|
||
let (mut px, mut pz) = (vec![0u64; wn], vec![0u64; wn]);
|
||
let mut seen = vec![false; n];
|
||
for &(q, c) in pauli {
|
||
let q = self.check(q)?;
|
||
if seen[q] {
|
||
return Err(TableauError::RepeatedQubit(q as u32));
|
||
}
|
||
seen[q] = true;
|
||
match c.to_ascii_uppercase() {
|
||
'X' => set(&mut px, q),
|
||
'Z' => set(&mut pz, q),
|
||
'Y' => {
|
||
set(&mut px, q);
|
||
set(&mut pz, q);
|
||
}
|
||
'I' => {}
|
||
other => return Err(TableauError::BadPauli(other)),
|
||
}
|
||
}
|
||
let rows = self.rows(0, 2 * n);
|
||
let anticommutes = |i: usize| -> bool {
|
||
let (rx, rz) = (rows.xr(i), rows.zr(i));
|
||
let mut c = 0u32;
|
||
for w in 0..wn {
|
||
c += ((px[w] & rz[w]) ^ (pz[w] & rx[w])).count_ones();
|
||
}
|
||
c % 2 == 1
|
||
};
|
||
if (n..2 * n).any(anticommutes) {
|
||
return Ok(0);
|
||
}
|
||
// P is ± the product of the stabilizers whose destabilizers it
|
||
// anticommutes with.
|
||
let mut q = Rows::new(1, n);
|
||
let mut scratch = Rows::new(2, n);
|
||
for i in 0..n {
|
||
if anticommutes(i) {
|
||
scratch.x[..wn].copy_from_slice(&q.x[..wn]);
|
||
scratch.z[..wn].copy_from_slice(&q.z[..wn]);
|
||
scratch.r[0] = q.r[0];
|
||
scratch.x[wn..].copy_from_slice(rows.xr(n + i));
|
||
scratch.z[wn..].copy_from_slice(rows.zr(n + i));
|
||
scratch.r[1] = rows.r[n + i];
|
||
scratch.mul_into(0, 1);
|
||
q.x.copy_from_slice(&scratch.x[..wn]);
|
||
q.z.copy_from_slice(&scratch.z[..wn]);
|
||
q.r[0] = scratch.r[0];
|
||
}
|
||
}
|
||
debug_assert!(q.x == px && q.z == pz, "the product of stabilizers is P up to sign");
|
||
Ok(if q.r[0] { -1 } else { 1 })
|
||
}
|
||
|
||
/// The computational-basis outcomes, as the affine subspace they are
|
||
/// uniform over.
|
||
pub fn support(&self) -> Support {
|
||
let n = self.n;
|
||
let mut rows = self.rows(n, 2 * n);
|
||
let wn = rows.wn;
|
||
// Eliminate the X parts: pivot rows first, ±Z-strings after.
|
||
let mut k = 0;
|
||
for q in 0..n {
|
||
let Some(p) = (k..n).find(|&i| get(rows.xr(i), q)) else { continue };
|
||
rows.swap(k, p);
|
||
for i in 0..n {
|
||
if i != k && get(rows.xr(i), q) {
|
||
rows.mul_into(i, k);
|
||
}
|
||
}
|
||
k += 1;
|
||
}
|
||
// Each ±Z-string is a constraint z·x = r; reduce them to echelon form.
|
||
let mut eqs: Vec<(Vec<u64>, bool)> = (k..n).map(|i| (rows.zr(i).to_vec(), rows.r[i])).collect();
|
||
let mut pivots: Vec<usize> = Vec::new();
|
||
let mut rank = 0;
|
||
for q in 0..n {
|
||
let Some(p) = (rank..eqs.len()).find(|&i| get(&eqs[i].0, q)) else { continue };
|
||
eqs.swap(rank, p);
|
||
let (pz, pr) = eqs[rank].clone();
|
||
for (i, e) in eqs.iter_mut().enumerate() {
|
||
if i != rank && get(&e.0, q) {
|
||
for (a, b) in e.0.iter_mut().zip(&pz) {
|
||
*a ^= b;
|
||
}
|
||
e.1 ^= pr;
|
||
}
|
||
}
|
||
pivots.push(q);
|
||
rank += 1;
|
||
}
|
||
let mut is_pivot = vec![false; n];
|
||
for &c in &pivots {
|
||
is_pivot[c] = true;
|
||
}
|
||
// x₀: the free variables 0, each pivot variable its equation's sign.
|
||
let mut x0 = vec![0u64; wn];
|
||
for (i, &c) in pivots.iter().enumerate() {
|
||
if eqs[i].1 {
|
||
set(&mut x0, c);
|
||
}
|
||
}
|
||
// One basis vector per free variable.
|
||
let mut basis = Vec::with_capacity(n - rank);
|
||
for f in (0..n).filter(|&c| !is_pivot[c]) {
|
||
let mut v = vec![0u64; wn];
|
||
set(&mut v, f);
|
||
for (i, &c) in pivots.iter().enumerate() {
|
||
if get(&eqs[i].0, f) {
|
||
set(&mut v, c);
|
||
}
|
||
}
|
||
basis.push(v);
|
||
}
|
||
eqs.truncate(rank);
|
||
Support { n, x0, basis, constraints: eqs }
|
||
}
|
||
}
|
||
|
||
/// The computational-basis outcomes of a stabilizer state: uniform over
|
||
/// `x₀ + span(basis)`. Bit `q` of an outcome is qubit `q`.
|
||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||
pub struct Support {
|
||
n: usize,
|
||
x0: Vec<u64>,
|
||
basis: Vec<Vec<u64>>,
|
||
/// The constraints `z·x = r` the outcomes satisfy, in echelon form.
|
||
constraints: Vec<(Vec<u64>, bool)>,
|
||
}
|
||
|
||
impl Support {
|
||
/// `k`: the outcomes are `2^k` equally likely bit strings.
|
||
pub fn dimension(&self) -> usize {
|
||
self.basis.len()
|
||
}
|
||
|
||
/// Whether `bits` (bit `q` = qubit `q`, `n` bits) is an outcome: every
|
||
/// constraint `z·x = r` holds.
|
||
pub fn contains(&self, bits: &[u64]) -> bool {
|
||
self.constraints.iter().all(|(z, r)| {
|
||
let parity = z.iter().zip(bits.iter().chain(std::iter::repeat(&0))).map(|(a, b)| (a & b).count_ones()).sum::<u32>() % 2 == 1;
|
||
parity == *r
|
||
})
|
||
}
|
||
|
||
/// A sampler for the qubits `measured`, in that order.
|
||
pub fn sampler(&self, measured: &[u32]) -> Sampler {
|
||
let m = measured.len();
|
||
let wm = words(m);
|
||
let project = |v: &[u64]| -> Vec<u64> {
|
||
let mut out = vec![0u64; wm];
|
||
for (j, &q) in measured.iter().enumerate() {
|
||
if (q as usize) < self.n && get(v, q as usize) {
|
||
set(&mut out, j);
|
||
}
|
||
}
|
||
out
|
||
};
|
||
let x0 = project(&self.x0);
|
||
let basis: Vec<Vec<u64>> = self.basis.iter().map(|v| project(v)).filter(|v| v.iter().any(|&w| w != 0)).collect();
|
||
Sampler { m, x0, basis }
|
||
}
|
||
}
|
||
|
||
/// Shots of the measured qubits: `x₀` plus a uniformly random combination of
|
||
/// the basis, projected onto them.
|
||
#[derive(Clone, Debug)]
|
||
pub struct Sampler {
|
||
m: usize,
|
||
x0: Vec<u64>,
|
||
basis: Vec<Vec<u64>>,
|
||
}
|
||
|
||
impl Sampler {
|
||
/// The measured qubits' count.
|
||
pub fn width(&self) -> usize {
|
||
self.m
|
||
}
|
||
|
||
/// The independent directions the measured outcomes vary in (at most the
|
||
/// support's dimension).
|
||
pub fn dimension(&self) -> usize {
|
||
self.basis.len()
|
||
}
|
||
|
||
/// One shot into `out` (bit `j` = the `j`-th measured qubit), drawing from
|
||
/// `rng`.
|
||
pub fn shot(&self, rng: &mut u64, out: &mut Vec<u64>) {
|
||
out.clear();
|
||
out.extend_from_slice(&self.x0);
|
||
let mut bits = 0u64;
|
||
for (j, v) in self.basis.iter().enumerate() {
|
||
if j % 64 == 0 {
|
||
bits = splitmix(rng);
|
||
}
|
||
if bits >> (j % 64) & 1 == 1 {
|
||
for (o, w) in out.iter_mut().zip(v) {
|
||
*o ^= w;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
/// `shots` shots from `seed`, counted: `(outcome, count)`, most frequent
|
||
/// first, then by outcome.
|
||
pub fn counts(&self, shots: u64, seed: u64) -> Vec<(Vec<u64>, u64)> {
|
||
let mut rng = seed ^ 0x7AB1_EA00_5EED_0001;
|
||
let mut map: std::collections::HashMap<Vec<u64>, u64> = std::collections::HashMap::new();
|
||
let mut buf = Vec::new();
|
||
for _ in 0..shots {
|
||
self.shot(&mut rng, &mut buf);
|
||
*map.entry(buf.clone()).or_insert(0) += 1;
|
||
}
|
||
let mut v: Vec<(Vec<u64>, u64)> = map.into_iter().collect();
|
||
v.sort_by(|a, b| b.1.cmp(&a.1).then(a.0.cmp(&b.0)));
|
||
v
|
||
}
|
||
}
|
||
|
||
#[cfg(test)]
|
||
mod tests {
|
||
use super::*;
|
||
|
||
fn next(s: &mut u64) -> u64 {
|
||
splitmix(s)
|
||
}
|
||
|
||
fn random_clifford(n: u32, ops: usize, seed: u64) -> Vec<Clifford> {
|
||
let mut s = seed;
|
||
(0..ops)
|
||
.map(|_| {
|
||
let a = (next(&mut s) % n as u64) as u32;
|
||
let mut b = (next(&mut s) % n as u64) as u32;
|
||
if b == a {
|
||
b = (a + 1) % n;
|
||
}
|
||
match next(&mut s) % 12 {
|
||
0 => Clifford::H(a),
|
||
1 => Clifford::S(a),
|
||
2 => Clifford::Sdg(a),
|
||
3 => Clifford::X(a),
|
||
4 => Clifford::Y(a),
|
||
5 => Clifford::Z(a),
|
||
6 => Clifford::SX(a),
|
||
7 => Clifford::SXdg(a),
|
||
8 => Clifford::CX(a, b),
|
||
9 => Clifford::CY(a, b),
|
||
10 => Clifford::CZ(a, b),
|
||
_ => Clifford::Swap(a, b),
|
||
}
|
||
})
|
||
.collect()
|
||
}
|
||
|
||
#[test]
|
||
fn the_zero_state_and_a_wide_ghz_state_read_exactly() {
|
||
let t = Tableau::new(5).unwrap();
|
||
assert_eq!(t.support().dimension(), 0);
|
||
assert_eq!(t.expectation(&[(3, 'Z')]).unwrap(), 1);
|
||
assert_eq!(t.expectation(&[(3, 'X')]).unwrap(), 0);
|
||
let n = 2000u32;
|
||
let mut ops = vec![Clifford::H(0)];
|
||
ops.extend((0..n - 1).map(|q| Clifford::CX(q, q + 1)));
|
||
let t = Tableau::run(n, &ops).unwrap();
|
||
let s = t.support();
|
||
assert_eq!(s.dimension(), 1);
|
||
assert_eq!(t.expectation(&[(0, 'Z'), (n - 1, 'Z')]).unwrap(), 1);
|
||
assert_eq!(t.expectation(&[(7, 'Z')]).unwrap(), 0);
|
||
let all_x: Vec<(u32, char)> = (0..n).map(|q| (q, 'X')).collect();
|
||
assert_eq!(t.expectation(&all_x).unwrap(), 1);
|
||
// Y on every qubit of an even GHZ: ⟨Y^{⊗n}⟩ = Re(i^n) = 1 for n ≡ 0 mod 4.
|
||
let all_y: Vec<(u32, char)> = (0..n).map(|q| (q, 'Y')).collect();
|
||
assert_eq!(t.expectation(&all_y).unwrap(), 1);
|
||
let measured: Vec<u32> = (0..n).collect();
|
||
let c = s.sampler(&measured).counts(1000, 3);
|
||
assert_eq!(c.len(), 2);
|
||
assert!(c.iter().all(|(o, k)| *k > 400 && (o.iter().all(|&w| w == 0) || o.iter().map(|w| w.count_ones()).sum::<u32>() == n)));
|
||
}
|
||
|
||
#[test]
|
||
fn a_gate_and_its_inverse_leave_the_tableau_as_it_was() {
|
||
let base = Tableau::run(6, &random_clifford(6, 40, 9)).unwrap();
|
||
for a in [
|
||
vec![Clifford::H(2), Clifford::H(2)],
|
||
vec![Clifford::S(1), Clifford::Sdg(1)],
|
||
vec![Clifford::SX(4), Clifford::SXdg(4)],
|
||
vec![Clifford::CX(0, 3), Clifford::CX(0, 3)],
|
||
vec![Clifford::CY(5, 1), Clifford::CY(5, 1)],
|
||
vec![Clifford::CZ(2, 4), Clifford::CZ(4, 2)],
|
||
vec![Clifford::Swap(0, 5), Clifford::Swap(5, 0)],
|
||
vec![Clifford::S(3), Clifford::S(3), Clifford::S(3), Clifford::S(3)],
|
||
] {
|
||
let mut t = base.clone();
|
||
for op in a.clone() {
|
||
t.apply(op).unwrap();
|
||
}
|
||
assert_eq!(t, base, "{a:?}");
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn refusals_are_typed() {
|
||
assert_eq!(Tableau::new(MAX_QUBITS + 1).unwrap_err(), TableauError::TooManyQubits(MAX_QUBITS + 1));
|
||
let mut t = Tableau::new(3).unwrap();
|
||
assert_eq!(t.apply(Clifford::H(3)).unwrap_err(), TableauError::QubitOutOfRange { qubit: 3, n: 3 });
|
||
assert_eq!(t.apply(Clifford::CX(1, 1)).unwrap_err(), TableauError::RepeatedQubit(1));
|
||
assert_eq!(t.expectation(&[(0, 'Q')]).unwrap_err(), TableauError::BadPauli('Q'));
|
||
assert_eq!(t.expectation(&[(0, 'Z'), (0, 'X')]).unwrap_err(), TableauError::RepeatedQubit(0));
|
||
}
|
||
|
||
/// Times at scale; run with `--ignored --nocapture`.
|
||
#[test]
|
||
#[ignore]
|
||
fn timings_at_scale() {
|
||
for n in [1000u32, 2000, 5000, 10000] {
|
||
let ops = random_clifford(n, 10 * n as usize, n as u64);
|
||
let t0 = std::time::Instant::now();
|
||
let t = Tableau::run(n, &ops).unwrap();
|
||
let gates = t0.elapsed();
|
||
let t1 = std::time::Instant::now();
|
||
let s = t.support();
|
||
let support = t1.elapsed();
|
||
let measured: Vec<u32> = (0..n).collect();
|
||
let t2 = std::time::Instant::now();
|
||
let c = s.sampler(&measured).counts(1000, 1);
|
||
let shots = t2.elapsed();
|
||
let t3 = std::time::Instant::now();
|
||
let e = t.expectation(&[(0, 'Z'), (n / 2, 'X')]).unwrap();
|
||
let expect = t3.elapsed();
|
||
println!("n={n}: {} gates {gates:?}; support dim {} in {support:?}; 1000 shots ({} distinct) {shots:?}; one expectation ({e}) {expect:?}", ops.len(), s.dimension(), c.len());
|
||
}
|
||
}
|
||
|
||
#[cfg(feature = "quantum_sv")]
|
||
mod against_the_state_vector {
|
||
use super::*;
|
||
use crate::gates::{Gate, complex};
|
||
use crate::quantum_sv::StateVector;
|
||
|
||
fn matrix(op: Clifford) -> Gate {
|
||
let c = |re: f64, im: f64| complex(re, im);
|
||
let o = c(0.0, 0.0);
|
||
let one = c(1.0, 0.0);
|
||
let ctrl = |a: u32, b: u32, u: [crate::gates::C; 4]| Gate { qubits: vec![a, b], matrix: vec![one, o, o, o, o, one, o, o, o, o, u[0], u[1], o, o, u[2], u[3]] };
|
||
match op {
|
||
Clifford::H(a) => Gate::h(a),
|
||
Clifford::S(a) => Gate::s(a),
|
||
Clifford::Sdg(a) => Gate::sdg(a),
|
||
Clifford::X(a) => Gate::x(a),
|
||
Clifford::Y(a) => Gate::y(a),
|
||
Clifford::Z(a) => Gate::z(a),
|
||
Clifford::SX(a) => Gate { qubits: vec![a], matrix: vec![c(0.5, 0.5), c(0.5, -0.5), c(0.5, -0.5), c(0.5, 0.5)] },
|
||
Clifford::SXdg(a) => Gate { qubits: vec![a], matrix: vec![c(0.5, -0.5), c(0.5, 0.5), c(0.5, 0.5), c(0.5, -0.5)] },
|
||
Clifford::CX(a, b) => Gate::cx(a, b),
|
||
Clifford::CY(a, b) => ctrl(a, b, [o, c(0.0, -1.0), c(0.0, 1.0), o]),
|
||
Clifford::CZ(a, b) => Gate::cz(a, b),
|
||
Clifford::Swap(a, b) => Gate::swap(a, b),
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn outcomes_and_expectations_are_the_state_vectors_on_random_circuits() {
|
||
for seed in 0..40 {
|
||
let n = 2 + (seed % 9) as u32;
|
||
let ops = random_clifford(n, 60, seed);
|
||
let t = Tableau::run(n, &ops).unwrap();
|
||
let mut sv = StateVector::zero(n).unwrap();
|
||
sv.run(&ops.iter().map(|&o| matrix(o)).collect::<Vec<_>>(), 1).unwrap();
|
||
let s = t.support();
|
||
let p = 1.0 / (1u64 << s.dimension()) as f64;
|
||
let mut support_size = 0;
|
||
for x in 0..1usize << n {
|
||
let prob = sv.re[x] * sv.re[x] + sv.im[x] * sv.im[x];
|
||
// The state vector's index has qubit 0 as its most significant bit.
|
||
let mut bits = vec![0u64; 1];
|
||
for q in 0..n as usize {
|
||
if x >> (n as usize - 1 - q) & 1 == 1 {
|
||
bits[0] |= 1 << q;
|
||
}
|
||
}
|
||
let inside = s.contains(&bits);
|
||
if prob > 1e-9 {
|
||
support_size += 1;
|
||
assert!(inside, "seed {seed}: outcome {x:b} has probability {prob} but is outside the support");
|
||
assert!((prob - p).abs() < 1e-9, "seed {seed}: probability {prob}, expected {p}");
|
||
} else {
|
||
assert!(!inside, "seed {seed}: outcome {x:b} is in the support with probability {prob}");
|
||
}
|
||
}
|
||
assert_eq!(support_size, 1 << s.dimension(), "seed {seed}");
|
||
let mut r = seed ^ 0xabc;
|
||
for _ in 0..20 {
|
||
let string: Vec<(u32, char)> = (0..n).filter_map(|q| match next(&mut r) % 4 { 0 => None, 1 => Some((q, 'X')), 2 => Some((q, 'Y')), _ => Some((q, 'Z')) }).collect();
|
||
let want = sv.expectation(&[(1.0, string.clone())]);
|
||
let got = t.expectation(&string).unwrap() as f64;
|
||
assert!((got - want).abs() < 1e-9, "seed {seed}: {string:?}: {got} vs {want}");
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|