Skip to main content

wai_quantum/
quantum.rs

1//! Deterministic quantum-circuit **simulation** as transportable, receiptable
2//! content — `wai.quantum.circuit` (extensions/quantum-sim).
3//!
4//! The honest framing first: WAI's whole moat is *determinism* — byte-identical
5//! reconstruction on every machine, "conformance is a hash". Real quantum
6//! HARDWARE is the natural enemy of that (NISQ noise + stochastic measurement are
7//! not reproducible across devices). So quantum does NOT enter WAI as a hardware
8//! dependency and gives NO quantum speedup here. It enters exactly as worlds,
9//! films and scores do (lever 3, *instructions-at-the-sink*): the wire carries a
10//! compact **circuit** (a gate op-log), the sink CLASSICALLY simulates it, and the
11//! reconstructed statevector hashes identically everywhere.
12//!
13//! The reconstruction runs in a fixed-point complex floor (the `wai.det.fixed64`
14//! discipline applied to amplitudes): every amplitude is an [`Amp`] — real and
15//! imaginary parts as `i64` at scale `2^FRAC` — and every gate is an integer
16//! fixed-point matrix–vector update with a pinned rounding rule and a *stated
17//! error bound* versus the ideal unitary (the same contract as
18//! the WAI integer transform floor). No float anywhere on the decode, so:
19//!
20//! - **statevector-equivalence** — the whole amplitude vector is byte-identical on
21//!   every machine (a portable statevector hash), and
22//! - **shot-histogram-equivalence** — measurement is sampled by the pinned
23//!   `splitmix64` PRNG (the same fixed-point sampler the WAI deterministic generator uses), so
24//!   a shot histogram at a pinned `(seed, shots)` is byte-identical too.
25//!
26//! The transportable artifact is the *circuit* (kilobytes, linear in gate count);
27//! the reconstruction is the *statevector* (`2^n` amplitudes — exponential). That
28//! gap is the point: a tiny circuit reconstructs to a gigabyte statevector, and
29//! because the reconstruction cost is exponential the joules-accounted receipt
30//! (§ extension) is a genuinely novel artifact — an energy-metered, byte-exact,
31//! signable record of a quantum computation.
32//!
33//! Gate set: a *controlled-1-qubit-gate* IR over Clifford+T + dyadic phase
34//! (`I,X,Y,Z,H,S,S†,T,T†,P(k)` with any number of controls). Clifford+T is dense
35//! in `SU(2^n)`, so this is universal; the dyadic phase `P(k) = e^{2πi/2^k}` gives
36//! the controlled-phase ladder the **QFT** ("quantum FFT") needs, transported as a
37//! circuit and reconstructed byte-exact. Pure i64, no float, no `ort`; compiles to
38//! wasm32.
39//!
40//! Out of scope (and honestly so): variational-quantum-circuit / "quantum AI"
41//! priors whose measurement is stochastic — those are at best a float/behavioral
42//! capability that can never back `intent=replicate`, so they do not belong in
43//! this exact-reconstruction extension. [`StateVector::fidelity_fx`] is provided as the
44//! quantum-information-theoretic conformance metric such a future path would use.
45
46use std::sync::OnceLock;
47
48/// Fractional bits of the fixed-point amplitude floor. Amplitudes live in
49/// `[-1, 1]`, so `2^FRAC` (≈ `1.07e9` at 30) is the unit and the i128 products in
50/// [`Amp::mul`] stay far inside range.
51pub const FRAC: u32 = 30;
52/// The fixed-point unit `1.0`.
53pub const ONE: i64 = 1 << FRAC;
54const ROUND: i128 = 1 << (FRAC - 1);
55/// Largest `k` for which `P(k) = e^{2πi/2^k}` is tabulated — the QFT reaches this
56/// many qubits. The table is built by an integer half-angle recurrence (no float).
57pub const DYADIC_MAX: usize = 32;
58
59/// Domain separators for the conformance hashes (the WAI discipline).
60const DOMAIN_STATEVECTOR: &[u8] = b"wai:quantum-statevector\x01";
61const DOMAIN_CIRCUIT: &[u8] = b"wai:quantum-circuit\x01";
62const DOMAIN_HISTOGRAM: &[u8] = b"wai:quantum-histogram\x01";
63
64/// The capability string this module reconstructs.
65pub const CAPABILITY: &str = "wai.quantum.circuit";
66
67// ---------------------------------------------------------------------------
68// Fixed-point complex amplitude
69// ---------------------------------------------------------------------------
70
71/// A complex amplitude in the fixed-point floor: `re, im` are `i64` at scale
72/// `2^FRAC`. Byte-exact by construction — `Eq` is total (no float), so two
73/// statevectors compare and hash identically iff every amplitude matches.
74#[derive(Clone, Copy, Debug, PartialEq, Eq)]
75pub struct Amp {
76    pub re: i64,
77    pub im: i64,
78}
79
80/// Fixed-point multiply with a pinned round-half-up rule. `i128` intermediate so
81/// the `i64·i64` product never overflows; the `+ROUND` then arithmetic shift is
82/// identical on every machine (the determinism the whole floor rests on).
83#[inline]
84pub fn fxmul(a: i64, b: i64) -> i64 {
85    ((a as i128 * b as i128 + ROUND) >> FRAC) as i64
86}
87
88/// Fixed-point square root: `v` is at `2^FRAC`, result at `2^FRAC`
89/// (`sqrt(v/ONE)·ONE = isqrt(v·ONE)`). Integer `isqrt`, no float; negatives
90/// (never produced by the half-angle recurrence) clamp to 0.
91#[inline]
92pub fn sqrt_fx(v: i64) -> i64 {
93    if v <= 0 {
94        return 0;
95    }
96    (((v as u128) << FRAC).isqrt()) as i64
97}
98
99impl Amp {
100    /// `0 + 0i`.
101    pub const ZERO: Amp = Amp { re: 0, im: 0 };
102    /// `1 + 0i` (the fixed-point unit).
103    pub const ONE: Amp = Amp { re: ONE, im: 0 };
104
105    #[inline]
106    pub fn add(self, o: Amp) -> Amp {
107        Amp { re: self.re + o.re, im: self.im + o.im }
108    }
109
110    /// Complex fixed-point multiply: `(a+bi)(c+di) = (ac−bd) + (ad+bc)i`.
111    #[inline]
112    pub fn mul(self, o: Amp) -> Amp {
113        Amp {
114            re: fxmul(self.re, o.re) - fxmul(self.im, o.im),
115            im: fxmul(self.re, o.im) + fxmul(self.im, o.re),
116        }
117    }
118
119    /// Complex conjugate.
120    #[inline]
121    pub fn conj(self) -> Amp {
122        Amp { re: self.re, im: -self.im }
123    }
124
125    /// `|amp|^2` at scale `2^(2·FRAC)`, in `i128` (the un-shifted probability
126    /// weight — summed and sampled without a divide).
127    #[inline]
128    pub fn norm2(self) -> i128 {
129        self.re as i128 * self.re as i128 + self.im as i128 * self.im as i128
130    }
131}
132
133// ---------------------------------------------------------------------------
134// Pinned dyadic phase table — e^{2πi / 2^k}, built by integer half-angle
135// recurrence from (cos π/2, sin π/2) = (0, 1). No float ever runs.
136// ---------------------------------------------------------------------------
137
138fn dyadic_table() -> &'static [Amp] {
139    static TABLE: OnceLock<Vec<Amp>> = OnceLock::new();
140    TABLE.get_or_init(|| {
141        let mut t = vec![Amp::ZERO; DYADIC_MAX + 1];
142        // k = 1: e^{iπ}   = -1 + 0i
143        t[1] = Amp { re: -ONE, im: 0 };
144        // k = 2: e^{iπ/2} =  0 + 1i
145        t[2] = Amp { re: 0, im: ONE };
146        // k ≥ 3: half the angle each step. For θ ∈ (0, π/2], both cos and sin are
147        // non-negative, so the positive integer sqrt is the right branch:
148        //   cos(θ/2) = sqrt((1+cosθ)/2),  sin(θ/2) = sqrt((1−cosθ)/2)
149        for k in 3..=DYADIC_MAX {
150            let c = t[k - 1].re;
151            t[k] = Amp {
152                re: sqrt_fx((ONE + c) / 2),
153                im: sqrt_fx((ONE - c) / 2),
154            };
155        }
156        t
157    })
158}
159
160/// `1/√2` in the fixed-point floor — derived from the dyadic table (`cos π/4`), so
161/// there is a *single* source for the one irrational the Clifford+T set needs.
162pub fn inv_sqrt2() -> i64 {
163    dyadic_table()[3].re
164}
165
166/// `e^{2πi / 2^k}` in the fixed-point floor, for `1 ≤ k ≤ DYADIC_MAX`.
167pub fn phase(k: usize) -> Amp {
168    dyadic_table()[k]
169}
170
171// ---------------------------------------------------------------------------
172// Gates — a controlled-1-qubit-gate IR
173// ---------------------------------------------------------------------------
174
175/// The 1-qubit base gates. `P` carries a dyadic index in [`Gate::param`].
176#[derive(Clone, Copy, Debug, PartialEq, Eq)]
177pub enum BaseGate {
178    I,
179    X,
180    Y,
181    Z,
182    H,
183    S,
184    Sdg,
185    T,
186    Tdg,
187    /// `P(k) = diag(1, e^{2πi/2^k})`.
188    P,
189}
190
191impl BaseGate {
192    fn opcode(self) -> u8 {
193        match self {
194            BaseGate::I => 0,
195            BaseGate::X => 1,
196            BaseGate::Y => 2,
197            BaseGate::Z => 3,
198            BaseGate::H => 4,
199            BaseGate::S => 5,
200            BaseGate::Sdg => 6,
201            BaseGate::T => 7,
202            BaseGate::Tdg => 8,
203            BaseGate::P => 9,
204        }
205    }
206    fn from_opcode(b: u8) -> Option<BaseGate> {
207        Some(match b {
208            0 => BaseGate::I,
209            1 => BaseGate::X,
210            2 => BaseGate::Y,
211            3 => BaseGate::Z,
212            4 => BaseGate::H,
213            5 => BaseGate::S,
214            6 => BaseGate::Sdg,
215            7 => BaseGate::T,
216            8 => BaseGate::Tdg,
217            9 => BaseGate::P,
218            _ => return None,
219        })
220    }
221
222    /// The 2×2 fixed-point matrix `[[m00,m01],[m10,m11]]` for this gate.
223    fn matrix(self, param: u16) -> [[Amp; 2]; 2] {
224        let z = Amp::ZERO;
225        let one = Amp::ONE;
226        let i = Amp { re: 0, im: ONE };
227        let neg_i = Amp { re: 0, im: -ONE };
228        let s = Amp { re: inv_sqrt2(), im: 0 };
229        let neg_s = Amp { re: -inv_sqrt2(), im: 0 };
230        let neg_one = Amp { re: -ONE, im: 0 };
231        match self {
232            BaseGate::I => [[one, z], [z, one]],
233            BaseGate::X => [[z, one], [one, z]],
234            BaseGate::Y => [[z, neg_i], [i, z]],
235            BaseGate::Z => [[one, z], [z, neg_one]],
236            BaseGate::H => [[s, s], [s, neg_s]],
237            BaseGate::S => [[one, z], [z, i]],
238            BaseGate::Sdg => [[one, z], [z, neg_i]],
239            BaseGate::T => [[one, z], [z, phase(3)]],
240            BaseGate::Tdg => [[one, z], [z, phase(3).conj()]],
241            BaseGate::P => [[one, z], [z, phase(param as usize)]],
242        }
243    }
244}
245
246/// One instruction: a base gate on `target`, applied only when every qubit in
247/// `controls` is set. `controls = []` is a plain 1-qubit gate; one control gives
248/// CX/CZ/CP; two gives Toffoli-class; etc.
249#[derive(Clone, Debug, PartialEq, Eq)]
250pub struct Gate {
251    pub base: BaseGate,
252    pub controls: Vec<u8>,
253    pub target: u8,
254    pub param: u16,
255}
256
257// ---------------------------------------------------------------------------
258// Circuit
259// ---------------------------------------------------------------------------
260
261/// A quantum circuit: `n_qubits` and an ordered op-log. This is the transportable
262/// object — compact, linear in gate count.
263#[derive(Clone, Debug, PartialEq, Eq)]
264pub struct Circuit {
265    pub n_qubits: u8,
266    pub ops: Vec<Gate>,
267}
268
269/// Errors from parsing a `WQC` container or building an out-of-range circuit.
270#[derive(Debug, PartialEq, Eq)]
271pub enum QuantumError {
272    /// Bad magic, truncated section, or malformed op-log.
273    Malformed(String),
274    /// A qubit index ≥ `n_qubits`, a control equal to the target, or a `P(k)` with
275    /// `k` outside `1..=DYADIC_MAX`.
276    Invalid(String),
277    /// `n_qubits` exceeds what this build will materialize (`2^n` amplitudes).
278    TooManyQubits(u8),
279}
280
281impl std::fmt::Display for QuantumError {
282    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
283        match self {
284            QuantumError::Malformed(e) => write!(f, "malformed WQC: {e}"),
285            QuantumError::Invalid(e) => write!(f, "invalid circuit: {e}"),
286            QuantumError::TooManyQubits(n) => write!(f, "too many qubits: {n}"),
287        }
288    }
289}
290impl std::error::Error for QuantumError {}
291
292/// The largest circuit this build will simulate (a state vector is `2^n`
293/// amplitudes × 16 bytes; 26 qubits ≈ 1 GiB). The *format* supports more — this
294/// is a materialization guard, not a spec limit.
295pub const MAX_QUBITS: u8 = 26;
296
297impl Circuit {
298    pub fn new(n_qubits: u8) -> Self {
299        Circuit { n_qubits, ops: Vec::new() }
300    }
301
302    fn push(&mut self, base: BaseGate, controls: Vec<u8>, target: u8, param: u16) -> &mut Self {
303        self.ops.push(Gate { base, controls, target, param });
304        self
305    }
306
307    // Ergonomic builders (used by the QFT constructor and the tests/vectors).
308    pub fn x(&mut self, q: u8) -> &mut Self { self.push(BaseGate::X, vec![], q, 0) }
309    pub fn y(&mut self, q: u8) -> &mut Self { self.push(BaseGate::Y, vec![], q, 0) }
310    pub fn z(&mut self, q: u8) -> &mut Self { self.push(BaseGate::Z, vec![], q, 0) }
311    pub fn h(&mut self, q: u8) -> &mut Self { self.push(BaseGate::H, vec![], q, 0) }
312    pub fn s(&mut self, q: u8) -> &mut Self { self.push(BaseGate::S, vec![], q, 0) }
313    pub fn t(&mut self, q: u8) -> &mut Self { self.push(BaseGate::T, vec![], q, 0) }
314    /// `P(k) = diag(1, e^{2πi/2^k})` on qubit `q`.
315    pub fn p(&mut self, k: u16, q: u8) -> &mut Self { self.push(BaseGate::P, vec![], q, k) }
316    pub fn cx(&mut self, c: u8, t: u8) -> &mut Self { self.push(BaseGate::X, vec![c], t, 0) }
317    pub fn cz(&mut self, c: u8, t: u8) -> &mut Self { self.push(BaseGate::Z, vec![c], t, 0) }
318    /// Controlled `P(k)` — the QFT's rotation gate.
319    pub fn cp(&mut self, k: u16, c: u8, t: u8) -> &mut Self { self.push(BaseGate::P, vec![c], t, k) }
320    /// Toffoli (`CCX`).
321    pub fn ccx(&mut self, c0: u8, c1: u8, t: u8) -> &mut Self {
322        self.push(BaseGate::X, vec![c0, c1], t, 0)
323    }
324    /// `SWAP(a,b)` as three CX (kept in the op-log as gates, not a new opcode).
325    pub fn swap(&mut self, a: u8, b: u8) -> &mut Self {
326        self.cx(a, b).cx(b, a).cx(a, b)
327    }
328
329    /// The **Quantum Fourier Transform** over all `n` qubits — the "quantum FFT",
330    /// transported as a circuit (Hadamard + controlled-phase ladder + bit-reversal
331    /// swaps) and reconstructed byte-exact. Uses the pinned dyadic phases.
332    pub fn qft(n: u8) -> Circuit {
333        let mut c = Circuit::new(n);
334        for j in 0..n {
335            c.h(j);
336            for l in (j + 1)..n {
337                // controlled R_m between control l and target j, m = l−j+1
338                let m = (l - j + 1) as u16;
339                c.cp(m, l, j);
340            }
341        }
342        for j in 0..(n / 2) {
343            c.swap(j, n - 1 - j);
344        }
345        c
346    }
347
348    /// Validate qubit indices, control/target distinctness, and `P(k)` range.
349    pub fn validate(&self) -> Result<(), QuantumError> {
350        let n = self.n_qubits;
351        for g in &self.ops {
352            if g.target >= n {
353                return Err(QuantumError::Invalid(format!("target {} ≥ n_qubits {}", g.target, n)));
354            }
355            for &c in &g.controls {
356                if c >= n {
357                    return Err(QuantumError::Invalid(format!("control {c} ≥ n_qubits {n}")));
358                }
359                if c == g.target {
360                    return Err(QuantumError::Invalid(format!("control {c} equals target")));
361                }
362            }
363            if g.base == BaseGate::P {
364                let k = g.param as usize;
365                if k < 1 || k > DYADIC_MAX {
366                    return Err(QuantumError::Invalid(format!("P(k) with k={k} out of 1..={DYADIC_MAX}")));
367                }
368            }
369        }
370        Ok(())
371    }
372
373    /// Classically simulate the circuit from `|0…0⟩`, returning the fixed-point
374    /// state vector. Pure i64: byte-identical on every machine.
375    pub fn simulate(&self) -> Result<StateVector, QuantumError> {
376        self.simulate_from(0)
377    }
378
379    /// Simulate starting from computational basis state `|start⟩`.
380    pub fn simulate_from(&self, start: usize) -> Result<StateVector, QuantumError> {
381        self.validate()?;
382        if self.n_qubits > MAX_QUBITS {
383            return Err(QuantumError::TooManyQubits(self.n_qubits));
384        }
385        let dim = 1usize << self.n_qubits;
386        let mut amps = vec![Amp::ZERO; dim];
387        amps[start % dim] = Amp::ONE;
388        for g in &self.ops {
389            apply(&mut amps, self.n_qubits, g);
390        }
391        Ok(StateVector { n_qubits: self.n_qubits, amps })
392    }
393}
394
395/// Apply one controlled-1-qubit gate in place. Iterates the amplitude pairs that
396/// differ only in the target bit, updating those whose control bits are all set.
397/// Apply one gate in place.
398///
399/// The obvious implementation walks every index and multiplies a 2x2 matrix into
400/// each pair. Most gates do not need that. `fxmul(ONE, x) == x` and
401/// `fxmul(0, x) == 0` hold exactly in this fixed point — `ROUND` is `1 << (FRAC-1)`,
402/// so the rounded product of an integer with one is that integer — which makes the
403/// shortcuts below **bit-identical to the general path by construction**, not merely
404/// close. Byte-exactness is the contract here; a faster simulator that changed a
405/// single amplitude would be a broken one.
406///
407/// - identity does nothing at all;
408/// - a **diagonal** gate (`Z S S† T T† P`, and their controlled forms) only phases
409///   the `|1⟩` half, so it touches half the amplitudes with one multiply instead of
410///   four plus two adds — and a phase of exactly `-1` is a negation, with none;
411/// - an **antidiagonal** gate is two multiplies, and `X` (hence `CX`, the most
412///   common two-qubit gate there is) is a pure swap with no arithmetic whatsoever;
413/// - an all-**real** matrix (`H`, and any real rotation) halves the multiplies,
414///   since the imaginary cross terms are multiplications by zero.
415///
416/// Measured at 20 qubits against the previous implementation, interleaved in one
417/// process: `Z` 8.9x, `X` 7.1x, `CZ` 4.6x, `CX` 4.3x, `T` 3.4x, `S` 3.1x, `Y` 1.9x,
418/// `H` 1.8x.
419///
420/// The loop shape is deliberately left alone. Blocking it, and indexing the pairs
421/// by bit-insertion, were both tried and both came out slower than the plain scan:
422/// the arithmetic is what costs here, not the iteration.
423fn apply(amps: &mut [Amp], n: u8, g: &Gate) {
424    if matches!(g.base, BaseGate::I) {
425        return;
426    }
427    let m = g.base.matrix(g.param);
428    let tbit = 1usize << g.target;
429    let ctrl_mask: usize = g.controls.iter().fold(0usize, |acc, &c| acc | (1usize << c));
430    let dim = 1usize << n;
431
432    let is_zero = |a: Amp| a.re == 0 && a.im == 0;
433    let is_one = |a: Amp| a.re == ONE && a.im == 0;
434
435    // diag(1, phase): the |0> half is untouched, so walk only the |1> half and do
436    // one multiply instead of four and two adds. A phase of exactly -1 negates.
437    if is_one(m[0][0]) && is_zero(m[0][1]) && is_zero(m[1][0]) {
438        let ph = m[1][1];
439        let negate = ph.re == -ONE && ph.im == 0;
440        let mut i = 0usize;
441        while i < dim {
442            if i & tbit != 0 && (i & ctrl_mask) == ctrl_mask {
443                amps[i] = if negate {
444                    Amp { re: -amps[i].re, im: -amps[i].im }
445                } else {
446                    ph.mul(amps[i])
447                };
448            }
449            i += 1;
450        }
451        return;
452    }
453
454    // antidiagonal: the halves exchange. X (so CX) is an exact swap, no arithmetic.
455    if is_zero(m[0][0]) && is_zero(m[1][1]) {
456        let (a01, a10) = (m[0][1], m[1][0]);
457        let plain_swap = is_one(a01) && is_one(a10);
458        let mut i = 0usize;
459        while i < dim {
460            if i & tbit == 0 && (i & ctrl_mask) == ctrl_mask {
461                let j = i | tbit;
462                if plain_swap {
463                    amps.swap(i, j);
464                } else {
465                    let (a0, a1) = (amps[i], amps[j]);
466                    amps[i] = a01.mul(a1);
467                    amps[j] = a10.mul(a0);
468                }
469            }
470            i += 1;
471        }
472        return;
473    }
474
475    // All four entries real — H, and any real rotation. A complex multiply by a
476    // real scalar is two fxmuls rather than four plus a subtract, because the
477    // imaginary cross terms are `fxmul(0, x)`, which is exactly zero. Same
478    // products, same order, half the multiplies.
479    if m[0][0].im == 0 && m[0][1].im == 0 && m[1][0].im == 0 && m[1][1].im == 0 {
480        let (p00, p01) = (m[0][0].re, m[0][1].re);
481        let (p10, p11) = (m[1][0].re, m[1][1].re);
482        let mut i = 0usize;
483        while i < dim {
484            if i & tbit == 0 && (i & ctrl_mask) == ctrl_mask {
485                let j = i | tbit;
486                let a0 = amps[i];
487                let a1 = amps[j];
488                amps[i] = Amp {
489                    re: fxmul(p00, a0.re) + fxmul(p01, a1.re),
490                    im: fxmul(p00, a0.im) + fxmul(p01, a1.im),
491                };
492                amps[j] = Amp {
493                    re: fxmul(p10, a0.re) + fxmul(p11, a1.re),
494                    im: fxmul(p10, a0.im) + fxmul(p11, a1.im),
495                };
496            }
497            i += 1;
498        }
499        return;
500    }
501
502    let mut i = 0usize;
503    while i < dim {
504        if i & tbit == 0 && (i & ctrl_mask) == ctrl_mask {
505            let j = i | tbit;
506            let a0 = amps[i];
507            let a1 = amps[j];
508            amps[i] = m[0][0].mul(a0).add(m[0][1].mul(a1));
509            amps[j] = m[1][0].mul(a0).add(m[1][1].mul(a1));
510        }
511        i += 1;
512    }
513}
514
515// ---------------------------------------------------------------------------
516// State vector + conformance
517// ---------------------------------------------------------------------------
518
519/// A reconstructed fixed-point state vector — the hashed quantity.
520#[derive(Clone, Debug, PartialEq, Eq)]
521pub struct StateVector {
522    pub n_qubits: u8,
523    pub amps: Vec<Amp>,
524}
525
526/// `splitmix64` — the pinned integer PRNG shared with the WAI deterministic generator; the
527/// fixed-point sampler the shot-histogram determinism requires.
528fn splitmix64(s: &mut u64) -> u64 {
529    *s = s.wrapping_add(0x9E37_79B9_7F4A_7C15);
530    let mut z = *s;
531    z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
532    z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
533    z ^ (z >> 31)
534}
535
536impl StateVector {
537    /// The `|k⟩` computational basis state on `n` qubits.
538    pub fn basis(n: u8, k: usize) -> StateVector {
539        let dim = 1usize << n;
540        let mut amps = vec![Amp::ZERO; dim];
541        amps[k % dim] = Amp::ONE;
542        StateVector { n_qubits: n, amps }
543    }
544
545    /// Canonical serialization of the amplitudes: `re_le(i64) ‖ im_le(i64)` per
546    /// amplitude, in index order — the bytes the statevector hash covers.
547    pub fn canonical_bytes(&self) -> Vec<u8> {
548        let mut out = Vec::with_capacity(self.amps.len() * 16);
549        for a in &self.amps {
550            out.extend_from_slice(&a.re.to_le_bytes());
551            out.extend_from_slice(&a.im.to_le_bytes());
552        }
553        out
554    }
555
556    /// **statevector-equivalence** — the portable BLAKE3 identity of the whole
557    /// reconstructed vector. Two conforming sinks agree on this iff every amplitude
558    /// is byte-identical, on every machine, with no tolerance parameter.
559    pub fn statevector_hash(&self) -> [u8; 32] {
560        let mut h = blake3::Hasher::new();
561        h.update(DOMAIN_STATEVECTOR);
562        h.update(&[self.n_qubits]);
563        h.update(&FRAC.to_le_bytes());
564        h.update(&self.canonical_bytes());
565        *h.finalize().as_bytes()
566    }
567
568    /// Per-basis-state probability weights `|amp|^2` at scale `2^(2·FRAC)`, `i128`
569    /// (un-normalized; summing gives ≈ `2^(2·FRAC)`). No divide — the sampler walks
570    /// these directly.
571    pub fn prob_weights(&self) -> Vec<i128> {
572        self.amps.iter().map(|a| a.norm2()).collect()
573    }
574
575    /// Sample `shots` computational-basis measurements with the pinned
576    /// `splitmix64` seeded by `seed`; returns a per-basis-state count vector.
577    /// Deterministic on every machine.
578    pub fn sample_shots(&self, seed: u64, shots: u64) -> Vec<u64> {
579        let weights = self.prob_weights();
580        // prefix sums in i128; total ≈ 2^(2·FRAC)
581        let mut cum = Vec::with_capacity(weights.len());
582        let mut total: i128 = 0;
583        for w in &weights {
584            total += *w;
585            cum.push(total);
586        }
587        let mut counts = vec![0u64; self.amps.len()];
588        if total <= 0 {
589            return counts;
590        }
591        let mut state = seed;
592        for _ in 0..shots {
593            let r = (splitmix64(&mut state) as u128 % total as u128) as i128;
594            // first index whose prefix sum strictly exceeds r
595            let idx = match cum.binary_search_by(|c| {
596                if *c <= r { std::cmp::Ordering::Less } else { std::cmp::Ordering::Greater }
597            }) {
598                Ok(i) | Err(i) => i,
599            };
600            let slot = idx.min(counts.len() - 1);
601            counts[slot] += 1;
602        }
603        counts
604    }
605
606    /// **shot-histogram-equivalence** — the portable BLAKE3 identity of a shot
607    /// histogram at a pinned `(seed, shots)`.
608    pub fn histogram_hash(&self, seed: u64, shots: u64) -> [u8; 32] {
609        let counts = self.sample_shots(seed, shots);
610        let mut h = blake3::Hasher::new();
611        h.update(DOMAIN_HISTOGRAM);
612        h.update(&[self.n_qubits]);
613        h.update(&seed.to_le_bytes());
614        h.update(&shots.to_le_bytes());
615        for c in &counts {
616            h.update(&c.to_le_bytes());
617        }
618        *h.finalize().as_bytes()
619    }
620
621    /// State **fidelity** `|⟨self|other⟩|^2` in the fixed-point floor (result at
622    /// `2^FRAC`) — the quantum-information-theoretic similarity metric. For two
623    /// exactly-reconstructed vectors it is a *diagnostic* (identical states give
624    /// ≈ `ONE`); it is the metric a future float/behavioral quantum prior — which
625    /// cannot back `intent=replicate` — would conform against. `i128` accumulation
626    /// so the overlap sum never overflows.
627    pub fn fidelity_fx(&self, other: &StateVector) -> i64 {
628        assert_eq!(self.amps.len(), other.amps.len(), "fidelity needs equal dimension");
629        let mut re: i128 = 0;
630        let mut im: i128 = 0;
631        for (a, b) in self.amps.iter().zip(&other.amps) {
632            // ⟨a|b⟩ = Σ conj(a)·b
633            let p = a.conj().mul(*b);
634            re += p.re as i128;
635            im += p.im as i128;
636        }
637        // |overlap|^2 at 2^FRAC: (re^2 + im^2) >> FRAC, re/im already at 2^FRAC
638        ((re * re + im * im) >> FRAC) as i64
639    }
640}
641
642// ---------------------------------------------------------------------------
643// WQC container (magic "WQC1"): the on-wire circuit object
644// ---------------------------------------------------------------------------
645
646/// A pinned measurement request carried in a `WQC` (section `0x03`), so a file can
647/// declare "conform on the shot histogram at this `(seed, shots)`".
648#[derive(Clone, Copy, Debug, PartialEq, Eq)]
649pub struct Measure {
650    pub seed: u64,
651    pub shots: u64,
652}
653
654const SECT_CONTRACT: u8 = 0x01;
655const SECT_OPLOG: u8 = 0x02;
656const SECT_MEASURE: u8 = 0x03;
657
658fn contract_json(n_qubits: u8) -> Vec<u8> {
659    // canonical, fixed field order (serde_json preserve_order is on for the crate)
660    let v = serde_json::json!({
661        "ext": "wai.quantum.circuit/1",
662        "n_qubits": n_qubits,
663        "numeric": "wai.det.fixed64",
664        "frac": FRAC,
665        "gateset": "cliffordT+dyadicP",
666    });
667    serde_json::to_vec(&v).expect("contract json")
668}
669
670fn oplog_bytes(c: &Circuit) -> Vec<u8> {
671    // u32 n_ops || per op: u8 opcode | u8 n_ctrl | u8 target | u16 param | ctrls...
672    let mut out = Vec::new();
673    out.extend_from_slice(&(c.ops.len() as u32).to_le_bytes());
674    for g in &c.ops {
675        out.push(g.base.opcode());
676        out.push(g.controls.len() as u8);
677        out.push(g.target);
678        out.extend_from_slice(&g.param.to_le_bytes());
679        out.extend_from_slice(&g.controls);
680    }
681    out
682}
683
684fn parse_oplog(n_qubits: u8, b: &[u8]) -> Result<Vec<Gate>, QuantumError> {
685    if b.len() < 4 {
686        return Err(QuantumError::Malformed("oplog header".into()));
687    }
688    let n_ops = u32::from_le_bytes([b[0], b[1], b[2], b[3]]) as usize;
689    let mut pos = 4;
690    let mut ops = Vec::with_capacity(n_ops);
691    for _ in 0..n_ops {
692        if pos + 5 > b.len() {
693            return Err(QuantumError::Malformed("op header".into()));
694        }
695        let base = BaseGate::from_opcode(b[pos])
696            .ok_or_else(|| QuantumError::Malformed(format!("opcode {}", b[pos])))?;
697        let n_ctrl = b[pos + 1] as usize;
698        let target = b[pos + 2];
699        let param = u16::from_le_bytes([b[pos + 3], b[pos + 4]]);
700        pos += 5;
701        if pos + n_ctrl > b.len() {
702            return Err(QuantumError::Malformed("op controls".into()));
703        }
704        let controls = b[pos..pos + n_ctrl].to_vec();
705        pos += n_ctrl;
706        ops.push(Gate { base, controls, target, param });
707    }
708    let _ = n_qubits;
709    Ok(ops)
710}
711
712/// Serialize a circuit (and optional measurement) to a `WQC1` container.
713pub fn to_wqc(c: &Circuit, measure: Option<Measure>) -> Vec<u8> {
714    let contract = contract_json(c.n_qubits);
715    let oplog = oplog_bytes(c);
716    let measure_bytes = measure.map(|m| {
717        let v = serde_json::json!({ "seed": m.seed, "shots": m.shots, "basis": "computational" });
718        serde_json::to_vec(&v).expect("measure json")
719    });
720
721    let mut sections: Vec<(u8, Vec<u8>)> = vec![(SECT_CONTRACT, contract), (SECT_OPLOG, oplog)];
722    if let Some(mb) = measure_bytes {
723        sections.push((SECT_MEASURE, mb));
724    }
725
726    let mut out = Vec::new();
727    out.extend_from_slice(b"WQC1");
728    out.extend_from_slice(&(sections.len() as u16).to_le_bytes());
729    // section table: u8 kind, u32 off (relative to start of the sections blob), u32 len
730    let mut off: u32 = 0;
731    for (kind, data) in &sections {
732        out.push(*kind);
733        out.extend_from_slice(&off.to_le_bytes());
734        out.extend_from_slice(&(data.len() as u32).to_le_bytes());
735        off += data.len() as u32;
736    }
737    for (_, data) in &sections {
738        out.extend_from_slice(data);
739    }
740    out
741}
742
743/// Parse a `WQC1` container back into a circuit and optional measurement.
744pub fn from_wqc(bytes: &[u8]) -> Result<(Circuit, Option<Measure>), QuantumError> {
745    if bytes.len() < 6 || &bytes[0..4] != b"WQC1" {
746        return Err(QuantumError::Malformed("magic".into()));
747    }
748    let n_sections = u16::from_le_bytes([bytes[4], bytes[5]]) as usize;
749    let table_start = 6;
750    let table_len = n_sections * 9;
751    if bytes.len() < table_start + table_len {
752        return Err(QuantumError::Malformed("section table".into()));
753    }
754    let blob_start = table_start + table_len;
755    let mut contract: Option<&[u8]> = None;
756    let mut oplog: Option<&[u8]> = None;
757    let mut measure_raw: Option<&[u8]> = None;
758    for s in 0..n_sections {
759        let p = table_start + s * 9;
760        let kind = bytes[p];
761        let off = u32::from_le_bytes([bytes[p + 1], bytes[p + 2], bytes[p + 3], bytes[p + 4]]) as usize;
762        let len = u32::from_le_bytes([bytes[p + 5], bytes[p + 6], bytes[p + 7], bytes[p + 8]]) as usize;
763        let start = blob_start + off;
764        if start + len > bytes.len() {
765            return Err(QuantumError::Malformed("section bounds".into()));
766        }
767        let data = &bytes[start..start + len];
768        match kind {
769            SECT_CONTRACT => contract = Some(data),
770            SECT_OPLOG => oplog = Some(data),
771            SECT_MEASURE => measure_raw = Some(data),
772            _ => {} // unknown section: ignore (forward-compatible)
773        }
774    }
775    let contract = contract.ok_or_else(|| QuantumError::Malformed("missing contract".into()))?;
776    let oplog = oplog.ok_or_else(|| QuantumError::Malformed("missing oplog".into()))?;
777
778    let cv: serde_json::Value =
779        serde_json::from_slice(contract).map_err(|e| QuantumError::Malformed(format!("contract json: {e}")))?;
780    let n_qubits = cv["n_qubits"].as_u64().ok_or_else(|| QuantumError::Malformed("n_qubits".into()))? as u8;
781
782    let ops = parse_oplog(n_qubits, oplog)?;
783    let circuit = Circuit { n_qubits, ops };
784    circuit.validate()?;
785
786    let measure = match measure_raw {
787        Some(mb) => {
788            let mv: serde_json::Value =
789                serde_json::from_slice(mb).map_err(|e| QuantumError::Malformed(format!("measure json: {e}")))?;
790            Some(Measure {
791                seed: mv["seed"].as_u64().unwrap_or(0),
792                shots: mv["shots"].as_u64().unwrap_or(0),
793            })
794        }
795        None => None,
796    };
797    Ok((circuit, measure))
798}
799
800/// **circuit-equivalence** — the portable BLAKE3 identity of the circuit itself
801/// (contract + op-log). The receipt binds this alongside the statevector hash.
802pub fn circuit_hash(c: &Circuit) -> [u8; 32] {
803    let mut h = blake3::Hasher::new();
804    h.update(DOMAIN_CIRCUIT);
805    h.update(&contract_json(c.n_qubits));
806    h.update(&oplog_bytes(c));
807    *h.finalize().as_bytes()
808}
809
810#[cfg(test)]
811mod tests {
812    use super::*;
813    /// The pre-optimisation `apply`, kept verbatim as an oracle. The fast paths
814    /// are justified by an argument (`fxmul(ONE,x) == x`, `fxmul(0,x) == 0`), and
815    /// an argument about fixed-point rounding is exactly the kind that is
816    /// convincing and wrong, so it is checked against the code it replaced.
817    fn apply_reference(amps: &mut [Amp], n: u8, g: &Gate) {
818        let m = g.base.matrix(g.param);
819        let tbit = 1usize << g.target;
820        let ctrl_mask: usize = g.controls.iter().fold(0usize, |acc, &c| acc | (1usize << c));
821        let dim = 1usize << n;
822        let mut i = 0usize;
823        while i < dim {
824            if i & tbit == 0 && (i & ctrl_mask) == ctrl_mask {
825                let j = i | tbit;
826                let a0 = amps[i];
827                let a1 = amps[j];
828                amps[i] = m[0][0].mul(a0).add(m[0][1].mul(a1));
829                amps[j] = m[1][0].mul(a0).add(m[1][1].mul(a1));
830            }
831            i += 1;
832        }
833    }
834
835
836    /// Optimised `apply` against the pre-optimisation oracle, in one process,
837    /// interleaved, best-of-N. Comparing separate runs on a busy machine produced
838    /// swings big enough to invent speedups and regressions that were not there —
839    /// including an apparent slowdown in a code path that had not changed.
840    #[test]
841    #[ignore]
842    fn probe_apply_speedup() {
843        use std::time::Instant;
844        let n = 20u8;
845        let dim = 1usize << n;
846        let reps = 5;
847        let gates = 60;
848        println!("\\n  n={n}  best of {reps}, interleaved");
849        println!("  gate   reference     optimised     speedup");
850        for kind in ["h", "t", "z", "s", "x", "cx", "cz", "y"] {
851            let mut ops: Vec<Gate> = Vec::new();
852            for i in 0..gates {
853                let q = (i % n as usize) as u8;
854                let o = (q + 1) % n;
855                ops.push(match kind {
856                    "h" => Gate { base: BaseGate::H, controls: vec![], target: q, param: 0 },
857                    "t" => Gate { base: BaseGate::T, controls: vec![], target: q, param: 0 },
858                    "z" => Gate { base: BaseGate::Z, controls: vec![], target: q, param: 0 },
859                    "s" => Gate { base: BaseGate::S, controls: vec![], target: q, param: 0 },
860                    "x" => Gate { base: BaseGate::X, controls: vec![], target: q, param: 0 },
861                    "y" => Gate { base: BaseGate::Y, controls: vec![], target: q, param: 0 },
862                    "cx" => Gate { base: BaseGate::X, controls: vec![o], target: q, param: 0 },
863                    _ => Gate { base: BaseGate::Z, controls: vec![o], target: q, param: 0 },
864                });
865            }
866            let (mut best_ref, mut best_opt) = (u128::MAX, u128::MAX);
867            for _ in 0..reps {
868                let mut a = vec![Amp::ZERO; dim];
869                a[0] = Amp::ONE;
870                let t0 = Instant::now();
871                for g in &ops { apply_reference(&mut a, n, g); }
872                best_ref = best_ref.min(t0.elapsed().as_nanos());
873                std::hint::black_box(&a);
874
875                let mut b = vec![Amp::ZERO; dim];
876                b[0] = Amp::ONE;
877                let t1 = Instant::now();
878                for g in &ops { apply(&mut b, n, g); }
879                best_opt = best_opt.min(t1.elapsed().as_nanos());
880                std::hint::black_box(&b);
881            }
882            let (r, o) = (best_ref as f64 / gates as f64, best_opt as f64 / gates as f64);
883            println!("  {kind:<5} {r:>10.0} ns {o:>10.0} ns {:>9.2}x", r / o);
884        }
885    }
886
887    /// Every amplitude, not just the hash: random circuits over the whole gate set
888    /// must come out of the optimised path bit-for-bit as they came out of the old
889    /// one. A faster simulator that moved one low bit would have broken every
890    /// receipt this crate has ever signed.
891    #[test]
892    fn optimised_apply_is_bit_identical_to_the_general_path() {
893        let mut st = 0x1234_5678_9ABC_DEF0u64;
894        let mut rnd = |m: usize| {
895            st = st.wrapping_mul(6364136223846793005).wrapping_add(1442695040888963407);
896            ((st >> 33) as usize) % m.max(1)
897        };
898        for _ in 0..300 {
899            let n = 3 + rnd(3) as u8;
900            let dim = 1usize << n;
901            let mut c = Circuit::new(n);
902            for _ in 0..24 {
903                let t = rnd(n as usize) as u8;
904                let mut other = rnd(n as usize) as u8;
905                if other == t {
906                    other = (t + 1) % n;
907                }
908                match rnd(13) {
909                    0 => { c.x(t); }
910                    1 => { c.y(t); }
911                    2 => { c.z(t); }
912                    3 => { c.h(t); }
913                    4 => { c.s(t); }
914                    5 => { c.t(t); }
915                    6 => { c.p(1 + rnd(5) as u16, t); }
916                    7 => { c.cx(other, t); }
917                    8 => { c.cz(other, t); }
918                    9 => { c.cp(1 + rnd(4) as u16, other, t); }
919                    10 => c.ops.push(Gate { base: BaseGate::Sdg, controls: vec![], target: t, param: 0 }),
920                    11 => c.ops.push(Gate { base: BaseGate::Tdg, controls: vec![], target: t, param: 0 }),
921                    _ => c.ops.push(Gate { base: BaseGate::I, controls: vec![], target: t, param: 0 }),
922                }
923            }
924            // controlled-Y and Toffoli too, which exercise the antidiagonal and
925            // multi-control paths
926            if n >= 3 {
927                c.ops.push(Gate { base: BaseGate::Y, controls: vec![0], target: n - 1, param: 0 });
928                c.ccx(0, 1, n - 1);
929            }
930
931            let mut want = vec![Amp::ZERO; dim];
932            want[0] = Amp::ONE;
933            for g in &c.ops {
934                apply_reference(&mut want, n, g);
935            }
936            let got = c.simulate().unwrap();
937            assert_eq!(got.amps, want, "optimised apply diverged on {} gates, n={n}", c.ops.len());
938        }
939    }
940
941
942    /// Tolerance for "≈ 1.0" checks — fixed-point rounding accumulates a little,
943    /// but determinism (not accuracy) is the contract. ~1e-4 of ONE.
944    const TOL: i64 = ONE / 10_000;
945
946    fn approx(a: i64, b: i64, tol: i64) -> bool {
947        (a - b).abs() <= tol
948    }
949
950    #[test]
951    fn dyadic_table_is_unit_modulus() {
952        // cos^2 + sin^2 ≈ 1 for every tabulated phase, and P(1)=Z, P(2)=S, P(3)=T.
953        for k in 1..=DYADIC_MAX {
954            let a = phase(k);
955            let n2 = ((a.norm2()) >> FRAC) as i64; // at 2^FRAC
956            assert!(approx(n2, ONE, ONE / 1000), "|e^(2πi/2^{k})|^2 = {n2} not ≈ ONE");
957        }
958        assert_eq!(phase(1), Amp { re: -ONE, im: 0 }); // e^{iπ}
959        assert_eq!(phase(2), Amp { re: 0, im: ONE }); // e^{iπ/2}
960        // T = e^{iπ/4}: re == im == 1/√2
961        assert_eq!(phase(3).re, inv_sqrt2());
962        assert_eq!(phase(3).im, inv_sqrt2());
963    }
964
965    #[test]
966    fn simulation_is_deterministic_byte_for_byte() {
967        // The core claim: same circuit → identical amplitudes, twice.
968        let c = Circuit::qft(6);
969        let a = c.simulate().unwrap();
970        let b = c.simulate().unwrap();
971        assert_eq!(a.amps, b.amps, "quantum sim must be byte-exact reproducible");
972        assert_eq!(a.statevector_hash(), b.statevector_hash());
973    }
974
975    #[test]
976    fn bell_state() {
977        // H(0); CX(0,1) → (|00⟩ + |11⟩)/√2
978        let mut c = Circuit::new(2);
979        c.h(0).cx(0, 1);
980        let sv = c.simulate().unwrap();
981        let s = inv_sqrt2();
982        assert!(approx(sv.amps[0b00].re, s, TOL));
983        assert_eq!(sv.amps[0b01], Amp::ZERO);
984        assert_eq!(sv.amps[0b10], Amp::ZERO);
985        assert!(approx(sv.amps[0b11].re, s, TOL));
986        // fidelity with itself ≈ 1
987        assert!(approx(sv.fidelity_fx(&sv), ONE, TOL));
988    }
989
990    #[test]
991    fn ghz_state() {
992        // H(0); CX(0,1); CX(1,2) → (|000⟩ + |111⟩)/√2
993        let mut c = Circuit::new(3);
994        c.h(0).cx(0, 1).cx(1, 2);
995        let sv = c.simulate().unwrap();
996        let s = inv_sqrt2();
997        assert!(approx(sv.amps[0b000].re, s, TOL));
998        assert!(approx(sv.amps[0b111].re, s, TOL));
999        for k in 1..7 {
1000            assert_eq!(sv.amps[k], Amp::ZERO, "index {k} should be empty");
1001        }
1002    }
1003
1004    #[test]
1005    fn qft_of_zero_is_uniform_superposition() {
1006        // QFT|0…0⟩ = (1/√N) Σ|k⟩ — every amplitude has magnitude 1/√N.
1007        let n = 4u8;
1008        let dim = 1usize << n;
1009        let sv = Circuit::qft(n).simulate().unwrap();
1010        // |amp|^2 = 1/N; norm2() is at 2^(2·FRAC), so (norm2 >> FRAC) is at 2^FRAC,
1011        // i.e. the expected value is ONE/N.
1012        let want2 = ONE / dim as i64;
1013        for k in 0..dim {
1014            let mag2 = ((sv.amps[k].norm2()) >> FRAC) as i64;
1015            assert!(approx(mag2, want2, ONE / 1000), "amp {k} magnitude^2 {mag2} != {want2}");
1016        }
1017    }
1018
1019    #[test]
1020    fn orthogonal_states_have_zero_fidelity() {
1021        let a = StateVector::basis(3, 0b000);
1022        let b = StateVector::basis(3, 0b111);
1023        assert_eq!(a.fidelity_fx(&b), 0);
1024        assert!(approx(a.fidelity_fx(&a), ONE, 1));
1025    }
1026
1027    #[test]
1028    fn shot_histogram_is_deterministic() {
1029        // Bell state: shots concentrate on |00⟩ and |11⟩, reproducibly.
1030        let mut c = Circuit::new(2);
1031        c.h(0).cx(0, 1);
1032        let sv = c.simulate().unwrap();
1033        let h1 = sv.histogram_hash(0xC0FFEE, 10_000);
1034        let h2 = sv.histogram_hash(0xC0FFEE, 10_000);
1035        assert_eq!(h1, h2, "pinned-seed histogram must be reproducible");
1036        let counts = sv.sample_shots(0xC0FFEE, 10_000);
1037        assert_eq!(counts[0b01], 0);
1038        assert_eq!(counts[0b10], 0);
1039        assert!(counts[0b00] > 4000 && counts[0b00] < 6000, "≈ half on |00⟩: {}", counts[0b00]);
1040        assert_eq!(counts[0b00] + counts[0b11], 10_000);
1041    }
1042
1043    #[test]
1044    fn wqc_round_trips() {
1045        let c = Circuit::qft(5);
1046        let m = Some(Measure { seed: 42, shots: 1024 });
1047        let bytes = to_wqc(&c, m);
1048        assert_eq!(&bytes[0..4], b"WQC1");
1049        let (c2, m2) = from_wqc(&bytes).unwrap();
1050        assert_eq!(c, c2, "circuit must round-trip through WQC");
1051        assert_eq!(m, m2);
1052        // and the reconstructed circuit hashes + simulates identically
1053        assert_eq!(circuit_hash(&c), circuit_hash(&c2));
1054        assert_eq!(c.simulate().unwrap().amps, c2.simulate().unwrap().amps);
1055    }
1056
1057
1058    #[test]
1059    fn validation_rejects_bad_indices() {
1060        let mut c = Circuit::new(2);
1061        c.push(BaseGate::X, vec![], 5, 0); // target out of range
1062        assert!(matches!(c.validate(), Err(QuantumError::Invalid(_))));
1063
1064        let mut c = Circuit::new(2);
1065        c.push(BaseGate::X, vec![1], 1, 0); // control == target
1066        assert!(matches!(c.validate(), Err(QuantumError::Invalid(_))));
1067
1068        let mut c = Circuit::new(1);
1069        c.push(BaseGate::P, vec![], 0, 999); // P(k) out of dyadic range
1070        assert!(matches!(c.validate(), Err(QuantumError::Invalid(_))));
1071    }
1072}