open-standards/wai/wai-quantum/src/quantum_spd.rs
Transaction Science e546a87a2f wai-quantum circuit2: the crate's consumers follow the circuit's gateset
QuantumReceipt gains gateset_of, which names the gateset under which a
circuit's ops hash to the receipt's circuit_hash, and gateset_note, which
turns that into the text to show when a reconstruction fails for that
reason. verify_reconstruction stays strict. from_json refuses an n_qubits
that does not fit a u8 and an n_ops that does not fit a u32 instead of
truncating them.

The fabric names its gate kernel and random corpus once per gateset:
wai.quantum.circuit2/apply and wai.quantum.circuit2/random-v1 join the
circuit/1 names. certify_lanes and lanes::random_circuit take a gateset and
draw the same ops from a seed in both. detect_local offers the reference
kernel of each gateset. The lanes32 corpus digests for seed 7 and 200 cases
are pinned: d18454c6 for circuit/1, as 0.3.41 certified it, and 3dbde276
for circuit2.

noisy_expect_z and route_line build their circuits with empty_like, so a
derived circuit keeps its source's gateset. from_qasm refuses a gate that
names one qubit twice as an invalid circuit when it reads it, in every
gateset. quantum_spd's controlled_phase refuses a phase on 32 or more
qubits instead of shifting out of range. The QIR and QASM export docs say
that a circuit/1 circuit exports ideal angles, not its own numerics. The
bell and receipt examples print the capability and handle the fallible
calls.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-09 11:19:49 -04:00

1408 lines
55 KiB
Rust
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//! Sparse Pauli dynamics at utility scale (`wai.quantum.spd`).
//!
//! Computes `⟨ψ₀| U† O U |ψ₀⟩` for circuits of up to 1024 qubits by evolving the
//! *observable* in the Heisenberg picture as a sum of Pauli strings,
//! `O = Σ c_P P`. A Clifford gate maps each string to one signed string. A Pauli
//! rotation `exp(−iθP/2)` leaves strings that commute with `P` alone and splits
//! the rest in two: `Q ↦ cos θ·Q + sin θ·(−iQP)`. Strings whose coefficient falls
//! below a threshold are dropped. A circuit therefore costs what its
//! non-Clifford rotations and the observable's light cone make it cost, not
//! `2ⁿ`. This is the method that computed the 127-qubit kicked-Ising
//! observables of the heavy-hex "utility" experiment on a laptop.
//!
//! **Arbitrary angles.** The dyadic circuit core is byte-exact because every
//! phase it allows is a power-of-two root of unity. This module takes any real
//! angle, so its contract is the one the crate's other `f64` layers keep:
//!
//! - **Reproducible.** Every result is bit-identical across runs, machines and
//! targets. Terms live in an insertion-ordered table, so the order in which
//! coefficients are added never depends on hashing or on the platform's word
//! size. `sin` and `cos` are computed in-crate from `+ − × ÷`, because the
//! platform's libm may differ in the last place.
//! - **Bounded.** Every dropped coefficient is added to
//! [`SpdResult::truncation_bound`]. A dropped term `δ·P` would have evolved
//! under the remaining gates into `δ` times a unitary conjugate of `P`, whose
//! expectation is at most `|δ|` in magnitude. So `|exact − value|` is at most
//! the bound, up to floating-point rounding. Clifford gates and exact
//! quarter-turn rotations drop nothing.
use std::fmt;
pub use crate::repro::sin_cos;
// ---------------------------------------------------------------------------
// Circuits
// ---------------------------------------------------------------------------
/// The largest register this module takes: 16 words of 64 qubits.
pub const MAX_QUBITS: u32 = 1024;
/// The largest angle magnitude a rotation may carry.
pub const MAX_ANGLE: f64 = 1_048_576.0;
/// A single-qubit Pauli factor.
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum Pauli {
X,
Y,
Z,
}
/// A Pauli string as `(qubit, factor)` pairs; qubits not named carry `I`.
pub type PauliString = Vec<(u32, Pauli)>;
/// One instruction.
#[derive(Clone, Debug, PartialEq)]
pub enum Op {
H(u32),
S(u32),
Sdg(u32),
/// √X.
SX(u32),
/// √X†.
SXdg(u32),
X(u32),
Y(u32),
Z(u32),
/// `CX(control, target)`.
CX(u32, u32),
CZ(u32, u32),
Swap(u32, u32),
/// `exp(−iθP/2)` for any Pauli string `P`: `RX`, `RY`, `RZ` for one factor,
/// `RZZ` and the like for two.
Rot { axis: PauliString, theta: f64 },
/// `exp(−i(kπ/2)P/2)` for `k ∈ {1, 2, 3}`: a Pauli rotation through a
/// multiple of π/2. These are Clifford, and applied exactly.
QuarterRot { axis: PauliString, k: u8 },
}
/// A circuit on `n` qubits.
#[derive(Clone, Debug, PartialEq)]
pub struct RotCircuit {
pub n: u32,
pub ops: Vec<Op>,
}
/// Why a circuit, observable or initial state was refused.
#[derive(Clone, Debug, PartialEq)]
pub enum SpdError {
TooManyQubits(u32),
QubitOutOfRange { qubit: u32, n: u32 },
/// A two-qubit gate or Pauli string naming one qubit twice.
RepeatedQubit(u32),
EmptyAxis,
BadAngle(f64),
BadQuarter(u8),
/// A dyadic gate with no Pauli-rotation form here (a controlled `H`).
Unsupported(String),
/// A receipt's meter returned without running the job.
NotRun,
}
impl fmt::Display for SpdError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
SpdError::TooManyQubits(n) => write!(f, "{n} qubits; at most {MAX_QUBITS}"),
SpdError::QubitOutOfRange { qubit, n } => write!(f, "qubit {qubit} of a {n}-qubit register"),
SpdError::RepeatedQubit(q) => write!(f, "qubit {q} named twice"),
SpdError::EmptyAxis => write!(f, "a rotation about the identity"),
SpdError::BadAngle(a) => write!(f, "angle {a} is not finite or exceeds {MAX_ANGLE}"),
SpdError::BadQuarter(k) => write!(f, "quarter turns {k}; expected 1, 2 or 3"),
SpdError::Unsupported(g) => write!(f, "unsupported gate: {g}"),
SpdError::NotRun => write!(f, "the meter did not run the job"),
}
}
}
impl std::error::Error for SpdError {}
impl RotCircuit {
pub fn new(n: u32) -> Self {
RotCircuit { n, ops: Vec::new() }
}
pub fn push(&mut self, op: Op) -> &mut Self {
self.ops.push(op);
self
}
pub fn rx(&mut self, q: u32, theta: f64) -> &mut Self {
self.push(Op::Rot { axis: vec![(q, Pauli::X)], theta })
}
pub fn ry(&mut self, q: u32, theta: f64) -> &mut Self {
self.push(Op::Rot { axis: vec![(q, Pauli::Y)], theta })
}
pub fn rz(&mut self, q: u32, theta: f64) -> &mut Self {
self.push(Op::Rot { axis: vec![(q, Pauli::Z)], theta })
}
/// `exp(−iθ Z⊗Z/2)`.
pub fn rzz(&mut self, a: u32, b: u32, theta: f64) -> &mut Self {
self.push(Op::Rot { axis: vec![(a, Pauli::Z), (b, Pauli::Z)], theta })
}
/// `exp(−iθ X⊗X/2)`.
pub fn rxx(&mut self, a: u32, b: u32, theta: f64) -> &mut Self {
self.push(Op::Rot { axis: vec![(a, Pauli::X), (b, Pauli::X)], theta })
}
/// Check every qubit index, axis and angle.
pub fn validate(&self) -> Result<(), SpdError> {
if self.n == 0 || self.n > MAX_QUBITS {
return Err(SpdError::TooManyQubits(self.n));
}
let q_ok = |q: u32| {
if q < self.n {
Ok(())
} else {
Err(SpdError::QubitOutOfRange { qubit: q, n: self.n })
}
};
let pair = |a: u32, b: u32| {
q_ok(a)?;
q_ok(b)?;
if a == b { Err(SpdError::RepeatedQubit(a)) } else { Ok(()) }
};
for op in &self.ops {
match op {
Op::H(q) | Op::S(q) | Op::Sdg(q) | Op::SX(q) | Op::SXdg(q) | Op::X(q) | Op::Y(q) | Op::Z(q) => {
q_ok(*q)?
}
Op::CX(a, b) | Op::CZ(a, b) | Op::Swap(a, b) => pair(*a, *b)?,
Op::Rot { axis, theta } => {
check_string(axis, self.n)?;
if axis.is_empty() {
return Err(SpdError::EmptyAxis);
}
if !theta.is_finite() || theta.abs() > MAX_ANGLE {
return Err(SpdError::BadAngle(*theta));
}
}
Op::QuarterRot { axis, k } => {
check_string(axis, self.n)?;
if axis.is_empty() {
return Err(SpdError::EmptyAxis);
}
if !(1..=3).contains(k) {
return Err(SpdError::BadQuarter(*k));
}
}
}
}
Ok(())
}
}
fn check_string(s: &[(u32, Pauli)], n: u32) -> Result<(), SpdError> {
for (i, (q, _)) in s.iter().enumerate() {
if *q >= n {
return Err(SpdError::QubitOutOfRange { qubit: *q, n });
}
if s[..i].iter().any(|(r, _)| r == q) {
return Err(SpdError::RepeatedQubit(*q));
}
}
Ok(())
}
// ---------------------------------------------------------------------------
// Pauli strings as bit words
// ---------------------------------------------------------------------------
/// A Hermitian Pauli string on up to `64·W` qubits: factor `X^x Z^z` at each
/// qubit, with `Y` (both bits set) meaning `i·XZ`, so every string is Hermitian.
#[derive(Clone, Copy, PartialEq, Eq)]
struct Key<const W: usize> {
x: [u64; W],
z: [u64; W],
}
impl<const W: usize> Key<W> {
const ZERO: Self = Key { x: [0; W], z: [0; W] };
fn from_string(s: &[(u32, Pauli)]) -> Self {
let mut k = Self::ZERO;
for &(q, p) in s {
let (w, b) = ((q / 64) as usize, 1u64 << (q % 64));
match p {
Pauli::X => k.x[w] |= b,
Pauli::Z => k.z[w] |= b,
Pauli::Y => {
k.x[w] |= b;
k.z[w] |= b;
}
}
}
k
}
#[inline]
fn get(&self, q: u32) -> (bool, bool) {
let (w, b) = ((q / 64) as usize, q % 64);
((self.x[w] >> b) & 1 == 1, (self.z[w] >> b) & 1 == 1)
}
#[inline]
fn set(&mut self, q: u32, x: bool, z: bool) {
let (w, b) = ((q / 64) as usize, 1u64 << (q % 64));
self.x[w] = (self.x[w] & !b) | if x { b } else { 0 };
self.z[w] = (self.z[w] & !b) | if z { b } else { 0 };
}
/// Whether two strings commute: the symplectic product is even.
#[inline]
fn commutes(&self, o: &Self) -> bool {
let mut p = 0u32;
for w in 0..W {
p += (self.x[w] & o.z[w]).count_ones() + (self.z[w] & o.x[w]).count_ones();
}
p & 1 == 0
}
/// `self · o = i^e · (self ⊕ o)`: the product string and the exponent `e`
/// (mod 4) of its phase.
#[inline]
fn mul(&self, o: &Self) -> (Self, u32) {
let mut out = Self::ZERO;
let mut e = 0u32;
for w in 0..W {
let (x, z) = (self.x[w] ^ o.x[w], self.z[w] ^ o.z[w]);
out.x[w] = x;
out.z[w] = z;
e = e.wrapping_add((self.x[w] & self.z[w]).count_ones());
e = e.wrapping_add((o.x[w] & o.z[w]).count_ones());
e = e.wrapping_add(2 * (self.z[w] & o.x[w]).count_ones());
e = e.wrapping_sub((x & z).count_ones());
}
(out, e & 3)
}
fn weight(&self) -> u32 {
(0..W).map(|w| (self.x[w] | self.z[w]).count_ones()).sum()
}
fn hash(&self) -> u64 {
let mut h = 0x9e37_79b9_7f4a_7c15u64;
for w in 0..W {
h = mix(h ^ self.x[w]);
h = mix(h ^ self.z[w]);
}
h
}
}
#[inline]
fn mix(mut z: u64) -> u64 {
z = (z ^ (z >> 30)).wrapping_mul(0xbf58_476d_1ce4_e5b9);
z = (z ^ (z >> 27)).wrapping_mul(0x94d0_49bb_1331_11eb);
z ^ (z >> 31)
}
/// Pauli terms in insertion order, with a hash index on the side. Iteration
/// follows the order terms were first inserted, so the order coefficients are
/// added in — and with it every bit of the result — is the same on every
/// platform.
///
/// A dropped term keeps its slot with coefficient `0.0`, so dropping costs
/// nothing to the index, and a term that reappears is revived in place. The
/// table is compacted, in order, when dead slots outnumber live ones, and
/// before a Clifford gate rewrites every key anyway. When it happens depends
/// only on the computation, so the result stays the same everywhere.
struct Terms<const W: usize> {
keys: Vec<Key<W>>,
coef: Vec<f64>,
index: Vec<u32>,
stale: bool,
/// Slots whose coefficient is `0.0`.
dead: usize,
}
const EMPTY: u32 = u32::MAX;
impl<const W: usize> Terms<W> {
fn new() -> Self {
Terms { keys: Vec::new(), coef: Vec::new(), index: vec![EMPTY; 16], stale: false, dead: 0 }
}
/// Terms with a nonzero coefficient.
fn live(&self) -> usize {
self.keys.len() - self.dead
}
/// Remove dead slots, keeping the order of the rest.
fn compact(&mut self) {
let mut keep = 0;
for i in 0..self.keys.len() {
if self.coef[i] != 0.0 {
self.keys[keep] = self.keys[i];
self.coef[keep] = self.coef[i];
keep += 1;
}
}
self.keys.truncate(keep);
self.coef.truncate(keep);
self.dead = 0;
self.stale = true;
}
fn reindex(&mut self) {
let mut cap = 16usize;
while cap < 2 * self.keys.len() + 2 {
cap *= 2;
}
self.index.clear();
self.index.resize(cap, EMPTY);
let mask = (cap - 1) as u64;
for (i, k) in self.keys.iter().enumerate() {
let mut slot = (k.hash() & mask) as usize;
while self.index[slot] != EMPTY {
slot = (slot + 1) & (cap - 1);
}
self.index[slot] = i as u32;
}
self.stale = false;
}
/// Add `c` to the coefficient of `k`, inserting it at the end if absent.
fn add(&mut self, k: Key<W>, c: f64) {
debug_assert!(!self.stale);
if 2 * (self.keys.len() + 1) > self.index.len() {
self.reindex();
}
let cap = self.index.len();
let mut slot = (k.hash() & (cap as u64 - 1)) as usize;
loop {
let i = self.index[slot];
if i == EMPTY {
self.index[slot] = self.keys.len() as u32;
self.keys.push(k);
self.coef.push(c);
if c == 0.0 {
self.dead += 1;
}
return;
}
if self.keys[i as usize] == k {
let before = self.coef[i as usize];
let after = before + c;
self.coef[i as usize] = after;
match (before == 0.0, after == 0.0) {
(true, false) => self.dead -= 1,
(false, true) => self.dead += 1,
_ => {}
}
return;
}
slot = (slot + 1) & (cap - 1);
}
}
/// Drop terms with `|c| < threshold`, or heavier than `max_weight`.
/// Returns the sum of dropped `|c|`.
fn truncate(&mut self, threshold: f64, max_weight: Option<u32>) -> f64 {
let mut dropped = 0.0;
for i in 0..self.keys.len() {
let c = self.coef[i];
if c == 0.0 {
continue;
}
if c.abs() < threshold || max_weight.is_some_and(|m| self.keys[i].weight() > m) {
dropped += c.abs();
self.coef[i] = 0.0;
self.dead += 1;
}
}
if 2 * self.dead > self.keys.len() {
self.compact();
}
dropped
}
}
// ---------------------------------------------------------------------------
// Conjugation by one gate
// ---------------------------------------------------------------------------
/// `G† (X^x Z^z) G` for a single-qubit Clifford `G` on one factor: the new
/// factor and whether the sign flips.
fn conj1(op: &Op, x: bool, z: bool) -> (bool, bool, bool) {
// factors: I (0,0), X (1,0), Z (0,1), Y (1,1)
match (op, x, z) {
(_, false, false) => (false, false, false),
(Op::H(_), true, false) => (false, true, false), // X → Z
(Op::H(_), false, true) => (true, false, false), // Z → X
(Op::H(_), true, true) => (true, true, true), // Y → −Y
(Op::S(_), true, false) => (true, true, true), // X → −Y
(Op::S(_), true, true) => (true, false, false), // Y → X
(Op::Sdg(_), true, false) => (true, true, false), // X → Y
(Op::Sdg(_), true, true) => (true, false, true), // Y → −X
(Op::SX(_), false, true) => (true, true, false), // Z → Y
(Op::SX(_), true, true) => (false, true, true), // Y → −Z
(Op::SXdg(_), false, true) => (true, true, true), // Z → −Y
(Op::SXdg(_), true, true) => (false, true, false), // Y → Z
(Op::X(_), _, true) => (x, z, true), // Z, Y → −
(Op::Y(_), true, false) | (Op::Y(_), false, true) => (x, z, true), // X, Z → −
(Op::Z(_), true, _) => (x, z, true), // X, Y → −
_ => (x, z, false),
}
}
/// Apply a Clifford gate to every term in place. Conjugation by a unitary is a
/// bijection on Pauli strings, so no two terms collide.
fn clifford<const W: usize>(t: &mut Terms<W>, op: &Op) {
if t.dead > 0 {
t.compact();
}
for i in 0..t.keys.len() {
let k = &mut t.keys[i];
let neg = match *op {
Op::H(q) | Op::S(q) | Op::Sdg(q) | Op::SX(q) | Op::SXdg(q) | Op::X(q) | Op::Y(q) | Op::Z(q) => {
let (x, z) = k.get(q);
let (nx, nz, neg) = conj1(op, x, z);
k.set(q, nx, nz);
neg
}
Op::CX(c, tq) => {
let ((xc, zc), (xt, zt)) = (k.get(c), k.get(tq));
k.set(tq, xt ^ xc, zt);
k.set(c, xc, zc ^ zt);
xc && zt && !(xt ^ zc)
}
Op::CZ(a, b) => {
let ((xa, za), (xb, zb)) = (k.get(a), k.get(b));
k.set(a, xa, za ^ xb);
k.set(b, xb, zb ^ xa);
xa && xb && (za ^ zb)
}
Op::Swap(a, b) => {
let (fa, fb) = (k.get(a), k.get(b));
k.set(a, fb.0, fb.1);
k.set(b, fa.0, fa.1);
false
}
_ => unreachable!("not a fixed Clifford"),
};
if neg {
t.coef[i] = -t.coef[i];
}
}
t.stale = true;
}
/// Conjugate by `exp(−i(kπ/2)P/2)`, exactly. A string anticommuting with `P`
/// becomes `±(−iQP)` for `k = 1` or `3`, and `−Q` for `k = 2`.
fn quarter<const W: usize>(t: &mut Terms<W>, p: &Key<W>, k: u8) {
if t.dead > 0 {
t.compact();
}
for i in 0..t.keys.len() {
let q = t.keys[i];
if q.commutes(p) {
continue;
}
if k == 2 {
t.coef[i] = -t.coef[i];
continue;
}
let (r, e) = q.mul(p);
// −i·QP = i^{e+3}·R; e is odd here, so the factor is ±1.
let mut neg = (e + 3) & 3 == 2;
if k == 3 {
neg = !neg;
}
t.keys[i] = r;
if neg {
t.coef[i] = -t.coef[i];
}
}
t.stale = true;
}
/// Conjugate by `exp(−iθP/2)`: `Q ↦ cos θ·Q + sin θ·(−iQP)` for every `Q` that
/// anticommutes with `P`. The new terms are merged after every old one has been
/// scaled, so each old coefficient is read before it changes.
fn rotate<const W: usize>(t: &mut Terms<W>, p: &Key<W>, sin: f64, cos: f64, pending: &mut Vec<(Key<W>, f64)>) {
if t.stale {
t.reindex();
}
pending.clear();
for i in 0..t.keys.len() {
let c = t.coef[i];
if c == 0.0 {
continue;
}
let q = t.keys[i];
if q.commutes(p) {
continue;
}
let (r, e) = q.mul(p);
let v = c * sin;
pending.push((r, if (e + 3) & 3 == 2 { -v } else { v }));
t.coef[i] = c * cos;
}
for &(r, v) in pending.iter() {
t.add(r, v);
}
}
// ---------------------------------------------------------------------------
// Propagation
// ---------------------------------------------------------------------------
/// How hard to truncate.
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct SpdConfig {
/// Drop terms whose coefficient magnitude falls below this after a rotation.
/// `0.0` drops only exact zeros: the computation is then exact up to
/// rounding, and its cost may be exponential.
pub threshold: f64,
/// Also drop strings acting on more than this many qubits.
pub max_weight: Option<u32>,
}
impl Default for SpdConfig {
fn default() -> Self {
SpdConfig { threshold: 1e-4, max_weight: None }
}
}
/// An expectation value, what it cost, and how far truncation can have moved it.
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct SpdResult {
/// `⟨ψ₀| U† O U |ψ₀⟩` as computed.
pub value: f64,
/// The sum of every dropped coefficient's magnitude: a bound on
/// `|exact − value|`, up to floating-point rounding.
pub truncation_bound: f64,
/// The most terms the sum held after any gate.
pub peak_terms: usize,
/// Terms left when propagation finished.
pub final_terms: usize,
/// Term-gate updates performed: a portable measure of the work, equal on
/// every machine.
pub term_updates: u64,
}
/// `⟨b| U† O U |b⟩` for the computational basis state `|b⟩` whose set qubits
/// are `ones` (empty for `|0…0⟩`), with `O` a weighted sum of Pauli strings.
pub fn expectation(
circuit: &RotCircuit,
observable: &[(f64, PauliString)],
ones: &[u32],
cfg: &SpdConfig,
) -> Result<SpdResult, SpdError> {
circuit.validate()?;
for (_, s) in observable {
check_string(s, circuit.n)?;
}
for &q in ones {
if q >= circuit.n {
return Err(SpdError::QubitOutOfRange { qubit: q, n: circuit.n });
}
}
Ok(match circuit.n.div_ceil(64) {
1 => run::<1>(circuit, observable, ones, cfg),
2 => run::<2>(circuit, observable, ones, cfg),
3 | 4 => run::<4>(circuit, observable, ones, cfg),
5..=8 => run::<8>(circuit, observable, ones, cfg),
_ => run::<16>(circuit, observable, ones, cfg),
})
}
/// `⟨0…0| U† Z_q U |0…0⟩`.
pub fn expect_z(circuit: &RotCircuit, q: u32, cfg: &SpdConfig) -> Result<SpdResult, SpdError> {
expectation(circuit, &[(1.0, vec![(q, Pauli::Z)])], &[], cfg)
}
fn run<const W: usize>(
circuit: &RotCircuit,
observable: &[(f64, PauliString)],
ones: &[u32],
cfg: &SpdConfig,
) -> SpdResult {
let mut t = Terms::<W>::new();
for (c, s) in observable {
t.add(Key::from_string(s), *c);
}
let mut bound = t.truncate(0.0, None);
let mut peak = t.live();
let mut work = 0u64;
let mut pending = Vec::new();
for op in circuit.ops.iter().rev() {
work += t.live() as u64;
match op {
Op::Rot { axis, theta } => {
let (s, c) = sin_cos(*theta);
rotate(&mut t, &Key::from_string(axis), s, c, &mut pending);
bound += t.truncate(cfg.threshold, cfg.max_weight);
}
Op::QuarterRot { axis, k } => quarter(&mut t, &Key::from_string(axis), *k),
other => clifford(&mut t, other),
}
peak = peak.max(t.live());
}
let b = Key::<W>::from_string(&ones.iter().map(|&q| (q, Pauli::Z)).collect::<Vec<_>>());
let mut value = 0.0;
for i in 0..t.keys.len() {
let k = &t.keys[i];
if t.coef[i] != 0.0 && k.x.iter().all(|&w| w == 0) {
let flips: u32 = (0..W).map(|w| (k.z[w] & b.z[w]).count_ones()).sum();
value += if flips & 1 == 1 { -t.coef[i] } else { t.coef[i] };
}
}
SpdResult { value, truncation_bound: bound, peak_terms: peak, final_terms: t.live(), term_updates: work }
}
// ---------------------------------------------------------------------------
// The heavy-hex kicked-Ising experiment
// ---------------------------------------------------------------------------
/// The 127-qubit heavy-hex coupling map: seven rows (14, 15, 15, 15, 15, 15
/// and 14 qubits) joined by six rows of four bridge qubits. Returns the 144
/// edges, each with the smaller qubit first.
pub fn heavy_hex_127() -> Vec<(u32, u32)> {
let rows: [(u32, u32); 7] = [(0, 13), (18, 32), (37, 51), (56, 70), (75, 89), (94, 108), (113, 126)];
// Each bridge row: (first bridge qubit, column offset into the row above,
// column offset into the row below).
let bridges: [(u32, u32, u32); 6] = [(14, 0, 0), (33, 2, 2), (52, 0, 0), (71, 2, 2), (90, 0, 0), (109, 2, 2)];
let mut edges = Vec::with_capacity(144);
for &(a, b) in &rows {
for q in a..b {
edges.push((q, q + 1));
}
}
for (r, &(first, up, down)) in bridges.iter().enumerate() {
let (above, below) = (rows[r].0, rows[r + 1].0);
for j in 0..4 {
let top = above + up + 4 * j;
// Row 6 starts one column right of the rows above it, so its
// bridge columns are one index earlier.
let bottom = below + down + 4 * j - if r == 5 { 1 } else { 0 };
edges.push((top.min(first + j), top.max(first + j)));
edges.push(((first + j).min(bottom), (first + j).max(bottom)));
}
}
edges.sort_unstable();
edges
}
/// The edges as layers of vertex-disjoint edges: an edge colouring. A graph
/// whose maximum degree is `Δ` and which is bipartite, such as heavy-hex, gets
/// exactly `Δ` layers (König's theorem): each edge takes a colour free at both
/// ends, and when none is, an alternating path is recoloured to free one. A
/// graph with an odd cycle may need a layer more, which it is given. Within a
/// layer the edges are in their input order, and the layers in colour order.
pub fn edge_layers(n: u32, edges: &[(u32, u32)]) -> Vec<Vec<(u32, u32)>> {
let n = n as usize;
let mut deg = vec![0usize; n];
for &(a, b) in edges {
deg[a as usize] += 1;
deg[b as usize] += 1;
}
let mut ncol = deg.iter().copied().max().unwrap_or(0).max(1);
// at[v][c]: the edge of colour c at vertex v, if any
let mut at: Vec<Vec<Option<usize>>> = vec![vec![None; ncol]; n];
let mut colour = vec![usize::MAX; edges.len()];
let other = |f: usize, x: usize| {
let (a, b) = edges[f];
if a as usize == x { b as usize } else { a as usize }
};
for (e, &(u, v)) in edges.iter().enumerate() {
let (u, v) = (u as usize, v as usize);
let free_u = (0..ncol).find(|&c| at[u][c].is_none());
let free_v = (0..ncol).find(|&c| at[v][c].is_none());
let chosen = match (free_u, free_v) {
(Some(a), Some(_)) if at[v][a].is_none() => Some(a),
(Some(a), Some(b)) => {
// Walk the a/b alternating path from v; flipping it frees a at v.
let mut path = Vec::new();
let (mut x, mut c) = (v, a);
let mut reaches_u = false;
while let Some(f) = at[x][c] {
path.push(f);
x = other(f, x);
if x == u {
reaches_u = true;
break;
}
c = if c == a { b } else { a };
}
if reaches_u {
None
} else {
for &f in &path {
let (p, q) = (edges[f].0 as usize, edges[f].1 as usize);
at[p][colour[f]] = None;
at[q][colour[f]] = None;
}
for &f in &path {
let (p, q) = (edges[f].0 as usize, edges[f].1 as usize);
colour[f] = if colour[f] == a { b } else { a };
at[p][colour[f]] = Some(f);
at[q][colour[f]] = Some(f);
}
Some(a)
}
}
_ => None,
};
let c = chosen.unwrap_or_else(|| {
ncol += 1;
for row in at.iter_mut() {
row.push(None);
}
ncol - 1
});
colour[e] = c;
at[u][c] = Some(e);
at[v][c] = Some(e);
}
(0..ncol)
.map(|c| edges.iter().zip(&colour).filter(|&(_, &k)| k == c).map(|(&e, _)| e).collect::<Vec<_>>())
.filter(|layer| !layer.is_empty())
.collect()
}
/// The kicked-Ising circuit of the heavy-hex utility experiment: `steps`
/// Trotter steps, each `RX(θ_h)` on every qubit and then `RZZ(−π/2)` on every
/// edge, layer by layer as [`edge_layers`] partitions them: three layers of
/// vertex-disjoint edges on heavy-hex, as on the device. The `RZZ` gates are applied
/// exactly, as quarter turns.
pub fn kicked_ising(n: u32, edges: &[(u32, u32)], theta_h: f64, steps: u32) -> RotCircuit {
let layers = edge_layers(n, edges);
let mut c = RotCircuit::new(n);
for _ in 0..steps {
for q in 0..n {
c.rx(q, theta_h);
}
for layer in &layers {
for &(a, b) in layer {
// RZZ(−π/2) = exp(+iπ/4·ZZ) = exp(−i(3π/2)ZZ/2) up to a global phase.
c.push(Op::QuarterRot { axis: vec![(a, Pauli::Z), (b, Pauli::Z)], k: 3 });
}
}
}
c
}
// ---------------------------------------------------------------------------
// From the dyadic core
// ---------------------------------------------------------------------------
/// The dyadic circuit as Pauli rotations. Fixed Cliffords map one to one; `T`
/// and `P(k)` become `Z` rotations; a controlled phase on any number of
/// controls becomes rotations about `Z` strings, from
/// `|1…1⟩⟨1…1| = Π (I − Z)/2`; controlled `X` and `Y` beyond one control are
/// that, conjugated by `H` (and `S`) on the target. Global phases drop out of a
/// Heisenberg-picture expectation and are not tracked.
#[cfg(feature = "quantum")]
pub fn from_dyadic(c: &crate::quantum::Circuit) -> Result<RotCircuit, SpdError> {
use crate::quantum::BaseGate as B;
let mut out = RotCircuit::new(u32::from(c.n_qubits));
let tau = 2.0 * std::f64::consts::PI;
for g in &c.ops {
let t = u32::from(g.target);
let ctrls: Vec<u32> = g.controls.iter().map(|&q| u32::from(q)).collect();
// The phase a diagonal base applies to |1⟩, in turns of 2π.
let phase = |base: B| -> Option<f64> {
match base {
B::Z => Some(0.5),
B::S => Some(0.25),
B::Sdg => Some(-0.25),
B::T => Some(0.125),
B::Tdg => Some(-0.125),
B::P => Some(1.0 / (1u64 << g.param) as f64),
_ => None,
}
};
match (g.base, ctrls.as_slice()) {
(B::I, _) => {}
(B::H, []) => _ = out.push(Op::H(t)),
(B::X, []) => _ = out.push(Op::X(t)),
(B::Y, []) => _ = out.push(Op::Y(t)),
(B::Z, []) => _ = out.push(Op::Z(t)),
(B::S, []) => _ = out.push(Op::S(t)),
(B::Sdg, []) => _ = out.push(Op::Sdg(t)),
(B::X, [a]) => _ = out.push(Op::CX(*a, t)),
(B::Z, [a]) => _ = out.push(Op::CZ(*a, t)),
(B::Y, [a]) => {
out.push(Op::Sdg(t)).push(Op::CX(*a, t)).push(Op::S(t));
}
(B::X, _) => {
out.push(Op::H(t));
controlled_phase(&mut out, &ctrls, t, 0.5 * tau)?;
out.push(Op::H(t));
}
(B::Y, _) => {
out.push(Op::Sdg(t)).push(Op::H(t));
controlled_phase(&mut out, &ctrls, t, 0.5 * tau)?;
out.push(Op::H(t)).push(Op::S(t));
}
(base, _) => match phase(base) {
Some(turns) => controlled_phase(&mut out, &ctrls, t, turns * tau)?,
None => return Err(SpdError::Unsupported(format!("{base:?} on {} controls", ctrls.len()))),
},
}
}
Ok(out)
}
/// `diag(…, e^{iφ})` on `controls ∪ {target}`: the phase `φ` on `|1…1⟩` only.
/// With `m` qubits, `|1…1⟩⟨1…1| = 2⁻ᵐ Σ_S (−1)^{|S|} Z_S`, so the gate is
/// `Π_S exp(iφ(−1)^{|S|} Z_S / 2ᵐ)` over nonempty subsets `S` (the empty one is
/// a global phase): a rotation `exp(−iθZ_S/2)` with `θ = −2φ(−1)^{|S|}/2ᵐ`.
///
/// The subsets are counted in a `u32`, so `m` must be below 32; a gate on more
/// qubits (a circuit2 gate may carry up to 254 controls) is refused as
/// unsupported rather than shifted out of range.
#[cfg(feature = "quantum")]
fn controlled_phase(out: &mut RotCircuit, controls: &[u32], target: u32, phi: f64) -> Result<(), SpdError> {
let mut qs = controls.to_vec();
qs.push(target);
let subsets = u32::try_from(qs.len()).ok().and_then(|m| 1u32.checked_shl(m).map(|s| (m, s)));
let Some((m, subsets)) = subsets else {
return Err(SpdError::Unsupported(format!("a phase on {} qubits; at most 31", qs.len())));
};
for mask in 1u32..subsets {
let axis: PauliString = (0..m).filter(|i| mask >> i & 1 == 1).map(|i| (qs[i as usize], Pauli::Z)).collect();
let sign = if axis.len() % 2 == 0 { 1.0 } else { -1.0 };
out.push(Op::Rot { axis, theta: -2.0 * phi * sign / f64::from(subsets) });
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
// ---- an independent Schrödinger-picture reference ----------------------
type C = (f64, f64);
fn cmul(a: C, b: C) -> C {
(a.0 * b.0 - a.1 * b.1, a.0 * b.1 + a.1 * b.0)
}
/// `P|ψ⟩` for a Pauli string, on a dense state.
fn apply_pauli(psi: &[C], s: &[(u32, Pauli)]) -> Vec<C> {
let mut out = vec![(0.0, 0.0); psi.len()];
for (j, &a) in psi.iter().enumerate() {
let mut k = j;
let mut ph: C = (1.0, 0.0);
for &(q, p) in s {
let bit = (j >> q) & 1;
match p {
Pauli::X => k ^= 1 << q,
Pauli::Y => {
k ^= 1 << q;
// Y|0⟩ = i|1⟩, Y|1⟩ = −i|0⟩
ph = cmul(ph, if bit == 0 { (0.0, 1.0) } else { (0.0, -1.0) });
}
Pauli::Z => {
if bit == 1 {
ph = (-ph.0, -ph.1);
}
}
}
}
let v = cmul(ph, a);
out[k].0 += v.0;
out[k].1 += v.1;
}
out
}
fn one_qubit(psi: &mut [C], q: u32, m: [[C; 2]; 2]) {
for j in 0..psi.len() {
if (j >> q) & 1 == 0 {
let k = j | (1 << q);
let (a, b) = (psi[j], psi[k]);
let add = |x: C, y: C| (x.0 + y.0, x.1 + y.1);
psi[j] = add(cmul(m[0][0], a), cmul(m[0][1], b));
psi[k] = add(cmul(m[1][0], a), cmul(m[1][1], b));
}
}
}
/// Run `circuit` on `|ones⟩` as a dense state, with the platform's `sin`
/// and `cos`: nothing shared with the code under test but the `Op` type.
fn dense(circuit: &RotCircuit, ones: &[u32]) -> Vec<C> {
let n = circuit.n;
let mut psi = vec![(0.0, 0.0); 1 << n];
psi[ones.iter().fold(0usize, |a, &q| a | 1 << q)] = (1.0, 0.0);
let r = std::f64::consts::FRAC_1_SQRT_2;
let (o, z, i, ni) = ((1.0, 0.0), (0.0, 0.0), (0.0, 1.0), (0.0, -1.0));
for op in &circuit.ops {
match op {
Op::H(q) => one_qubit(&mut psi, *q, [[(r, 0.0), (r, 0.0)], [(r, 0.0), (-r, 0.0)]]),
Op::S(q) => one_qubit(&mut psi, *q, [[o, z], [z, i]]),
Op::Sdg(q) => one_qubit(&mut psi, *q, [[o, z], [z, ni]]),
Op::SX(q) => one_qubit(&mut psi, *q, [[(0.5, 0.5), (0.5, -0.5)], [(0.5, -0.5), (0.5, 0.5)]]),
Op::SXdg(q) => one_qubit(&mut psi, *q, [[(0.5, -0.5), (0.5, 0.5)], [(0.5, 0.5), (0.5, -0.5)]]),
Op::X(q) => psi = apply_pauli(&psi, &[(*q, Pauli::X)]),
Op::Y(q) => psi = apply_pauli(&psi, &[(*q, Pauli::Y)]),
Op::Z(q) => psi = apply_pauli(&psi, &[(*q, Pauli::Z)]),
Op::CX(c, t) => {
for j in 0..psi.len() {
if (j >> c) & 1 == 1 && (j >> t) & 1 == 0 {
psi.swap(j, j | 1 << t);
}
}
}
Op::CZ(a, b) => {
for (j, v) in psi.iter_mut().enumerate() {
if (j >> a) & 1 == 1 && (j >> b) & 1 == 1 {
*v = (-v.0, -v.1);
}
}
}
Op::Swap(a, b) => {
for j in 0..psi.len() {
if (j >> a) & 1 == 1 && (j >> b) & 1 == 0 {
psi.swap(j, (j & !(1 << a)) | 1 << b);
}
}
}
Op::Rot { axis, theta } => {
let p = apply_pauli(&psi, axis);
let (c, s) = ((theta / 2.0).cos(), (theta / 2.0).sin());
for (v, w) in psi.iter_mut().zip(&p) {
// cos(θ/2)ψ − i sin(θ/2) Pψ
*v = (c * v.0 + s * w.1, c * v.1 - s * w.0);
}
}
Op::QuarterRot { axis, k } => {
let theta = f64::from(*k) * std::f64::consts::FRAC_PI_2;
let p = apply_pauli(&psi, axis);
let (c, s) = ((theta / 2.0).cos(), (theta / 2.0).sin());
for (v, w) in psi.iter_mut().zip(&p) {
*v = (c * v.0 + s * w.1, c * v.1 - s * w.0);
}
}
}
}
psi
}
fn dense_expect(circuit: &RotCircuit, obs: &[(u32, Pauli)], ones: &[u32]) -> f64 {
let psi = dense(circuit, ones);
let p = apply_pauli(&psi, obs);
psi.iter().zip(&p).map(|(a, b)| a.0 * b.0 + a.1 * b.1).sum()
}
/// A small deterministic generator for test circuits.
struct Rng(u64);
impl Rng {
fn next(&mut self) -> u64 {
self.0 = self.0.wrapping_add(0x9e37_79b9_7f4a_7c15);
mix(self.0)
}
fn below(&mut self, n: u64) -> u64 {
self.next() % n
}
fn angle(&mut self) -> f64 {
(self.next() >> 11) as f64 / (1u64 << 53) as f64 * 8.0 - 4.0
}
fn pauli(&mut self) -> Pauli {
[Pauli::X, Pauli::Y, Pauli::Z][self.below(3) as usize]
}
fn string(&mut self, n: u32, max: usize) -> PauliString {
let mut qs: Vec<u32> = (0..n).collect();
let len = 1 + self.below(max as u64) as usize;
let mut s = Vec::new();
for _ in 0..len.min(n as usize) {
let i = self.below(qs.len() as u64) as usize;
s.push((qs.swap_remove(i), self.pauli()));
}
s
}
}
fn random_circuit(rng: &mut Rng, n: u32, len: usize) -> RotCircuit {
let mut c = RotCircuit::new(n);
for _ in 0..len {
let q = rng.below(u64::from(n)) as u32;
let mut r = rng.below(u64::from(n - 1)) as u32;
if r >= q {
r += 1;
}
let op = match rng.below(14) {
0 => Op::H(q),
1 => Op::S(q),
2 => Op::Sdg(q),
3 => Op::SX(q),
4 => Op::SXdg(q),
5 => Op::X(q),
6 => Op::Y(q),
7 => Op::Z(q),
8 => Op::CX(q, r),
9 => Op::CZ(q, r),
10 => Op::Swap(q, r),
11 => Op::QuarterRot { axis: rng.string(n, 3), k: 1 + rng.below(3) as u8 },
_ => Op::Rot { axis: rng.string(n, 3), theta: rng.angle() },
};
c.push(op);
}
c
}
// ---- trigonometry --------------------------------------------------------
#[test]
fn sin_cos_is_within_an_ulp_of_the_platform() {
let mut rng = Rng(7);
let ulp = |a: f64, b: f64| (a.to_bits() as i64 - b.to_bits() as i64).unsigned_abs();
for i in 0..200_000 {
let x = match i % 4 {
0 => rng.angle(),
1 => rng.angle() * 1e-6,
2 => rng.angle() * 1e4,
_ => rng.angle() * 0.2,
};
let (s, c) = sin_cos(x);
// Both are faithful roundings; they may differ by one ulp at most,
// and only where the true value is close to a rounding boundary.
assert!(ulp(s, x.sin()) <= 1 || (s - x.sin()).abs() < 1e-300, "sin {x}: {s} vs {}", x.sin());
assert!(ulp(c, x.cos()) <= 1, "cos {x}: {c} vs {}", x.cos());
}
}
#[test]
fn sin_cos_bits_are_pinned() {
// Bit patterns this implementation must produce on every target: they
// are what the cross-architecture run (wasm32-wasip2) is checked against.
// Each is within 0.51 ulp of the true value, checked against an
// 80-digit Taylor series when it was pinned.
let pins: [(f64, u64, u64); 4] = [
(0.3, 0x3fd2_e9cd_95ba_ba33, 0x3fee_921d_d42f_09ba),
(1.0, 0x3fea_ed54_8f09_0cee, 0x3fe1_4a28_0fb5_068c),
(-2.5, 0xbfe3_26af_0dcf_cab0, 0xbfe9_a2f7_ef85_8b7d),
(100.0, 0xbfe0_3425_b78c_4db8, 0x3feb_981d_bf66_5fdf),
];
for (x, sb, cb) in pins {
let (s, c) = sin_cos(x);
assert_eq!((s.to_bits(), c.to_bits()), (sb, cb), "x = {x}: sin {s:e} cos {c:e}");
}
}
#[test]
fn quarter_turns_are_exact() {
for (k, x) in [(1, std::f64::consts::FRAC_PI_2), (2, std::f64::consts::PI)] {
let (s, c) = sin_cos(x);
// Not exactly 0 and ±1 — π/2 is not representable — which is why the
// Clifford rotations have their own exact op.
assert!(c.abs() < 1e-15 || k == 2, "k = {k}");
assert!((s.abs() - if k == 1 { 1.0 } else { 0.0 }).abs() < 1e-15);
}
}
// ---- Pauli algebra against matrices --------------------------------------
#[test]
fn every_clifford_conjugates_every_pauli_as_its_matrix_does() {
// For each gate G and each Pauli P on two qubits, the Heisenberg rule's
// G†PG must give the same expectation on random states as conjugating
// the state instead: ⟨ψ|G†PG|ψ⟩ = ⟨Gψ|P|Gψ⟩.
let gates = [
Op::H(0), Op::S(0), Op::Sdg(0), Op::SX(0), Op::SXdg(0), Op::X(0), Op::Y(0), Op::Z(0),
Op::H(1), Op::SX(1), Op::CX(0, 1), Op::CX(1, 0), Op::CZ(0, 1), Op::Swap(0, 1),
];
let mut rng = Rng(11);
let paulis = [None, Some(Pauli::X), Some(Pauli::Y), Some(Pauli::Z)];
for g in &gates {
for p0 in paulis {
for p1 in paulis {
let obs: PauliString = [(0, p0), (1, p1)].iter().filter_map(|&(q, p)| p.map(|p| (q, p))).collect();
if obs.is_empty() {
continue;
}
// a random state prepared by rotations, then the gate
let mut c = RotCircuit::new(2);
for _ in 0..6 {
c.push(Op::Rot { axis: rng.string(2, 2), theta: rng.angle() });
}
c.push(g.clone());
let want = dense_expect(&c, &obs, &[]);
let got = expectation(&c, &[(1.0, obs.clone())], &[], &SpdConfig { threshold: 0.0, max_weight: None }).unwrap();
assert!((got.value - want).abs() < 1e-12, "{g:?} {obs:?}: {} vs {want}", got.value);
}
}
}
}
#[test]
fn random_circuits_match_the_dense_reference() {
let mut rng = Rng(2026);
for trial in 0..300 {
let n = 2 + rng.below(6) as u32;
let len = 6 + rng.below(30) as usize;
let c = random_circuit(&mut rng, n, len);
let obs = rng.string(n, 3);
let ones: Vec<u32> = (0..n).filter(|_| rng.below(2) == 1).collect();
let want = dense_expect(&c, &obs, &ones);
let got = expectation(&c, &[(1.0, obs.clone())], &ones, &SpdConfig { threshold: 0.0, max_weight: None }).unwrap();
assert!((got.value - want).abs() < 1e-10, "trial {trial}: {} vs {want}", got.value);
assert_eq!(got.truncation_bound, 0.0, "nothing is dropped at threshold 0 but exact zeros");
}
}
#[test]
fn the_truncation_bound_holds() {
// At every threshold the computed value is within the reported bound of
// the exact one — the guarantee, checked on circuits where the bound is
// not vacuous.
let mut rng = Rng(99);
let mut nontrivial = 0;
for _ in 0..120 {
let n = 6;
let c = random_circuit(&mut rng, n, 60);
let obs = vec![(rng.below(6) as u32, Pauli::Z)];
let exact = dense_expect(&c, &obs, &[]);
for threshold in [1e-3, 1e-2, 5e-2, 0.2] {
let r = expectation(&c, &[(1.0, obs.clone())], &[], &SpdConfig { threshold, max_weight: None }).unwrap();
assert!((r.value - exact).abs() <= r.truncation_bound + 1e-12, "|{} − {exact}| > {}", r.value, r.truncation_bound);
if r.truncation_bound > 0.0 && r.truncation_bound < 1.0 {
nontrivial += 1;
}
}
}
assert!(nontrivial > 50, "the bound was exercised {nontrivial} times");
}
#[test]
fn results_are_bit_identical_from_run_to_run() {
// A fresh std HashMap is seeded differently every time, so a term table
// that summed in its order would already differ here, in one process.
let c = kicked_ising(127, &heavy_hex_127(), 0.6, 5);
let cfg = SpdConfig { threshold: 1e-4, max_weight: None };
let a = expect_z(&c, 62, &cfg).unwrap();
assert!(a.peak_terms > 1000, "the run merges many terms: {}", a.peak_terms);
for _ in 0..4 {
let b = expect_z(&c, 62, &cfg).unwrap();
assert_eq!(a.value.to_bits(), b.value.to_bits());
assert_eq!(a.truncation_bound.to_bits(), b.truncation_bound.to_bits());
assert_eq!((a.peak_terms, a.term_updates), (b.peak_terms, b.term_updates));
}
}
#[test]
fn word_width_does_not_change_the_answer() {
// The same circuit, placed in a 30-qubit and a 900-qubit register, is
// computed with one and with sixteen words per string: the bits agree.
let chain: Vec<(u32, u32)> = (0..29).map(|q| (q, q + 1)).collect();
let small = kicked_ising(30, &chain, 0.5, 6);
let mut big = small.clone();
big.n = 900;
let cfg = SpdConfig { threshold: 1e-6, max_weight: None };
let a = expect_z(&small, 7, &cfg).unwrap();
let b = expect_z(&big, 7, &cfg).unwrap();
assert_eq!(a.value.to_bits(), b.value.to_bits());
assert_eq!(a.term_updates, b.term_updates);
}
#[test]
fn invalid_circuits_are_refused() {
let mut c = RotCircuit::new(3);
c.push(Op::CX(1, 1));
assert_eq!(c.validate(), Err(SpdError::RepeatedQubit(1)));
let mut c = RotCircuit::new(3);
c.rx(3, 0.1);
assert_eq!(c.validate(), Err(SpdError::QubitOutOfRange { qubit: 3, n: 3 }));
let mut c = RotCircuit::new(3);
c.rx(0, f64::NAN);
assert!(matches!(c.validate(), Err(SpdError::BadAngle(_))));
let mut c = RotCircuit::new(3);
c.push(Op::QuarterRot { axis: vec![(0, Pauli::Z)], k: 4 });
assert_eq!(c.validate(), Err(SpdError::BadQuarter(4)));
assert_eq!(RotCircuit::new(1025).validate(), Err(SpdError::TooManyQubits(1025)));
}
// ---- the heavy-hex experiment ---------------------------------------------
#[test]
fn the_heavy_hex_map_has_the_published_shape() {
let e = heavy_hex_127();
assert_eq!(e.len(), 144);
let mut deg = [0u32; 127];
for &(a, b) in &e {
assert!(a < b && b < 127);
deg[a as usize] += 1;
deg[b as usize] += 1;
}
assert!(deg.iter().all(|&d| (1..=3).contains(&d)));
// Heavy-hex: every bridge qubit has degree 2, and no two degree-3
// qubits are adjacent.
for &(a, b) in &e {
assert!(!(deg[a as usize] == 3 && deg[b as usize] == 3), "({a},{b})");
}
for b in [14u32, 15, 16, 17, 33, 34, 35, 36, 52, 53, 54, 55, 71, 72, 73, 74, 90, 91, 92, 93, 109, 110, 111, 112] {
assert_eq!(deg[b as usize], 2, "bridge {b}");
}
// connected
let mut seen = [false; 127];
let mut stack = vec![0u32];
while let Some(q) = stack.pop() {
if !std::mem::replace(&mut seen[q as usize], true) {
stack.extend(e.iter().filter(|&&(a, b)| a == q || b == q).map(|&(a, b)| a + b - q));
}
}
assert!(seen.iter().all(|&s| s));
// It is bipartite, so three matchings cover it, as the experiment's
// three RZZ layers do.
let mut colour = [u8::MAX; 127];
colour[0] = 0;
let mut stack = vec![0u32];
while let Some(q) = stack.pop() {
for &(a, b) in e.iter().filter(|&&(a, b)| a == q || b == q) {
let o = (a + b - q) as usize;
if colour[o] == u8::MAX {
colour[o] = 1 - colour[q as usize];
stack.push(o as u32);
}
assert_ne!(colour[o], colour[q as usize]);
}
}
}
#[test]
fn edge_layers_are_proper_colourings_with_as_few_layers_as_the_degree() {
let check = |n: u32, edges: &[(u32, u32)], want: usize| {
let layers = edge_layers(n, edges);
assert_eq!(layers.len(), want);
let mut all: Vec<(u32, u32)> = layers.iter().flatten().copied().collect();
all.sort_unstable();
let mut sorted = edges.to_vec();
sorted.sort_unstable();
assert_eq!(all, sorted, "every edge exactly once");
for layer in &layers {
let mut seen = std::collections::BTreeSet::new();
for &(a, b) in layer {
assert!(seen.insert(a) && seen.insert(b), "vertex-disjoint");
}
}
};
check(127, &heavy_hex_127(), 3);
let chain: Vec<(u32, u32)> = (0..9).map(|q| (q, q + 1)).collect();
check(10, &chain, 2);
// A triangle is not bipartite: three edges, degree two, three layers.
check(3, &[(0, 1), (1, 2), (0, 2)], 3);
}
#[test]
fn the_kicked_ising_clifford_points_are_exact() {
// θ_h = 0: no rotation, every ⟨Z_q⟩ = 1. θ_h = π/2 written as quarter
// turns: a Clifford circuit, so the Heisenberg sum stays one term and
// ⟨Z_q⟩ is exactly 0 or ±1 with nothing dropped.
let e = heavy_hex_127();
let still = kicked_ising(127, &e, 0.0, 20);
let r = expect_z(&still, 62, &SpdConfig::default()).unwrap();
assert_eq!((r.value, r.truncation_bound, r.peak_terms), (1.0, 0.0, 1));
let mut cliff = RotCircuit::new(127);
for _ in 0..20 {
for q in 0..127 {
cliff.push(Op::QuarterRot { axis: vec![(q, Pauli::X)], k: 1 });
}
for &(a, b) in &e {
cliff.push(Op::QuarterRot { axis: vec![(a, Pauli::Z), (b, Pauli::Z)], k: 3 });
}
}
for q in [0, 13, 62, 126] {
let r = expect_z(&cliff, q, &SpdConfig::default()).unwrap();
assert!(r.value == 0.0 || r.value.abs() == 1.0, "q {q}: {}", r.value);
assert_eq!((r.truncation_bound, r.peak_terms), (0.0, 1));
}
}
#[test]
fn kicked_ising_on_a_fragment_matches_the_dense_reference() {
// The same circuit family on a 15-qubit heavy-hex fragment, where a
// dense state fits: exact propagation agrees with it at every angle.
let e: Vec<(u32, u32)> = heavy_hex_127().into_iter().filter(|&(a, b)| a < 15 && b < 15).collect();
assert!(e.len() >= 13);
for theta_h in [0.1, 0.4, 0.7854, 1.2] {
let c = kicked_ising(15, &e, theta_h, 4);
for q in [0, 4, 14] {
let want = dense_expect(&c, &[(q, Pauli::Z)], &[]);
let got = expect_z(&c, q, &SpdConfig { threshold: 0.0, max_weight: None }).unwrap();
assert!((got.value - want).abs() < 1e-10, "θ_h {theta_h} q {q}: {} vs {want}", got.value);
}
}
}
// ---- the dyadic adapter ------------------------------------------------------
/// A controlled phase counts its `2^m` subsets in a `u32`. Forty controls
/// would shift out of range; the gate is refused instead.
#[cfg(feature = "quantum")]
#[test]
fn a_phase_on_forty_controls_does_not_overflow() {
use crate::quantum::{BaseGate, Circuit, Gate};
let mut out = RotCircuit::new(41);
let controls: Vec<u32> = (0..40).collect();
assert!(matches!(controlled_phase(&mut out, &controls, 40, 1.0), Err(SpdError::Unsupported(_))));
assert!(out.ops.is_empty(), "nothing is emitted before the refusal");
// At the edge: 31 controls and the target make 32 qubits, refused.
assert!(matches!(controlled_phase(&mut out, &(0..31).collect::<Vec<u32>>(), 31, 1.0), Err(SpdError::Unsupported(_))));
// Through the adapter, for every multi-control gate it expands.
for base in [BaseGate::X, BaseGate::Y, BaseGate::Z, BaseGate::T, BaseGate::P] {
let mut c = Circuit::new(41);
let param = if base == BaseGate::P { 5 } else { 0 };
c.ops.push(Gate { base, controls: (0..40).collect(), target: 40, param });
c.validate().unwrap();
assert!(matches!(from_dyadic(&c), Err(SpdError::Unsupported(_))), "{base:?}");
}
// A small gate still expands: two controls, 2^3 − 1 rotations.
let mut out = RotCircuit::new(3);
controlled_phase(&mut out, &[0, 1], 2, 1.0).unwrap();
assert_eq!(out.ops.len(), 7);
}
#[cfg(feature = "quantum")]
#[test]
fn every_dyadic_algorithm_agrees_with_the_byte_exact_core() {
use crate::quantum::Circuit;
// Random dyadic circuits with every base gate and up to three controls:
// ⟨Z_q⟩ from the byte-exact simulator's probabilities and from the
// rotation form agree.
let mut rng = Rng(31);
for trial in 0..150 {
let n = 3 + rng.below(4) as u8;
let mut c = Circuit::new(n);
for _ in 0..25 {
let t = rng.below(u64::from(n)) as u8;
let mut ctrls = Vec::new();
for _ in 0..rng.below(4) {
let q = rng.below(u64::from(n)) as u8;
if q != t && !ctrls.contains(&q) {
ctrls.push(q);
}
}
let (base, param) = match rng.below(9) {
0 => (crate::quantum::BaseGate::H, 0),
1 => (crate::quantum::BaseGate::X, 0),
2 => (crate::quantum::BaseGate::Y, 0),
3 => (crate::quantum::BaseGate::Z, 0),
4 => (crate::quantum::BaseGate::S, 0),
5 => (crate::quantum::BaseGate::Sdg, 0),
6 => (crate::quantum::BaseGate::T, 0),
7 => (crate::quantum::BaseGate::Tdg, 0),
_ => (crate::quantum::BaseGate::P, 1 + rng.below(6) as u16),
};
if base == crate::quantum::BaseGate::H {
ctrls.clear();
}
c.ops.push(crate::quantum::Gate { base, controls: ctrls, target: t, param });
}
let sv = c.simulate().unwrap();
let w = sv.prob_weights();
let total: f64 = w.iter().map(|&x| x as f64).sum();
let rc = from_dyadic(&c).unwrap();
for q in 0..n {
let want: f64 = w.iter().enumerate().map(|(j, &x)| if (j >> q) & 1 == 0 { x as f64 } else { -(x as f64) }).sum::<f64>() / total;
let got = expect_z(&rc, u32::from(q), &SpdConfig { threshold: 0.0, max_weight: None }).unwrap();
assert!((got.value - want).abs() < 1e-6, "trial {trial} q {q}: {} vs {want}", got.value);
}
}
}
}