From 808c30f79600832374fc303d1c36565c8c6910d3 Mon Sep 17 00:00:00 2001 From: Transaction Science Date: Fri, 9 Oct 2026 10:17:04 -0400 Subject: [PATCH 01/13] wai-quantum circuit2: the two phase tables and the Gateset type MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PHASE_V1 and PHASE_V2 are literal [Amp; 33] constants. PHASE_V1 holds the values of the circuit/1 half-angle recurrence, unchanged, and PHASE_V2 holds the integer nearest to 2^30·cos(2π/2^k) and 2^30·sin(2π/2^k) for k = 1..32. The OnceLock and the runtime recurrence are gone; the recurrence now lives in a test that checks it against PHASE_V1. Gateset { V1, #[default] V2 } is non_exhaustive and names, per variant, the capability, magic, contract ext, numeric identifier, gateset string, table, phase(k), inv_sqrt2, canonical contract bytes (rendered with format!) and a BLAKE3 table digest, with from_magic and from_capability as inverses. The free phase() and inv_sqrt2() still return the circuit/1 values, now read from Gateset::V1, so every existing circuit, hash and pin is unchanged. Their removal comes with the Circuit gateset field. New tests: phase_table_v1_is_frozen (recurrence, digest 3fa9805f…8466, the P(19..32) defect, inv_sqrt2 759250124, the dyadic_ladder statevector hash 9754af24…851d); phase_table_v2_is_certified_nearest (u128 interval enclosures, both ends round alike and sit more than 2^20 ulps from a tie, digest d34cb793…3510, inv_sqrt2 759250125); every_v2_phase_is_within_one_unit_of_ideal (against repro::sin_cos, with circuit/1 failing the same check for every k >= 8 as the negative control); table_identities_per_gateset, which replaces dyadic_table_is_unit_modulus; and gateset_identifiers_are_consistent, which checks the identifiers round-trip and that the V1 contract bytes equal the serializer render for every n_qubits. Co-Authored-By: Claude Opus 5.5 --- wai/wai-quantum/src/quantum.rs | 479 ++++++++++++++++++++++++++++++--- 1 file changed, 438 insertions(+), 41 deletions(-) diff --git a/wai/wai-quantum/src/quantum.rs b/wai/wai-quantum/src/quantum.rs index 3afc0e06c..845e61782 100644 --- a/wai/wai-quantum/src/quantum.rs +++ b/wai/wai-quantum/src/quantum.rs @@ -43,8 +43,6 @@ //! this exact-reconstruction extension. [`StateVector::fidelity_fx`] is provided as the //! quantum-information-theoretic conformance metric such a future path would use. -use std::sync::OnceLock; - /// Fractional bits of the fixed-point amplitude floor. Amplitudes live in /// `[-1, 1]`, so `2^FRAC` (≈ `1.07e9` at 30) is the unit and the i128 products in /// [`Amp::mul`] stay far inside range. @@ -52,8 +50,8 @@ pub const FRAC: u32 = 30; /// The fixed-point unit `1.0`. pub const ONE: i64 = 1 << FRAC; const ROUND: i128 = 1 << (FRAC - 1); -/// Largest `k` for which `P(k) = e^{2πi/2^k}` is tabulated — the QFT reaches this -/// many qubits. The table is built by an integer half-angle recurrence (no float). +/// Largest `k` for which `P(k) = e^{2πi/2^k}` is tabulated: [`PHASE_V1`] and +/// [`PHASE_V2`] each hold `P(1..=DYADIC_MAX)`. pub const DYADIC_MAX: usize = 32; /// Domain separators for the conformance hashes (the WAI discipline). @@ -131,41 +129,245 @@ impl Amp { } // --------------------------------------------------------------------------- -// Pinned dyadic phase table — e^{2πi / 2^k}, built by integer half-angle -// recurrence from (cos π/2, sin π/2) = (0, 1). No float ever runs. +// Dyadic phase tables — P(k) = e^{2πi / 2^k} at scale 2^FRAC, one per gateset. +// Literal constants: no float and no recurrence runs on the decode path. // --------------------------------------------------------------------------- -fn dyadic_table() -> &'static [Amp] { - static TABLE: OnceLock> = OnceLock::new(); - TABLE.get_or_init(|| { - let mut t = vec![Amp::ZERO; DYADIC_MAX + 1]; - // k = 1: e^{iπ} = -1 + 0i - t[1] = Amp { re: -ONE, im: 0 }; - // k = 2: e^{iπ/2} = 0 + 1i - t[2] = Amp { re: 0, im: ONE }; - // k ≥ 3: half the angle each step. For θ ∈ (0, π/2], both cos and sin are - // non-negative, so the positive integer sqrt is the right branch: - // cos(θ/2) = sqrt((1+cosθ)/2), sin(θ/2) = sqrt((1−cosθ)/2) - for k in 3..=DYADIC_MAX { - let c = t[k - 1].re; - t[k] = Amp { - re: sqrt_fx((ONE + c) / 2), - im: sqrt_fx((ONE - c) / 2), - }; +/// The `wai.quantum.circuit` phase table, frozen bit for bit. +/// +/// Index `k` holds `P(k) = e^{2πi/2^k}` as `(re, im)` at scale `2^FRAC`, for +/// `1 ≤ k ≤ DYADIC_MAX`; index 0 is unused and holds [`Amp::ZERO`]. +/// +/// These are the values of the integer half-angle recurrence every circuit/1 +/// artefact was made with: `P(1) = (−2^30, 0)`, `P(2) = (0, 2^30)`, and for +/// `k ≥ 3`, with `c = P(k−1).re`, +/// `re = isqrt(⌊(2^30 + c)/2⌋·2^30)` and `im = isqrt(⌊(2^30 − c)/2⌋·2^30)`, +/// where `isqrt` is the floor square root. The floors compound: from `P(8)` on +/// the imaginary part is too large, by up to 7032 units at `P(18)`, and +/// `P(19..=32)` apply no phase at all. The table is kept exactly as it is so that +/// every circuit/1 file, hash and receipt reproduces. [`PHASE_V2`] is the +/// correctly rounded table. +pub const PHASE_V1: [Amp; 33] = [ + Amp::ZERO, + Amp { re: -1073741824, im: 0 }, // k = 1 + Amp { re: 0, im: 1073741824 }, // k = 2 + Amp { re: 759250124, im: 759250124 }, // k = 3 + Amp { re: 992008094, im: 410903207 }, // k = 4 + Amp { re: 1053110175, im: 209476638 }, // k = 5 + Amp { re: 1068571463, im: 105245102 }, // k = 6 + Amp { re: 1072448454, im: 52686012 }, // k = 7 + Amp { re: 1073418432, im: 26350953 }, // k = 8 + Amp { re: 1073660972, im: 13176485 }, // k = 9 + Amp { re: 1073721610, im: 6588405 }, // k = 10 + Amp { re: 1073736770, im: 3294284 }, // k = 11 + Amp { re: 1073740560, im: 1647223 }, // k = 12 + Amp { re: 1073741507, im: 823774 }, // k = 13 + Amp { re: 1073741744, im: 411887 }, // k = 14 + Amp { re: 1073741803, im: 207243 }, // k = 15 + Amp { re: 1073741818, im: 103621 }, // k = 16 + Amp { re: 1073741822, im: 56755 }, // k = 17 + Amp { re: 1073741823, im: 32768 }, // k = 18 + Amp { re: 1073741823, im: 0 }, // k = 19 + Amp { re: 1073741823, im: 0 }, // k = 20 + Amp { re: 1073741823, im: 0 }, // k = 21 + Amp { re: 1073741823, im: 0 }, // k = 22 + Amp { re: 1073741823, im: 0 }, // k = 23 + Amp { re: 1073741823, im: 0 }, // k = 24 + Amp { re: 1073741823, im: 0 }, // k = 25 + Amp { re: 1073741823, im: 0 }, // k = 26 + Amp { re: 1073741823, im: 0 }, // k = 27 + Amp { re: 1073741823, im: 0 }, // k = 28 + Amp { re: 1073741823, im: 0 }, // k = 29 + Amp { re: 1073741823, im: 0 }, // k = 30 + Amp { re: 1073741823, im: 0 }, // k = 31 + Amp { re: 1073741823, im: 0 }, // k = 32 +]; + +/// The `wai.quantum.circuit2` phase table: each entry is the integer nearest to +/// `2^30·cos(2π/2^k)` and to `2^30·sin(2π/2^k)`. +/// +/// Index `k` holds `P(k)` as `(re, im)` at scale `2^FRAC`, for +/// `1 ≤ k ≤ DYADIC_MAX`; index 0 is unused and holds [`Amp::ZERO`]. `P(1)` and +/// `P(2)` are exact. For `k ≥ 3` every true value is irrational, so none is a +/// half-integer and the nearest integer is unique; the closest any comes to a +/// tie is 0.00456 unit, at the imaginary part of `P(22)`. Every component is +/// within ½ unit of the ideal, which the crate's tests prove with integer +/// interval arithmetic. The literal values are normative. +/// +/// `re P(3) == im P(3)`, so `T` keeps `re == im`. The resolution of `2^-30` is a +/// floor no rounding removes: `P(26..=32)` carry imaginary parts of 101 down to +/// 2, so the smallest angles are correct to the nearest unit and no further. +pub const PHASE_V2: [Amp; 33] = [ + Amp::ZERO, + Amp { re: -1073741824, im: 0 }, // k = 1 + Amp { re: 0, im: 1073741824 }, // k = 2 + Amp { re: 759250125, im: 759250125 }, // k = 3 + Amp { re: 992008094, im: 410903207 }, // k = 4 + Amp { re: 1053110176, im: 209476638 }, // k = 5 + Amp { re: 1068571464, im: 105245103 }, // k = 6 + Amp { re: 1072448455, im: 52686014 }, // k = 7 + Amp { re: 1073418433, im: 26350943 }, // k = 8 + Amp { re: 1073660973, im: 13176464 }, // k = 9 + Amp { re: 1073721611, im: 6588356 }, // k = 10 + Amp { re: 1073736771, im: 3294193 }, // k = 11 + Amp { re: 1073740561, im: 1647099 }, // k = 12 + Amp { re: 1073741508, im: 823550 }, // k = 13 + Amp { re: 1073741745, im: 411775 }, // k = 14 + Amp { re: 1073741804, im: 205887 }, // k = 15 + Amp { re: 1073741819, im: 102944 }, // k = 16 + Amp { re: 1073741823, im: 51472 }, // k = 17 + Amp { re: 1073741824, im: 25736 }, // k = 18 + Amp { re: 1073741824, im: 12868 }, // k = 19 + Amp { re: 1073741824, im: 6434 }, // k = 20 + Amp { re: 1073741824, im: 3217 }, // k = 21 + Amp { re: 1073741824, im: 1608 }, // k = 22 + Amp { re: 1073741824, im: 804 }, // k = 23 + Amp { re: 1073741824, im: 402 }, // k = 24 + Amp { re: 1073741824, im: 201 }, // k = 25 + Amp { re: 1073741824, im: 101 }, // k = 26 + Amp { re: 1073741824, im: 50 }, // k = 27 + Amp { re: 1073741824, im: 25 }, // k = 28 + Amp { re: 1073741824, im: 13 }, // k = 29 + Amp { re: 1073741824, im: 6 }, // k = 30 + Amp { re: 1073741824, im: 3 }, // k = 31 + Amp { re: 1073741824, im: 2 }, // k = 32 +]; + +/// Which phase table a circuit is reconstructed with, and so which capability +/// carries it. +/// +/// Both gatesets share the arithmetic, the gate matrices and the container +/// layout. They differ only in the phase table, which `H`, `T`, `T†` and `P(k)` +/// read (`H` uses `re P(3)` as `1/√2`). +/// +/// - [`Gateset::V1`] is `wai.quantum.circuit` (magic `WQC1`), frozen so that +/// existing content reproduces bit for bit. +/// - [`Gateset::V2`] is `wai.quantum.circuit2` (magic `WQC2`), with the correctly +/// rounded table. It is the default. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)] +#[non_exhaustive] +pub enum Gateset { + /// `wai.quantum.circuit`, with the frozen [`PHASE_V1`] table. + V1, + /// `wai.quantum.circuit2`, with the correctly rounded [`PHASE_V2`] table. + #[default] + V2, +} + +impl Gateset { + /// Every gateset, oldest first. + pub const ALL: [Gateset; 2] = [Gateset::V1, Gateset::V2]; + + /// The capability string: `wai.quantum.circuit` or `wai.quantum.circuit2`. + pub const fn capability(self) -> &'static str { + match self { + Gateset::V1 => "wai.quantum.circuit", + Gateset::V2 => "wai.quantum.circuit2", } - t - }) + } + + /// The container magic: `WQC1` or `WQC2`. + pub const fn magic(self) -> [u8; 4] { + match self { + Gateset::V1 => *b"WQC1", + Gateset::V2 => *b"WQC2", + } + } + + /// The contract's `ext`: `/`, so + /// `wai.quantum.circuit2/1` is revision 1 of the circuit2 capability. + pub const fn ext(self) -> &'static str { + match self { + Gateset::V1 => "wai.quantum.circuit/1", + Gateset::V2 => "wai.quantum.circuit2/1", + } + } + + /// The contract's `numeric` identifier. + pub const fn numeric(self) -> &'static str { + match self { + Gateset::V1 => "wai.det.fixed64", + Gateset::V2 => "wai.det.amp64", + } + } + + /// The contract's `gateset` string, which names the reconstruction's gate + /// algebra and so differs wherever its constants differ. + pub const fn name(self) -> &'static str { + match self { + Gateset::V1 => "cliffordT+dyadicP", + Gateset::V2 => "cliffordT+dyadicP.rn", + } + } + + /// The phase table: [`PHASE_V1`] or [`PHASE_V2`]. + pub const fn table(self) -> &'static [Amp; 33] { + match self { + Gateset::V1 => &PHASE_V1, + Gateset::V2 => &PHASE_V2, + } + } + + /// `P(k) = e^{2πi/2^k}` from this gateset's table. `validate` guarantees + /// `1 ≤ k ≤ DYADIC_MAX` for every `P` in a circuit. + #[inline] + pub fn phase(self, k: usize) -> Amp { + self.table()[k] + } + + /// `1/√2` in this gateset: `re P(3)`, the single source of the one irrational + /// the Clifford+T set needs. 759250124 in V1, 759250125 in V2. + #[inline] + pub fn inv_sqrt2(self) -> i64 { + self.table()[3].re + } + + /// The canonical contract bytes for an `n_qubits` circuit: no whitespace, + /// members in a fixed order, rendered directly rather than through a JSON + /// serializer so that no serializer setting can change them. + pub fn contract_bytes(self, n_qubits: u8) -> Vec { + format!( + "{{\"ext\":\"{}\",\"n_qubits\":{},\"numeric\":\"{}\",\"frac\":{},\"gateset\":\"{}\"}}", + self.ext(), + n_qubits, + self.numeric(), + FRAC, + self.name(), + ) + .into_bytes() + } + + /// BLAKE3 of the table: `i64le(re) ‖ i64le(im)` for `k = 1..=DYADIC_MAX`, + /// 512 bytes, with no domain prefix. + pub fn table_digest(self) -> [u8; 32] { + let mut h = blake3::Hasher::new(); + for a in &self.table()[1..] { + h.update(&a.re.to_le_bytes()); + h.update(&a.im.to_le_bytes()); + } + *h.finalize().as_bytes() + } + + /// The gateset a container magic names, or `None` for any other bytes. + pub fn from_magic(m: &[u8]) -> Option { + Gateset::ALL.into_iter().find(|g| m == g.magic().as_slice()) + } + + /// The gateset a capability string names, or `None` for any other string. + pub fn from_capability(s: &str) -> Option { + Gateset::ALL.into_iter().find(|g| s == g.capability()) + } } -/// `1/√2` in the fixed-point floor — derived from the dyadic table (`cos π/4`), so -/// there is a *single* source for the one irrational the Clifford+T set needs. +/// `1/√2` in the circuit/1 fixed-point floor ([`Gateset::V1`]). pub fn inv_sqrt2() -> i64 { - dyadic_table()[3].re + Gateset::V1.inv_sqrt2() } -/// `e^{2πi / 2^k}` in the fixed-point floor, for `1 ≤ k ≤ DYADIC_MAX`. +/// `e^{2πi / 2^k}` from the circuit/1 table ([`Gateset::V1`]), for +/// `1 ≤ k ≤ DYADIC_MAX`. pub fn phase(k: usize) -> Amp { - dyadic_table()[k] + Gateset::V1.phase(k) } // --------------------------------------------------------------------------- @@ -947,19 +1149,214 @@ mod tests { (a - b).abs() <= tol } + fn hx(b: &[u8]) -> String { + b.iter().map(|x| format!("{x:02x}")).collect() + } + + /// The conformance corpus's `dyadic_ladder`: X(0); then Y, Z, S, S†, T, T† + /// and I on qubit 1; then CP(k; control 0, target 1) for k = 1..=32, twice. + /// Every table entry multiplies a nonzero amplitude, so a change of one unit + /// in any entry moves the statevector hash. + fn dyadic_ladder() -> Circuit { + let mut c = Circuit::new(2); + c.x(0).y(1).z(1).s(1); + c.push(BaseGate::Sdg, vec![], 1, 0); + c.t(1); + c.push(BaseGate::Tdg, vec![], 1, 0); + c.push(BaseGate::I, vec![], 1, 0); + for _ in 0..2 { + for k in 1..=DYADIC_MAX as u16 { + c.cp(k, 0, 1); + } + } + c + } + + /// circuit/1's table is the recurrence its artefacts were made with, and + /// stays that way. The defect is pinned on purpose, so that nobody repairs + /// circuit/1 in place: a repaired table would stop every circuit/1 file and + /// receipt from reproducing. The repair is circuit2. #[test] - fn dyadic_table_is_unit_modulus() { - // cos^2 + sin^2 ≈ 1 for every tabulated phase, and P(1)=Z, P(2)=S, P(3)=T. - for k in 1..=DYADIC_MAX { - let a = phase(k); - let n2 = ((a.norm2()) >> FRAC) as i64; // at 2^FRAC - assert!(approx(n2, ONE, ONE / 1000), "|e^(2πi/2^{k})|^2 = {n2} not ≈ ONE"); + fn phase_table_v1_is_frozen() { + // The recurrence: truncating halving, floor square root. Both halves are + // non-negative, so truncating division is floor division. + let mut t = [Amp::ZERO; DYADIC_MAX + 1]; + t[1] = Amp { re: -ONE, im: 0 }; + t[2] = Amp { re: 0, im: ONE }; + for k in 3..=DYADIC_MAX { + let c = t[k - 1].re; + t[k] = Amp { re: sqrt_fx((ONE + c) / 2), im: sqrt_fx((ONE - c) / 2) }; + } + assert_eq!(t, PHASE_V1, "PHASE_V1 is not the circuit/1 recurrence"); + assert_eq!( + hx(&Gateset::V1.table_digest()), + "3fa9805ff96feb819a1cf371fedcd3b5228f93eddf933d0edcf401cbf8c28466" + ); + // From k = 19 on, circuit/1 applies no phase at all. + for k in 19..=DYADIC_MAX { + assert_eq!(PHASE_V1[k], Amp { re: ONE - 1, im: 0 }, "PHASE_V1[{k}]"); + } + assert_eq!(Gateset::V1.inv_sqrt2(), 759_250_124); + // End to end, so the unit suite alone catches circuit/1 drift: the ladder + // reproduces its circuit/1 corpus hash. + let sv = dyadic_ladder().simulate().unwrap(); + assert_eq!( + hx(&sv.statevector_hash()), + "9754af24edd026601297a0f3334eb7171174978841a2938c9b372db942a3851d" + ); + } + + /// Every circuit2 entry is the nearest integer, proved with integers alone. + /// + /// Enclose `2^62·cos(2π/2^k)` and `2^62·sin(2π/2^k)` between integer bounds, + /// starting exactly at `k = 2`, and halve the angle with rounding chosen to + /// keep each bound on its side: `cos(θ/2) = √((1 + cos θ)/2)` and + /// `sin(θ/2) = sin θ / (2 cos(θ/2))`, a quotient, so nothing cancels. If both + /// ends of an enclosure round to the same integer at `2^-30`, the true value + /// does too, and that integer is within ½ unit of it. Every endpoint is also + /// required to sit more than `2^20` ulps from a tie. + #[test] + fn phase_table_v2_is_certified_nearest() { + const S: u128 = 1 << 62; + let mul = |a: u128, b: u128| a.checked_mul(b).expect("the enclosure overflowed u128"); + let ceil_isqrt = |x: u128| { + let r = x.isqrt(); + if r * r == x { r } else { r + 1 } + }; + // Round half up from 2^-62 to 2^-30. + let r = |x: u128| ((x + (1 << 31)) >> 32) as i64; + let far_from_tie = |x: u128| ((x & 0xFFFF_FFFF) as i128 - (1i128 << 31)).abs() > 1 << 20; + + // (cos, sin)(π/2) = (0, 1), exactly. + let (mut c_lo, mut c_hi, mut s_lo, mut s_hi) = (0u128, 0u128, S, S); + for k in 3..=DYADIC_MAX { + let c_lo2 = (mul(S + c_lo, S) >> 1).isqrt(); + let c_hi2 = ceil_isqrt(mul(S + c_hi, S).div_ceil(2)); + let s_lo2 = mul(s_lo, S) / (2 * c_hi2); + let s_hi2 = mul(s_hi, S).div_ceil(2 * c_lo2); + assert!(c_lo2 <= c_hi2 && s_lo2 <= s_hi2, "P({k}): an empty enclosure"); + let want = PHASE_V2[k]; + assert_eq!((r(c_lo2), r(c_hi2)), (want.re, want.re), "re P({k}) is not certified"); + assert_eq!((r(s_lo2), r(s_hi2)), (want.im, want.im), "im P({k}) is not certified"); + for x in [c_lo2, c_hi2, s_lo2, s_hi2] { + assert!(far_from_tie(x), "P({k}): an endpoint lies within 2^20 ulps of a tie"); + } + (c_lo, c_hi, s_lo, s_hi) = (c_lo2, c_hi2, s_lo2, s_hi2); + } + assert_eq!(PHASE_V2[0], Amp::ZERO); + assert_eq!(PHASE_V2[1], Amp { re: -ONE, im: 0 }); + assert_eq!(PHASE_V2[2], Amp { re: 0, im: ONE }); + assert_eq!( + hx(&Gateset::V2.table_digest()), + "d34cb793715348e7c92db027c0bc3dddb6008f5730470bafaed978ea7fb23510" + ); + assert_eq!(Gateset::V2.inv_sqrt2(), 759_250_125); + } + + /// Every circuit2 component is within one unit of the ideal, against an + /// independent floating-point reference, and the same check fails on + /// circuit/1, so it can tell a good table from a bad one. + /// + /// The reference is the crate's own `sin_cos`, which gives the same bits on + /// every IEEE platform and is within an ulp; it is compiled under these + /// features, all of which `full` enables. + #[cfg(any( + feature = "quantum_spd", + feature = "quantum_pauli", + feature = "quantum_mps", + feature = "quantum_vml", + feature = "quantum_phasor", + feature = "quantum_kernel", + feature = "quantum_tn", + feature = "quantum_sv", + feature = "quantum_tdvp", + feature = "quantum_nqs" + ))] + #[test] + fn every_v2_phase_is_within_one_unit_of_ideal() { + // ½ unit, plus slack for the reference's own error (about 1e-7 unit). + const BOUND: f64 = 0.5 + 1e-6; + // The largest deviation of each entry from 2^30·(cos, sin)(2π/2^k). + let deviation = |t: &[Amp; 33]| -> Vec<(usize, f64)> { + (1..=DYADIC_MAX) + .map(|k| { + let (s, c) = crate::repro::sin_cos(std::f64::consts::TAU / (1u64 << k) as f64); + let one = ONE as f64; + let dre = (t[k].re as f64 - c * one).abs(); + let dim = (t[k].im as f64 - s * one).abs(); + (k, dre.max(dim)) + }) + .collect() + }; + let mut worst = 0.0f64; + for (k, d) in deviation(&PHASE_V2) { + assert!(d <= BOUND, "PHASE_V2[{k}] is {d} units from the ideal"); + worst = worst.max(d); + } + assert!(worst < 1.0); + // Negative control: circuit/1 fails the same check from k = 8 on (its + // P(8) imaginary part is 9.52 units high). + let v1_bad: Vec = + deviation(&PHASE_V1).into_iter().filter(|&(_, d)| d > BOUND).map(|(k, _)| k).collect(); + assert!(v1_bad.contains(&8), "the check passed circuit/1's P(8): {v1_bad:?}"); + assert!((8..=DYADIC_MAX).all(|k| v1_bad.contains(&k)), "circuit/1 failures: {v1_bad:?}"); + } + + /// The identities each table must keep: `P(1) = −1` and `P(2) = i` exactly, + /// `T = e^{iπ/4}` with `re == im == 1/√2`, and `|P(k)|² = 1` to within one + /// unit in circuit2 and three in circuit/1. + #[test] + fn table_identities_per_gateset() { + for gs in Gateset::ALL { + assert_eq!(gs.phase(1), Amp { re: -ONE, im: 0 }, "{gs:?} P(1)"); + assert_eq!(gs.phase(2), Amp { re: 0, im: ONE }, "{gs:?} P(2)"); + assert_eq!(gs.phase(3).re, gs.phase(3).im, "{gs:?} T"); + assert_eq!(gs.phase(3).re, gs.inv_sqrt2(), "{gs:?} 1/√2"); + assert_eq!(gs.table()[0], Amp::ZERO, "{gs:?} index 0"); + let units: i128 = match gs { + Gateset::V1 => 3, + Gateset::V2 => 1, + }; + for k in 1..=DYADIC_MAX { + // |P|² − 1 at scale 2^60, exactly. + let d = gs.phase(k).norm2() - (ONE as i128) * (ONE as i128); + assert!( + d.abs() <= units << FRAC, + "{gs:?}: |P({k})|² − 1 = {} units", + d as f64 / ONE as f64 + ); + } + } + } + + /// The identifiers each gateset names map back to it, and its contract bytes + /// are the canonical template. circuit/1's must equal the serializer render + /// every existing circuit hash was taken over. + #[test] + fn gateset_identifiers_are_consistent() { + assert_eq!(Gateset::default(), Gateset::V2); + for gs in Gateset::ALL { + assert_eq!(Gateset::from_magic(&gs.magic()), Some(gs)); + assert_eq!(Gateset::from_capability(gs.capability()), Some(gs)); + assert_eq!(gs.ext(), format!("{}/1", gs.capability())); + } + assert_eq!(Gateset::from_magic(b"WQC3"), None); + assert_eq!(Gateset::from_magic(b"WQC"), None); + assert_eq!(Gateset::from_magic(b"WQC1\0"), None); + assert_eq!(Gateset::from_capability("wai.quantum.circuit/1"), None); + assert_eq!(Gateset::from_capability("wai.quantum.circuit3"), None); + assert_eq!( + Gateset::V1.contract_bytes(5), + br#"{"ext":"wai.quantum.circuit/1","n_qubits":5,"numeric":"wai.det.fixed64","frac":30,"gateset":"cliffordT+dyadicP"}"# + ); + assert_eq!( + Gateset::V2.contract_bytes(5), + br#"{"ext":"wai.quantum.circuit2/1","n_qubits":5,"numeric":"wai.det.amp64","frac":30,"gateset":"cliffordT+dyadicP.rn"}"# + ); + assert_eq!((Gateset::V1.contract_bytes(5).len(), Gateset::V2.contract_bytes(5).len()), (112, 114)); + for n in 0..=u8::MAX { + assert_eq!(Gateset::V1.contract_bytes(n), contract_json(n), "n_qubits {n}"); } - assert_eq!(phase(1), Amp { re: -ONE, im: 0 }); // e^{iπ} - assert_eq!(phase(2), Amp { re: 0, im: ONE }); // e^{iπ/2} - // T = e^{iπ/4}: re == im == 1/√2 - assert_eq!(phase(3).re, inv_sqrt2()); - assert_eq!(phase(3).im, inv_sqrt2()); } #[test] -- 2.54.0 (Apple Git-157) From 85120eb98726ffa88c8750c2a426e4f99e5539f5 Mon Sep 17 00:00:00 2001 From: Transaction Science Date: Fri, 9 Oct 2026 10:34:21 -0400 Subject: [PATCH 02/13] wai-quantum circuit2: circuits carry their gateset MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Circuit gains a public gateset field and is non_exhaustive. Circuit::new gives Gateset::V2, Circuit::with_gateset picks one, and Circuit::empty_like copies the qubit count and gateset with no ops. Circuit::qft goes through new, so it is V2. PartialEq compares the gateset, so the same ops in V1 and V2 are different circuits. validate follows the gateset. In both, a gate has at most 255 controls, since the op-log counts them in a u8. V2 also requires pairwise distinct controls, checked with a 256-bit bitmap, and a parameter of 0 on every gate other than P. BaseGate::matrix and apply take the gateset, and simulate passes the circuit's own, so H, T, T† and P(k) read that gateset's table. The free phase(), inv_sqrt2() and CAPABILITY are removed in favour of the Gateset methods. quantum_comm names Gateset::V2 once and uses it for preparation, the X-basis measurement and the cloning circuits. quantum_qudit's d = 2 normaliser is Gateset::V2.inv_sqrt2(), so its Fourier gate is that gateset's Hadamard. The fabric's lane matrix reads the circuit's gateset, and random_circuit builds circuit/1 circuits, as the kernel and generator names it is certified under say; the lanes32 corpus for seed 7 and 200 cases is still d18454c6. QuantumError is non_exhaustive and gains Contract, shown as "contract mismatch: ...". kind() names each class: malformed, invalid, contract or too-many-qubits. The container is unchanged in this commit. It reads and writes WQC1 only, and from_wqc builds V1 circuits, so wqc_round_trips builds its circuit in V1. The legacy pins build their circuits in Gateset::V1 and keep their values: the circuit/1 receipt, the MPS truncation and sparse Pauli digests, the two-qubit entropy and the depolarised spectrum. New tests: every P(k) is copied exactly onto |1>, plain and controlled, in both gatesets; QFT-14 of |1...1> is within 3 units of an f64 reference in V2 (2.03 measured) and more than 8 off in V1 (14.43); P(19) applied 2^18 times turns |1> by pi in V2 and leaves it at (1073479680, 0) in V1; one V2 gate on 14,000 seeded normalised pairs stays within 2 units per component (1.40 measured) while V1 fails the same check; V2 rejects the repeated controls and non-P parameters V1 accepts. The apply equivalence test now runs 600 random circuits over both gatesets with P(1..32). Two small tests cover the constructors and the error kinds. Co-Authored-By: Claude Opus 5.5 --- wai/wai-quantum/src/quantum.rs | 437 ++++++++++++++++++++++--- wai/wai-quantum/src/quantum_channel.rs | 10 +- wai/wai-quantum/src/quantum_comm.rs | 14 +- wai/wai-quantum/src/quantum_fabric.rs | 32 +- wai/wai-quantum/src/quantum_qudit.rs | 10 +- wai/wai-quantum/src/quantum_source.rs | 3 +- wai/wai-quantum/src/receipt_pins.rs | 5 +- wai/wai-quantum/tests/bit_identity.rs | 8 +- 8 files changed, 435 insertions(+), 84 deletions(-) diff --git a/wai/wai-quantum/src/quantum.rs b/wai/wai-quantum/src/quantum.rs index 845e61782..15377587b 100644 --- a/wai/wai-quantum/src/quantum.rs +++ b/wai/wai-quantum/src/quantum.rs @@ -59,9 +59,6 @@ const DOMAIN_STATEVECTOR: &[u8] = b"wai:quantum-statevector\x01"; const DOMAIN_CIRCUIT: &[u8] = b"wai:quantum-circuit\x01"; const DOMAIN_HISTOGRAM: &[u8] = b"wai:quantum-histogram\x01"; -/// The capability string this module reconstructs. -pub const CAPABILITY: &str = "wai.quantum.circuit"; - // --------------------------------------------------------------------------- // Fixed-point complex amplitude // --------------------------------------------------------------------------- @@ -359,17 +356,6 @@ impl Gateset { } } -/// `1/√2` in the circuit/1 fixed-point floor ([`Gateset::V1`]). -pub fn inv_sqrt2() -> i64 { - Gateset::V1.inv_sqrt2() -} - -/// `e^{2πi / 2^k}` from the circuit/1 table ([`Gateset::V1`]), for -/// `1 ≤ k ≤ DYADIC_MAX`. -pub fn phase(k: usize) -> Amp { - Gateset::V1.phase(k) -} - // --------------------------------------------------------------------------- // Gates — a controlled-1-qubit-gate IR // --------------------------------------------------------------------------- @@ -421,14 +407,16 @@ impl BaseGate { }) } - /// The 2×2 fixed-point matrix `[[m00,m01],[m10,m11]]` for this gate. - pub(crate) fn matrix(self, param: u16) -> [[Amp; 2]; 2] { + /// The 2×2 fixed-point matrix `[[m00,m01],[m10,m11]]` for this gate in the + /// gateset `gs`, whose table `H` (`re P(3)` as `1/√2`), `T`, `T†` and `P` + /// read. `param` must be in `1..=DYADIC_MAX` for `P`, as `validate` ensures. + pub(crate) fn matrix(self, param: u16, gs: Gateset) -> [[Amp; 2]; 2] { let z = Amp::ZERO; let one = Amp::ONE; let i = Amp { re: 0, im: ONE }; let neg_i = Amp { re: 0, im: -ONE }; - let s = Amp { re: inv_sqrt2(), im: 0 }; - let neg_s = Amp { re: -inv_sqrt2(), im: 0 }; + let s = Amp { re: gs.inv_sqrt2(), im: 0 }; + let neg_s = Amp { re: -gs.inv_sqrt2(), im: 0 }; let neg_one = Amp { re: -ONE, im: 0 }; match self { BaseGate::I => [[one, z], [z, one]], @@ -438,9 +426,9 @@ impl BaseGate { BaseGate::H => [[s, s], [s, neg_s]], BaseGate::S => [[one, z], [z, i]], BaseGate::Sdg => [[one, z], [z, neg_i]], - BaseGate::T => [[one, z], [z, phase(3)]], - BaseGate::Tdg => [[one, z], [z, phase(3).conj()]], - BaseGate::P => [[one, z], [z, phase(param as usize)]], + BaseGate::T => [[one, z], [z, gs.phase(3)]], + BaseGate::Tdg => [[one, z], [z, gs.phase(3).conj()]], + BaseGate::P => [[one, z], [z, gs.phase(param as usize)]], } } } @@ -460,31 +448,60 @@ pub struct Gate { // Circuit // --------------------------------------------------------------------------- -/// A quantum circuit: `n_qubits` and an ordered op-log. This is the transportable -/// object — compact, linear in gate count. +/// A quantum circuit: `n_qubits`, an ordered op-log, and the [`Gateset`] it is +/// reconstructed with. This is the transportable object — compact, linear in +/// gate count. +/// +/// The gateset is part of the circuit: two circuits with the same ops and +/// different gatesets are different circuits, and compare unequal. +/// [`Circuit::new`] gives [`Gateset::V2`]; a circuit decoded from a container +/// takes the container's; a circuit derived from another keeps the other's +/// ([`Circuit::empty_like`]). The fields stay public, so a circuit/1 build is +/// `let mut c = Circuit::qft(n); c.gateset = Gateset::V1;`. #[derive(Clone, Debug, PartialEq, Eq)] +#[non_exhaustive] pub struct Circuit { pub n_qubits: u8, pub ops: Vec, + pub gateset: Gateset, } /// Errors from parsing a `WQC` container or building an out-of-range circuit. #[derive(Debug, PartialEq, Eq)] +#[non_exhaustive] pub enum QuantumError { /// Bad magic, truncated section, or malformed op-log. Malformed(String), - /// A qubit index ≥ `n_qubits`, a control equal to the target, or a `P(k)` with - /// `k` outside `1..=DYADIC_MAX`. + /// A circuit its gateset does not allow: a qubit index ≥ `n_qubits`, a + /// control equal to the target, more than 255 controls, a `P(k)` with `k` + /// outside `1..=DYADIC_MAX`, or, in [`Gateset::V2`], a repeated control or + /// a parameter on a gate other than `P`. Invalid(String), + /// A container's contract does not match the capability its magic names. + Contract(String), /// `n_qubits` exceeds what this build will materialize (`2^n` amplitudes). TooManyQubits(u8), } +impl QuantumError { + /// The error's class, as the conformance corpus names it: `"malformed"`, + /// `"invalid"`, `"contract"` or `"too-many-qubits"`. + pub fn kind(&self) -> &'static str { + match self { + QuantumError::Malformed(_) => "malformed", + QuantumError::Invalid(_) => "invalid", + QuantumError::Contract(_) => "contract", + QuantumError::TooManyQubits(_) => "too-many-qubits", + } + } +} + impl std::fmt::Display for QuantumError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { QuantumError::Malformed(e) => write!(f, "malformed WQC: {e}"), QuantumError::Invalid(e) => write!(f, "invalid circuit: {e}"), + QuantumError::Contract(e) => write!(f, "contract mismatch: {e}"), QuantumError::TooManyQubits(n) => write!(f, "too many qubits: {n}"), } } @@ -497,8 +514,20 @@ impl std::error::Error for QuantumError {} pub const MAX_QUBITS: u8 = 26; impl Circuit { + /// An empty circuit on `n_qubits` in [`Gateset::V2`], `wai.quantum.circuit2`. pub fn new(n_qubits: u8) -> Self { - Circuit { n_qubits, ops: Vec::new() } + Circuit::with_gateset(n_qubits, Gateset::V2) + } + + /// An empty circuit on `n_qubits` in the gateset `gateset`. + pub fn with_gateset(n_qubits: u8, gateset: Gateset) -> Self { + Circuit { n_qubits, ops: Vec::new(), gateset } + } + + /// An empty circuit with this one's qubit count and gateset, for building a + /// circuit derived from it without resetting its gateset. + pub fn empty_like(&self) -> Self { + Circuit::with_gateset(self.n_qubits, self.gateset) } fn push(&mut self, base: BaseGate, controls: Vec, target: u8, param: u16) -> &mut Self { @@ -530,7 +559,8 @@ impl Circuit { /// The **Quantum Fourier Transform** over all `n` qubits — the "quantum FFT", /// transported as a circuit (Hadamard + controlled-phase ladder + bit-reversal - /// swaps) and reconstructed byte-exact. Uses the pinned dyadic phases. + /// swaps) and reconstructed byte-exact. Built with [`Circuit::new`], so in + /// [`Gateset::V2`], whose table gives every phase the ladder needs. pub fn qft(n: u8) -> Circuit { let mut c = Circuit::new(n); for j in 0..n { @@ -547,13 +577,28 @@ impl Circuit { c } - /// Validate qubit indices, control/target distinctness, and `P(k)` range. + /// Check the circuit against its gateset's rules. + /// + /// In both gatesets every target and control is a qubit of the circuit, no + /// control equals its target, a gate has at most 255 controls (the op-log + /// counts them in a `u8`), and `P(k)` has `1 ≤ k ≤ DYADIC_MAX`. + /// [`Gateset::V2`] also requires a gate's controls to be pairwise distinct, + /// so at most 254, and the parameter of every gate other than `P` to be 0. pub fn validate(&self) -> Result<(), QuantumError> { let n = self.n_qubits; + let strict = match self.gateset { + Gateset::V1 => false, + Gateset::V2 => true, + }; for g in &self.ops { if g.target >= n { return Err(QuantumError::Invalid(format!("target {} ≥ n_qubits {}", g.target, n))); } + if g.controls.len() > usize::from(u8::MAX) { + return Err(QuantumError::Invalid(format!("{} controls, more than 255", g.controls.len()))); + } + // One bit per qubit index, for the distinct-controls rule. + let mut seen = [0u64; 4]; for &c in &g.controls { if c >= n { return Err(QuantumError::Invalid(format!("control {c} ≥ n_qubits {n}"))); @@ -561,19 +606,29 @@ impl Circuit { if c == g.target { return Err(QuantumError::Invalid(format!("control {c} equals target"))); } + if strict { + let (word, bit) = (usize::from(c >> 6), 1u64 << (c & 63)); + if seen[word] & bit != 0 { + return Err(QuantumError::Invalid(format!("control {c} repeated"))); + } + seen[word] |= bit; + } } if g.base == BaseGate::P { let k = g.param as usize; if k < 1 || k > DYADIC_MAX { return Err(QuantumError::Invalid(format!("P(k) with k={k} out of 1..={DYADIC_MAX}"))); } + } else if strict && g.param != 0 { + return Err(QuantumError::Invalid(format!("{:?} with param {}; only P takes one", g.base, g.param))); } } Ok(()) } - /// Classically simulate the circuit from `|0…0⟩`, returning the fixed-point - /// state vector. Pure i64: byte-identical on every machine. + /// Classically simulate the circuit from `|0…0⟩` with its gateset's table, + /// returning the fixed-point state vector. Pure i64: byte-identical on every + /// machine. pub fn simulate(&self) -> Result { self.simulate_from(0) } @@ -588,7 +643,7 @@ impl Circuit { let mut amps = vec![Amp::ZERO; dim]; amps[start % dim] = Amp::ONE; for g in &self.ops { - apply(&mut amps, self.n_qubits, g); + apply(&mut amps, self.n_qubits, g, self.gateset); } Ok(StateVector { n_qubits: self.n_qubits, amps }) } @@ -622,11 +677,15 @@ impl Circuit { /// The loop shape is deliberately left alone. Blocking it, and indexing the pairs /// by bit-insertion, were both tried and both came out slower than the plain scan: /// the arithmetic is what costs here, not the iteration. -fn apply(amps: &mut [Amp], n: u8, g: &Gate) { +/// +/// The shortcuts read only the matrix, so they hold for either gateset's table: +/// circuit2's `P(k ≥ 18)` has `re == ONE` and `im ≠ 0`, which is the diagonal +/// path, and only an exact `(−ONE, 0)` takes the negation. +fn apply(amps: &mut [Amp], n: u8, g: &Gate, gs: Gateset) { if matches!(g.base, BaseGate::I) { return; } - let m = g.base.matrix(g.param); + let m = g.base.matrix(g.param, gs); let tbit = 1usize << g.target; let ctrl_mask: usize = g.controls.iter().fold(0usize, |acc, &c| acc | (1usize << c)); let dim = 1usize << n; @@ -982,7 +1041,8 @@ pub fn from_wqc(bytes: &[u8]) -> Result<(Circuit, Option), QuantumError let n_qubits = cv["n_qubits"].as_u64().ok_or_else(|| QuantumError::Malformed("n_qubits".into()))? as u8; let ops = parse_oplog(n_qubits, oplog)?; - let circuit = Circuit { n_qubits, ops }; + // The magic is WQC1, so the circuit is circuit/1. + let circuit = Circuit { n_qubits, ops, gateset: Gateset::V1 }; circuit.validate()?; let measure = match measure_raw { @@ -1016,8 +1076,8 @@ mod tests { /// are justified by an argument (`fxmul(ONE,x) == x`, `fxmul(0,x) == 0`), and /// an argument about fixed-point rounding is exactly the kind that is /// convincing and wrong, so it is checked against the code it replaced. - fn apply_reference(amps: &mut [Amp], n: u8, g: &Gate) { - let m = g.base.matrix(g.param); + fn apply_reference(amps: &mut [Amp], n: u8, g: &Gate, gs: Gateset) { + let m = g.base.matrix(g.param, gs); let tbit = 1usize << g.target; let ctrl_mask: usize = g.controls.iter().fold(0usize, |acc, &c| acc | (1usize << c)); let dim = 1usize << n; @@ -1070,14 +1130,14 @@ mod tests { let mut a = vec![Amp::ZERO; dim]; a[0] = Amp::ONE; let t0 = Instant::now(); - for g in &ops { apply_reference(&mut a, n, g); } + for g in &ops { apply_reference(&mut a, n, g, Gateset::V2); } best_ref = best_ref.min(t0.elapsed().as_nanos()); std::hint::black_box(&a); let mut b = vec![Amp::ZERO; dim]; b[0] = Amp::ONE; let t1 = Instant::now(); - for g in &ops { apply(&mut b, n, g); } + for g in &ops { apply(&mut b, n, g, Gateset::V2); } best_opt = best_opt.min(t1.elapsed().as_nanos()); std::hint::black_box(&b); } @@ -1089,7 +1149,9 @@ mod tests { /// Every amplitude, not just the hash: random circuits over the whole gate set /// must come out of the optimised path bit-for-bit as they came out of the old /// one. A faster simulator that moved one low bit would have broken every - /// receipt this crate has ever signed. + /// receipt this crate has ever signed. Both tables, and every `P(k)`: in + /// circuit2, `P(k ≥ 18)` has `re == ONE` with `im ≠ 0`, which must take the + /// diagonal path and nothing else. #[test] fn optimised_apply_is_bit_identical_to_the_general_path() { let mut st = 0x1234_5678_9ABC_DEF0u64; @@ -1097,10 +1159,10 @@ mod tests { st = st.wrapping_mul(6364136223846793005).wrapping_add(1442695040888963407); ((st >> 33) as usize) % m.max(1) }; - for _ in 0..300 { + for (case, gs) in Gateset::ALL.into_iter().flat_map(|gs| std::iter::repeat_n(gs, 300)).enumerate() { let n = 3 + rnd(3) as u8; let dim = 1usize << n; - let mut c = Circuit::new(n); + let mut c = Circuit::with_gateset(n, gs); for _ in 0..24 { let t = rnd(n as usize) as u8; let mut other = rnd(n as usize) as u8; @@ -1114,10 +1176,10 @@ mod tests { 3 => { c.h(t); } 4 => { c.s(t); } 5 => { c.t(t); } - 6 => { c.p(1 + rnd(5) as u16, t); } + 6 => { c.p(1 + rnd(DYADIC_MAX) as u16, t); } 7 => { c.cx(other, t); } 8 => { c.cz(other, t); } - 9 => { c.cp(1 + rnd(4) as u16, other, t); } + 9 => { c.cp(1 + rnd(DYADIC_MAX) as u16, other, t); } 10 => c.ops.push(Gate { base: BaseGate::Sdg, controls: vec![], target: t, param: 0 }), 11 => c.ops.push(Gate { base: BaseGate::Tdg, controls: vec![], target: t, param: 0 }), _ => c.ops.push(Gate { base: BaseGate::I, controls: vec![], target: t, param: 0 }), @@ -1133,10 +1195,10 @@ mod tests { let mut want = vec![Amp::ZERO; dim]; want[0] = Amp::ONE; for g in &c.ops { - apply_reference(&mut want, n, g); + apply_reference(&mut want, n, g, gs); } let got = c.simulate().unwrap(); - assert_eq!(got.amps, want, "optimised apply diverged on {} gates, n={n}", c.ops.len()); + assert_eq!(got.amps, want, "case {case}: optimised apply diverged on {} gates, n={n}, {gs:?}", c.ops.len()); } } @@ -1157,8 +1219,8 @@ mod tests { /// and I on qubit 1; then CP(k; control 0, target 1) for k = 1..=32, twice. /// Every table entry multiplies a nonzero amplitude, so a change of one unit /// in any entry moves the statevector hash. - fn dyadic_ladder() -> Circuit { - let mut c = Circuit::new(2); + fn dyadic_ladder(gs: Gateset) -> Circuit { + let mut c = Circuit::with_gateset(2, gs); c.x(0).y(1).z(1).s(1); c.push(BaseGate::Sdg, vec![], 1, 0); c.t(1); @@ -1199,7 +1261,7 @@ mod tests { assert_eq!(Gateset::V1.inv_sqrt2(), 759_250_124); // End to end, so the unit suite alone catches circuit/1 drift: the ladder // reproduces its circuit/1 corpus hash. - let sv = dyadic_ladder().simulate().unwrap(); + let sv = dyadic_ladder(Gateset::V1).simulate().unwrap(); assert_eq!( hx(&sv.statevector_hash()), "9754af24edd026601297a0f3334eb7171174978841a2938c9b372db942a3851d" @@ -1375,7 +1437,7 @@ mod tests { let mut c = Circuit::new(2); c.h(0).cx(0, 1); let sv = c.simulate().unwrap(); - let s = inv_sqrt2(); + let s = c.gateset.inv_sqrt2(); assert!(approx(sv.amps[0b00].re, s, TOL)); assert_eq!(sv.amps[0b01], Amp::ZERO); assert_eq!(sv.amps[0b10], Amp::ZERO); @@ -1390,7 +1452,7 @@ mod tests { let mut c = Circuit::new(3); c.h(0).cx(0, 1).cx(1, 2); let sv = c.simulate().unwrap(); - let s = inv_sqrt2(); + let s = c.gateset.inv_sqrt2(); assert!(approx(sv.amps[0b000].re, s, TOL)); assert!(approx(sv.amps[0b111].re, s, TOL)); for k in 1..7 { @@ -1439,7 +1501,10 @@ mod tests { #[test] fn wqc_round_trips() { - let c = Circuit::qft(5); + // The container writes and reads circuit/1 (WQC1) only, so the round + // trip is a circuit/1 one. + let mut c = Circuit::qft(5); + c.gateset = Gateset::V1; let m = Some(Measure { seed: 42, shots: 1024 }); let bytes = to_wqc(&c, m); assert_eq!(&bytes[0..4], b"WQC1"); @@ -1465,5 +1530,275 @@ mod tests { let mut c = Circuit::new(1); c.push(BaseGate::P, vec![], 0, 999); // P(k) out of dyadic range assert!(matches!(c.validate(), Err(QuantumError::Invalid(_)))); + + // The op-log counts controls in a u8: 256 is too many in either + // gateset, and circuit/1, which allows repeats, takes 255. + for gs in Gateset::ALL { + let mut c = Circuit::with_gateset(2, gs); + c.push(BaseGate::X, vec![0; 256], 1, 0); + assert!(matches!(c.validate(), Err(QuantumError::Invalid(_))), "{gs:?}"); + } + let mut c = Circuit::with_gateset(2, Gateset::V1); + c.push(BaseGate::X, vec![0; 255], 1, 0); + assert_eq!(c.validate(), Ok(())); + } + + /// A circuit carries its gateset, and a circuit compares unequal to the same + /// ops in the other one. + #[test] + fn circuits_carry_their_gateset() { + assert_eq!(Circuit::new(3).gateset, Gateset::V2); + assert_eq!(Circuit::qft(3).gateset, Gateset::V2); + for gs in Gateset::ALL { + let mut c = Circuit::with_gateset(3, gs); + assert_eq!((c.n_qubits, c.gateset), (3, gs)); + c.h(0).cx(0, 1); + let e = c.empty_like(); + assert_eq!((e.n_qubits, e.gateset, e.ops.len()), (3, gs, 0)); + } + let v2 = Circuit::qft(3); + let mut v1 = v2.clone(); + v1.gateset = Gateset::V1; + assert_ne!(v1, v2); + assert_ne!(v1.simulate().unwrap(), v2.simulate().unwrap()); + } + + #[test] + fn error_kinds_name_their_class() { + let cases = [ + (QuantumError::Malformed("m".into()), "malformed", "malformed WQC: m"), + (QuantumError::Invalid("i".into()), "invalid", "invalid circuit: i"), + (QuantumError::Contract("c".into()), "contract", "contract mismatch: c"), + (QuantumError::TooManyQubits(27), "too-many-qubits", "too many qubits: 27"), + ]; + for (e, kind, shown) in cases { + assert_eq!((e.kind(), e.to_string().as_str()), (kind, shown)); + } + } + + /// `P(k)` on `|1⟩` lands on the table entry itself, bit for bit, in both + /// gatesets, plain and controlled: `fxmul` by `ONE` is exact. + #[test] + fn phase_is_copied_exactly() { + for gs in Gateset::ALL { + for k in 1..=DYADIC_MAX as u16 { + let mut c = Circuit::with_gateset(1, gs); + c.x(0).p(k, 0); + assert_eq!(c.simulate().unwrap().amps, [Amp::ZERO, gs.phase(k.into())], "{gs:?} P({k})"); + let mut c = Circuit::with_gateset(2, gs); + c.x(0).x(1).cp(k, 0, 1); + let amps = c.simulate().unwrap().amps; + assert_eq!(amps[0b11], gs.phase(k.into()), "{gs:?} CP({k})"); + assert!(amps[..0b11].iter().all(|a| *a == Amp::ZERO), "{gs:?} CP({k})"); + } + } + } + + /// A small phase applied many times. `P(19)` applied 2^18 times to `|1⟩` is + /// a rotation by π. circuit2 gets there. circuit/1's `P(19)` is + /// `(2^30 − 1, 0)`: it never rotates, and each application loses one unit. + #[test] + fn repeated_small_phase() { + let reps = 1usize << 18; + let run = |gs| { + let mut c = Circuit::with_gateset(1, gs); + c.x(0); + c.ops.extend(std::iter::repeat_n(Gate { base: BaseGate::P, controls: vec![], target: 0, param: 19 }, reps)); + let amps = c.simulate().unwrap().amps; + assert_eq!(amps[0], Amp::ZERO); + amps[1] + }; + assert_eq!(run(Gateset::V1), Amp { re: 1_073_479_680, im: 0 }); + assert_eq!(run(Gateset::V1).re, ONE - reps as i64); + let v2 = run(Gateset::V2); + let angle = (v2.im as f64).atan2(v2.re as f64); + assert!((angle.abs() - std::f64::consts::PI).abs() < 1e-4, "circuit2 turned {angle} rad, not π"); + // The norm drifts as slowly as the rounding allows (1.0000189 here). + let norm = (v2.norm2() as f64).sqrt() / ONE as f64; + assert!((norm - 1.0).abs() < 1e-4, "|amp| = {norm}"); + } + + /// circuit2 is strict where circuit/1 is not: a repeated control and a + /// parameter on a gate that takes none are circuit/1 circuits, and are not + /// circuit2 ones. + #[test] + fn v2_rejects_what_v1_tolerates() { + let use_gate = |gs, n, g: Gate| { + let mut c = Circuit::with_gateset(n, gs); + c.ops.push(g); + c + }; + let mut lax = vec![Gate { base: BaseGate::X, controls: vec![0, 0], target: 1, param: 0 }]; + for base in [BaseGate::I, BaseGate::X, BaseGate::Y, BaseGate::Z, BaseGate::H, BaseGate::S, BaseGate::Sdg, BaseGate::T, BaseGate::Tdg] { + lax.push(Gate { base, controls: vec![], target: 0, param: 1 }); + } + lax.push(Gate { base: BaseGate::H, controls: vec![], target: 0, param: u16::MAX }); + for g in lax { + let v1 = use_gate(Gateset::V1, 2, g.clone()); + assert_eq!(v1.validate(), Ok(()), "{g:?}"); + assert!(v1.simulate().is_ok(), "{g:?}"); + let v2 = use_gate(Gateset::V2, 2, g.clone()); + let e = v2.validate().unwrap_err(); + assert_eq!(e.kind(), "invalid", "{g:?}"); + assert_eq!(v2.simulate().unwrap_err(), e, "{g:?}"); + } + // The parameter rule spares P, whose parameter is its k. + let mut p = Circuit::new(1); + p.p(5, 0); + assert_eq!(p.validate(), Ok(())); + // Distinct controls: 254 on 255 qubits is the most circuit2 can hold. + let all: Vec = (0..254).collect(); + assert_eq!(use_gate(Gateset::V2, 255, Gate { base: BaseGate::X, controls: all.clone(), target: 254, param: 0 }).validate(), Ok(())); + let mut repeat = all.clone(); + repeat.push(253); + let g = Gate { base: BaseGate::X, controls: repeat, target: 254, param: 0 }; + assert_eq!(use_gate(Gateset::V1, 255, g.clone()).validate(), Ok(())); + assert!(matches!(use_gate(Gateset::V2, 255, g).validate(), Err(QuantumError::Invalid(_)))); + } + + /// Accuracy against the ideal, by floating-point references whose phases + /// come from the crate's own `sin_cos`, compiled under these features, all + /// of which `full` enables. + #[cfg(any( + feature = "quantum_spd", + feature = "quantum_pauli", + feature = "quantum_mps", + feature = "quantum_vml", + feature = "quantum_phasor", + feature = "quantum_kernel", + feature = "quantum_tn", + feature = "quantum_sv", + feature = "quantum_tdvp", + feature = "quantum_nqs" + ))] + mod against_ideal { + use super::*; + + 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) + } + + fn cadd(a: C, b: C) -> C { + (a.0 + b.0, a.1 + b.1) + } + + /// `e^{2πi/2^k}`. + fn root(k: u16) -> C { + let (s, c) = crate::repro::sin_cos(std::f64::consts::TAU / (1u64 << k) as f64); + (c, s) + } + + /// The ideal matrix of a base gate. + fn matrix(base: BaseGate, param: u16) -> [[C; 2]; 2] { + let (z, one, i, neg_i) = ((0.0, 0.0), (1.0, 0.0), (0.0, 1.0), (0.0, -1.0)); + let h = std::f64::consts::FRAC_1_SQRT_2; + let t = root(3); + match base { + BaseGate::I => [[one, z], [z, one]], + BaseGate::X => [[z, one], [one, z]], + BaseGate::Y => [[z, neg_i], [i, z]], + BaseGate::Z => [[one, z], [z, (-1.0, 0.0)]], + BaseGate::H => [[(h, 0.0), (h, 0.0)], [(h, 0.0), (-h, 0.0)]], + BaseGate::S => [[one, z], [z, i]], + BaseGate::Sdg => [[one, z], [z, neg_i]], + BaseGate::T => [[one, z], [z, t]], + BaseGate::Tdg => [[one, z], [z, (t.0, -t.1)]], + BaseGate::P => [[one, z], [z, root(param)]], + } + } + + /// The circuit's ideal state from `|0…0⟩`, gate by gate. + fn simulate(c: &Circuit) -> Vec { + let dim = 1usize << c.n_qubits; + let mut a = vec![(0.0, 0.0); dim]; + a[0] = (1.0, 0.0); + for g in &c.ops { + let m = matrix(g.base, g.param); + let tbit = 1usize << g.target; + let mask = g.controls.iter().fold(0usize, |acc, &q| acc | (1usize << q)); + for i in 0..dim { + if i & tbit == 0 && i & mask == mask { + let j = i | tbit; + let (a0, a1) = (a[i], a[j]); + a[i] = cadd(cmul(m[0][0], a0), cmul(m[0][1], a1)); + a[j] = cadd(cmul(m[1][0], a0), cmul(m[1][1], a1)); + } + } + } + a + } + + /// QFT-14 of `|1…1⟩`, whose ladder multiplies nonzero amplitudes by every + /// `P(2..=14)`. circuit2 stays within 3 units of the ideal amplitude + /// (2.0 measured); circuit/1 is off by more than 8 (14.4 measured). + #[test] + fn qft14_accuracy() { + let n = 14u8; + let mut c = Circuit::new(n); + for q in 0..n { + c.x(q); + } + c.ops.extend(Circuit::qft(n).ops); + let want = simulate(&c); + let error = |gs| { + let mut c = c.clone(); + c.gateset = gs; + let one = ONE as f64; + c.simulate() + .unwrap() + .amps + .iter() + .zip(&want) + .map(|(a, w)| { + let (dre, dim) = (a.re as f64 - w.0 * one, a.im as f64 - w.1 * one); + (dre * dre + dim * dim).sqrt() + }) + .fold(0.0f64, f64::max) + }; + let (v2, v1) = (error(Gateset::V2), error(Gateset::V1)); + assert!(v2 <= 3.0, "circuit2 QFT-14 is {v2} units from the ideal"); + assert!(v1 > 8.0, "circuit/1 QFT-14 is only {v1} units from the ideal"); + } + + /// One circuit2 gate on a normalised pair moves each component at most + /// 2 units from the ideal (1.43 measured): ½ unit per table entry and ½ + /// per rounded product. `H`, `T`, `T†` and every `P(k)`, 400 seeded pairs + /// each, through the simulator's own `apply`. circuit/1 fails the same + /// check. + #[test] + fn one_v2_gate_is_within_two_units() { + let mut gates = vec![(BaseGate::H, 0u16), (BaseGate::T, 0), (BaseGate::Tdg, 0)]; + gates.extend((1..=DYADIC_MAX as u16).map(|k| (BaseGate::P, k))); + let worst = |gs| { + let mut s = 0x5EED_0000_0000_0008u64; + let mut uniform = || (splitmix64(&mut s) >> 11) as f64 / (1u64 << 53) as f64 * 2.0 - 1.0; + let one = ONE as f64; + let mut worst = 0.0f64; + for &(base, param) in &gates { + let m = matrix(base, param); + for _ in 0..400 { + let z = [uniform(), uniform(), uniform(), uniform()]; + let norm = z.iter().map(|x| x * x).sum::().sqrt(); + let fixed = |x: f64| (x / norm * one) as i64; + let a = [Amp { re: fixed(z[0]), im: fixed(z[1]) }, Amp { re: fixed(z[2]), im: fixed(z[3]) }]; + let mut got = a; + apply(&mut got, 1, &Gate { base, controls: vec![], target: 0, param }, gs); + let af = a.map(|x| (x.re as f64 / one, x.im as f64 / one)); + for r in 0..2 { + let w = cadd(cmul(m[r][0], af[0]), cmul(m[r][1], af[1])); + worst = worst.max((got[r].re as f64 - w.0 * one).abs()).max((got[r].im as f64 - w.1 * one).abs()); + } + } + } + worst + }; + assert_eq!(gates.len() * 400, 14_000); + let v2 = worst(Gateset::V2); + assert!(v2 <= 2.0, "a circuit2 gate is {v2} units from the ideal"); + let v1 = worst(Gateset::V1); + assert!(v1 > 2.0, "the check passed circuit/1: {v1}"); + } } } diff --git a/wai/wai-quantum/src/quantum_channel.rs b/wai/wai-quantum/src/quantum_channel.rs index 2e5e76912..faf70c20b 100644 --- a/wai/wai-quantum/src/quantum_channel.rs +++ b/wai/wai-quantum/src/quantum_channel.rs @@ -212,7 +212,7 @@ pub fn tomography(rho: &DensityMatrix) -> DensityMatrix { #[cfg(test)] mod tests { use super::*; - use crate::quantum::Circuit; + use crate::quantum::{Circuit, Gateset}; use crate::quantum_source::{holevo_bound, von_neumann_entropy}; fn pure(c: &mut Circuit) -> DensityMatrix { @@ -227,7 +227,10 @@ mod tests { pure(&mut c) } fn plus() -> DensityMatrix { - let mut c = Circuit::new(1); + plus_in(Gateset::V2) + } + fn plus_in(gs: Gateset) -> DensityMatrix { + let mut c = Circuit::with_gateset(1, gs); c.h(0); pure(&mut c) } @@ -366,7 +369,8 @@ mod tests { /// on `aarch64-apple-darwin` and `wasm32-wasip2` under wasmtime. #[test] fn a_non_trivial_spectrum_is_bit_identical_across_architectures() { - let out = Channel::depolarizing(ONE / 2).apply(&plus()).unwrap(); + // circuit/1's |+⟩, whose bits these are. + let out = Channel::depolarizing(ONE / 2).apply(&plus_in(Gateset::V1)).unwrap(); let s = von_neumann_entropy(&out); assert_eq!( s.to_bits(), diff --git a/wai/wai-quantum/src/quantum_comm.rs b/wai/wai-quantum/src/quantum_comm.rs index b37fcacda..f7cf46e30 100644 --- a/wai/wai-quantum/src/quantum_comm.rs +++ b/wai/wai-quantum/src/quantum_comm.rs @@ -12,7 +12,11 @@ //! seed the whole run is reproducible, so a reported error rate is a fact about a //! specific run that anyone can reproduce, not a number from a private die. -use crate::quantum::{fxmul, inv_sqrt2, Amp, Circuit, StateVector, FRAC, ONE}; +use crate::quantum::{fxmul, Amp, Circuit, Gateset, StateVector, FRAC, ONE}; + +/// The gateset every state here is prepared and measured in, named once so that +/// the X-basis measurement's `1/√2` is the one the preparation's `H` used. +const GS: Gateset = Gateset::V2; fn splitmix64(s: &mut u64) -> u64 { *s = s.wrapping_add(0x9E37_79B9_7F4A_7C15); @@ -27,7 +31,7 @@ fn coin(s: &mut u64) -> bool { /// Prepare `bit` in the Z basis (`false`) or the X basis (`true`). fn prepare(bit: bool, x_basis: bool) -> StateVector { - let mut c = Circuit::new(1); + let mut c = Circuit::with_gateset(1, GS); if bit { c.x(0); } @@ -45,7 +49,7 @@ fn measure(sv: &StateVector, x_basis: bool, rng: &mut u64) -> bool { // X basis is measuring in Z after a Hadamard. let (a0, a1) = (sv.amps[0], sv.amps[1]); let p0 = if x_basis { - let inv = inv_sqrt2(); + let inv = GS.inv_sqrt2(); let h0 = Amp { re: fxmul(inv, a0.re + a1.re), im: fxmul(inv, a0.im + a1.im) }; (h0.norm2() >> FRAC) as i64 } else { @@ -114,7 +118,7 @@ pub fn bb84(rounds: u32, seed: u64, eavesdropper: bool) -> Bb84 { /// produces an entangled pair, and the fidelity against a true clone collapses. pub fn clone_fidelity(x_basis_input: bool) -> f64 { // attempt: |psi> (x) |0> --CNOT--> hoping for |psi> (x) |psi> - let mut attempt = Circuit::new(2); + let mut attempt = Circuit::with_gateset(2, GS); if x_basis_input { attempt.h(0); } @@ -122,7 +126,7 @@ pub fn clone_fidelity(x_basis_input: bool) -> f64 { let got = attempt.simulate().expect("two qubits"); // the state a real cloner would have produced - let mut ideal = Circuit::new(2); + let mut ideal = Circuit::with_gateset(2, GS); if x_basis_input { ideal.h(0); ideal.h(1); diff --git a/wai/wai-quantum/src/quantum_fabric.rs b/wai/wai-quantum/src/quantum_fabric.rs index 7efe5ce83..5d37fe4ad 100644 --- a/wai/wai-quantum/src/quantum_fabric.rs +++ b/wai/wai-quantum/src/quantum_fabric.rs @@ -793,7 +793,7 @@ impl FabricReceipt { /// values. pub mod lanes { use super::{Circuit, MAX_QUBITS, QuantumError}; - use crate::quantum::{BaseGate, Gate, ONE}; + use crate::quantum::{BaseGate, Gate, Gateset, ONE}; /// The 64-bit two's-complement product of two 32-bit values, as /// `(high, low)` words, built from 16-bit halves so that no partial @@ -844,9 +844,10 @@ pub mod lanes { [a[0].wrapping_add(b[0]), a[1].wrapping_add(b[1])] } - /// The gate's matrix in lanes. Only for a validated gate. - fn matrix(g: &Gate) -> [[Lane; 2]; 2] { - let m = g.base.matrix(g.param); + /// The gate's matrix in lanes, from the gateset's table. Only for a + /// validated gate. + fn matrix(g: &Gate, gs: Gateset) -> [[Lane; 2]; 2] { + let m = g.base.matrix(g.param, gs); let l = |a: crate::quantum::Amp| [a.re as i32, a.im as i32]; [[l(m[0][0]), l(m[0][1])], [l(m[1][0]), l(m[1][1])]] } @@ -877,7 +878,7 @@ pub mod lanes { amps[0] = [ONE as i32, 0]; for g in &c.ops { let mask = g.controls.iter().fold(0usize, |acc, &q| acc | (1usize << q)); - apply(&mut amps, g.target, mask, matrix(g)); + apply(&mut amps, g.target, mask, matrix(g, c.gateset)); } Ok(amps) } @@ -891,10 +892,11 @@ pub mod lanes { } /// A random circuit of the core's gates: two to ten qubits, up to three - /// controls, dyadic phases up to `P(32)`. + /// controls, dyadic phases up to `P(32)`. It is a circuit/1 circuit, as the + /// kernel and generator it is certified under name. pub fn random_circuit(s: &mut u64) -> Circuit { let n = 2 + (next(s) % 9) as u8; - let mut c = Circuit::new(n); + let mut c = Circuit::with_gateset(n, Gateset::V1); let bases = [BaseGate::I, BaseGate::X, BaseGate::Y, BaseGate::Z, BaseGate::H, BaseGate::S, BaseGate::Sdg, BaseGate::T, BaseGate::Tdg, BaseGate::P]; for _ in 0..(10 + next(s) % 50) { let base = bases[(next(s) % bases.len() as u64) as usize]; @@ -933,7 +935,7 @@ const _: () = assert!(ONE <= i32::MAX as i64); #[cfg(test)] mod tests { use super::*; - use crate::quantum::{BaseGate, Gate, fxmul}; + use crate::quantum::{BaseGate, Gate, Gateset, fxmul}; use crate::quantum_energy::{CounterFiltering, Uncertainty}; fn next(s: &mut u64) -> u64 { @@ -1004,10 +1006,12 @@ mod tests { (2, vec![gate(BaseGate::X, vec![0], 0, 0)]), (2, vec![gate(BaseGate::X, vec![5], 1, 0)]), ]; - for (n, ops) in bad { - let c = Circuit { n_qubits: n, ops }; - let native = c.simulate_from(0).unwrap_err(); - assert_eq!(lanes::simulate(&c).unwrap_err(), native); + for gateset in Gateset::ALL { + for (n, ops) in bad.clone() { + let c = Circuit { n_qubits: n, ops, gateset }; + let native = c.simulate_from(0).unwrap_err(); + assert_eq!(lanes::simulate(&c).unwrap_err(), native); + } } assert!(matches!(lanes::simulate(&Circuit::new(MAX_QUBITS + 1)), Err(QuantumError::TooManyQubits(_)))); } @@ -1037,8 +1041,8 @@ mod tests { assert_eq!(compare(&Output::Float(vec![f64::INFINITY]), &Output::Float(vec![f64::INFINITY])), Parity::Uncertified); // Circuits that differ encode differently, however many controls. let x = |controls: Vec| Gate { base: BaseGate::X, controls, target: 1, param: 0 }; - let a = Circuit { n_qubits: 2, ops: vec![x(vec![0; 1280])] }; - let mut b = Circuit { n_qubits: 2, ops: vec![x(vec![])] }; + let a = Circuit { n_qubits: 2, ops: vec![x(vec![0; 1280])], gateset: Gateset::V2 }; + let mut b = Circuit { n_qubits: 2, ops: vec![x(vec![])], gateset: Gateset::V2 }; b.ops.extend((0..256).map(|_| Gate { base: BaseGate::I, controls: vec![], target: 0, param: 0 })); assert_ne!(lanes::circuit_bytes(&a), lanes::circuit_bytes(&b)); // Outputs split differently between reference and member hash apart. diff --git a/wai/wai-quantum/src/quantum_qudit.rs b/wai/wai-quantum/src/quantum_qudit.rs index 4d4c66bb5..61bf95b4e 100644 --- a/wai/wai-quantum/src/quantum_qudit.rs +++ b/wai/wai-quantum/src/quantum_qudit.rs @@ -22,7 +22,7 @@ //! whose accuracy is a promise rather than a proof. Adding a dimension means //! adding its exact roots, and [`omega`] refuses the rest rather than guessing. -use crate::quantum::{sqrt_fx, Amp, FRAC, ONE}; +use crate::quantum::{sqrt_fx, Amp, Gateset, FRAC, ONE}; /// `e^{2πik/d}`, exactly, for the supported dimensions. /// @@ -45,10 +45,12 @@ pub fn omega(d: u8, k: u32) -> Option { } } -/// `1/√d`, the Fourier normaliser, exactly. +/// `1/√d`, the Fourier normaliser, exactly. At `d = 2` it is circuit2's +/// `1/√2`, the nearest integer, so that the qudit Fourier gate is that +/// gateset's Hadamard; `d = 3` is the floor square root. fn inv_sqrt_d(d: u8) -> Option { match d { - 2 => Some(sqrt_fx(ONE / 2)), + 2 => Some(Gateset::V2.inv_sqrt2()), 3 => Some(sqrt_fx(ONE / 3)), _ => None, } @@ -234,7 +236,7 @@ mod tests { q.fourier(0); q.csum(0, 1); - let mut c = Circuit::new(2); + let mut c = Circuit::with_gateset(2, Gateset::V2); c.h(0).cx(0, 1); let sv = c.simulate().unwrap(); diff --git a/wai/wai-quantum/src/quantum_source.rs b/wai/wai-quantum/src/quantum_source.rs index 46a94efa5..76ecf96f2 100644 --- a/wai/wai-quantum/src/quantum_source.rs +++ b/wai/wai-quantum/src/quantum_source.rs @@ -263,7 +263,8 @@ mod tests { /// in its last places between platforms, and this test would catch it. #[test] fn entropy_is_bit_identical_across_architectures() { - let mut c = Circuit::new(2); + // circuit/1's Bell pair, whose bits these are. + let mut c = Circuit::with_gateset(2, crate::quantum::Gateset::V1); c.h(0).cx(0, 1); let half = DensityMatrix::from_pure(&c.simulate().unwrap()).partial_trace(&[0]); let s = von_neumann_entropy(&half); diff --git a/wai/wai-quantum/src/receipt_pins.rs b/wai/wai-quantum/src/receipt_pins.rs index 9a7c7917e..6973e00a6 100644 --- a/wai/wai-quantum/src/receipt_pins.rs +++ b/wai/wai-quantum/src/receipt_pins.rs @@ -34,9 +34,10 @@ fn json_hash(j: &str) -> String { #[cfg(feature = "quantum_receipt")] #[test] fn quantum_circuit_legacy_bytes() { - use crate::quantum::{Circuit, Measure}; + use crate::quantum::{Circuit, Gateset, Measure}; use crate::quantum_receipt::QuantumReceipt; - let mut c = Circuit::new(3); + // Sealed by circuit/1, as every receipt before circuit2 was. + let mut c = Circuit::with_gateset(3, Gateset::V1); c.h(0).cx(0, 1).cx(1, 2); let r = QuantumReceipt::seal(&key(1), "did:key:pin", &c, Some(Measure { seed: 7, shots: 1024 }), 117_000, Some([0x21; 32])) .unwrap(); diff --git a/wai/wai-quantum/tests/bit_identity.rs b/wai/wai-quantum/tests/bit_identity.rs index 910f9723d..795acac12 100644 --- a/wai/wai-quantum/tests/bit_identity.rs +++ b/wai/wai-quantum/tests/bit_identity.rs @@ -87,8 +87,8 @@ fn phasor_kernel() { assert_eq!(digest(&v), 0x58ba_1666_5ef6_fd9e); } -fn brickwork() -> quantum::Circuit { - let mut c = quantum::Circuit::new(8); +fn brickwork(gs: quantum::Gateset) -> quantum::Circuit { + let mut c = quantum::Circuit::with_gateset(8, gs); for l in 0..4u8 { for q in 0..8 { c.h(q); @@ -103,14 +103,14 @@ fn brickwork() -> quantum::Circuit { #[test] fn mps_truncation() { - let c = brickwork(); + let c = brickwork(quantum::Gateset::V1); let (mps, _) = quantum_mps::run(&c, 4).unwrap(); assert_eq!(digest(&[quantum_mps::fidelity(&mps, &c.simulate().unwrap())]), 0xa807_44ca_a97f_bff8); } #[test] fn sparse_pauli_propagation() { - let (e, _) = quantum_pauli::expect_z(&brickwork(), 3, 1e-6).unwrap(); + let (e, _) = quantum_pauli::expect_z(&brickwork(quantum::Gateset::V1), 3, 1e-6).unwrap(); assert_eq!(digest(&[e]), 0x8cc6_5829_1fb1_8909); } -- 2.54.0 (Apple Git-157) From b60517d6e5a4980633805f907f91c3d2806763a5 Mon Sep 17 00:00:00 2001 From: Transaction Science Date: Fri, 9 Oct 2026 10:55:22 -0400 Subject: [PATCH 03/13] wai-quantum circuit2: the container reads and writes both capabilities to_wqc writes the container that a circuit's gateset names: WQC1 for circuit/1 and WQC2 for circuit2, with the same layout. The contract and the measurement are rendered with format!, so no serializer setting can change them. circuit/1 output is byte for byte what 0.3.x wrote: the four corpus files and their manifest circuit hashes reproduce. to_wqc and circuit_hash run validate first and return a Result. circuit_hash covers the canonical contract of the circuit's gateset, so it binds the capability and its table. The op-log writes its counts with try_from. validate now also bounds a circuit at u32::MAX ops, because the op-log counts them in a u32, and to_wqc refuses a section too long for the table's u32 fields. from_wqc reads both magics and applies its checks in a fixed order: - the magic; - each section's range, computed with checked arithmetic so that nothing wraps on a 32-bit usize; a known section that repeats; a missing contract or op-log; - the contract; - the op-log; - validate; - the measurement. A WQC1 contract must be a JSON object that carries circuit/1's ext, numeric, gateset and frac, where frac is the integer 30. Its formatting stays free. A WQC2 contract must be the canonical bytes. Anything else is a contract mismatch. n_qubits must be a JSON unsigned integer of at most 255, or the file is malformed, so 258 no longer reads as 2. The op-log reserves space for the ops its bytes can hold, not for the count it claims. A WQC2 op-log must end at its last op, and a WQC2 measurement must be canonical. WQC1 still ignores trailing op-log bytes and reads a lax measurement. An error quotes at most 64 characters of the file. quantum_receipt's seal propagates a circuit_hash error, and reconstruction_matches treats one as a mismatch. quantum_toolchain::circuit_id returns a Result. New tests: - circuit1_files_are_written_as_before pins the digests and circuit hashes of the four corpus files, and checks the measurement render against the serializer's. - wqc_round_trips covers both gatesets, the sizes and circuit hashes of the four circuit2 corpus circuits, and what circuit/1 still tolerates. - container_rejects builds the 26-case reject corpus in code and checks each case's error class. It also covers structural edge cases, the order of the rules, the 255-control limit, the widest circuit2 gate, the op-log reservation and section ranges that cannot wrap. wai-rs, wai-web and wai-quantum-serve still call the old signatures. They are updated in later stages. Co-Authored-By: Claude Opus 5.5 --- wai/wai-quantum/src/quantum.rs | 713 +++++++++++++++++++---- wai/wai-quantum/src/quantum_receipt.rs | 4 +- wai/wai-quantum/src/quantum_toolchain.rs | 8 +- 3 files changed, 617 insertions(+), 108 deletions(-) diff --git a/wai/wai-quantum/src/quantum.rs b/wai/wai-quantum/src/quantum.rs index 15377587b..ae928534c 100644 --- a/wai/wai-quantum/src/quantum.rs +++ b/wai/wai-quantum/src/quantum.rs @@ -470,14 +470,21 @@ pub struct Circuit { #[derive(Debug, PartialEq, Eq)] #[non_exhaustive] pub enum QuantumError { - /// Bad magic, truncated section, or malformed op-log. + /// A container whose structure is broken: a bad magic, a section out of + /// bounds, repeated or missing, an `n_qubits` that is not an integer in + /// `0..=255`, a malformed op-log, or a measurement that is not JSON. In + /// `WQC2`, also bytes after the last op or a measurement that is not the + /// canonical bytes. Malformed(String), /// A circuit its gateset does not allow: a qubit index ≥ `n_qubits`, a - /// control equal to the target, more than 255 controls, a `P(k)` with `k` - /// outside `1..=DYADIC_MAX`, or, in [`Gateset::V2`], a repeated control or - /// a parameter on a gate other than `P`. + /// control equal to the target, more than 255 controls, more than + /// `u32::MAX` ops, a `P(k)` with `k` outside `1..=DYADIC_MAX`, or, in + /// [`Gateset::V2`], a repeated control or a parameter on a gate other than + /// `P`. Also a circuit too large for a container's `u32` section sizes. Invalid(String), - /// A container's contract does not match the capability its magic names. + /// A container's contract section does not declare the capability its + /// magic names: it is not a JSON object carrying that capability's values, + /// or, in `WQC2`, it is not the canonical bytes. Contract(String), /// `n_qubits` exceeds what this build will materialize (`2^n` amplitudes). TooManyQubits(u8), @@ -581,7 +588,8 @@ impl Circuit { /// /// In both gatesets every target and control is a qubit of the circuit, no /// control equals its target, a gate has at most 255 controls (the op-log - /// counts them in a `u8`), and `P(k)` has `1 ≤ k ≤ DYADIC_MAX`. + /// counts them in a `u8`), and `P(k)` has `1 ≤ k ≤ DYADIC_MAX`. The circuit + /// has at most `u32::MAX` ops, since the op-log counts them in a `u32`. /// [`Gateset::V2`] also requires a gate's controls to be pairwise distinct, /// so at most 254, and the parameter of every gate other than `P` to be 0. pub fn validate(&self) -> Result<(), QuantumError> { @@ -590,6 +598,9 @@ impl Circuit { Gateset::V1 => false, Gateset::V2 => true, }; + if u32::try_from(self.ops.len()).is_err() { + return Err(QuantumError::Invalid(format!("{} ops, more than u32::MAX", self.ops.len()))); + } for g in &self.ops { if g.target >= n { return Err(QuantumError::Invalid(format!("target {} ≥ n_qubits {}", g.target, n))); @@ -901,7 +912,7 @@ impl StateVector { } // --------------------------------------------------------------------------- -// WQC container (magic "WQC1"): the on-wire circuit object +// WQC container (magic "WQC1" or "WQC2"): the on-wire circuit object // --------------------------------------------------------------------------- /// A pinned measurement request carried in a `WQC` (section `0x03`), so a file can @@ -916,25 +927,22 @@ const SECT_CONTRACT: u8 = 0x01; const SECT_OPLOG: u8 = 0x02; const SECT_MEASURE: u8 = 0x03; -fn contract_json(n_qubits: u8) -> Vec { - // canonical, fixed field order (serde_json preserve_order is on for the crate) - let v = serde_json::json!({ - "ext": "wai.quantum.circuit/1", - "n_qubits": n_qubits, - "numeric": "wai.det.fixed64", - "frac": FRAC, - "gateset": "cliffordT+dyadicP", - }); - serde_json::to_vec(&v).expect("contract json") +/// The canonical measurement bytes, `{"seed":S,"shots":N,"basis":"computational"}`, +/// rendered directly, as the contract is. +fn measure_bytes(m: Measure) -> Vec { + format!("{{\"seed\":{},\"shots\":{},\"basis\":\"computational\"}}", m.seed, m.shots).into_bytes() } +/// The op-log of a circuit that passed `validate`: `u32le n_ops`, then per op +/// `u8 opcode | u8 n_ctrl | u8 target | u16le param | n_ctrl × u8`. `validate` +/// bounds both counts, so neither conversion can fail. fn oplog_bytes(c: &Circuit) -> Vec { - // u32 n_ops || per op: u8 opcode | u8 n_ctrl | u8 target | u16 param | ctrls... + let n_ops = u32::try_from(c.ops.len()).expect("validate bounds the op count by u32::MAX"); let mut out = Vec::new(); - out.extend_from_slice(&(c.ops.len() as u32).to_le_bytes()); + out.extend_from_slice(&n_ops.to_le_bytes()); for g in &c.ops { out.push(g.base.opcode()); - out.push(g.controls.len() as u8); + out.push(u8::try_from(g.controls.len()).expect("validate bounds a gate's controls by 255")); out.push(g.target); out.extend_from_slice(&g.param.to_le_bytes()); out.extend_from_slice(&g.controls); @@ -942,131 +950,273 @@ fn oplog_bytes(c: &Circuit) -> Vec { out } -fn parse_oplog(n_qubits: u8, b: &[u8]) -> Result, QuantumError> { +/// How many ops to reserve for an op-log of `len` bytes that claims `n_ops`. +/// The count is the file's claim, so it never sizes an allocation alone: every +/// op takes at least 5 bytes, so the section holds at most `(len − 4) / 5`. +fn oplog_capacity(n_ops: u32, len: usize) -> usize { + usize::try_from(n_ops).unwrap_or(usize::MAX).min(len.saturating_sub(4) / 5) +} + +/// Read an op-log: the ops, and how many bytes they took. Every op is +/// bounds-checked, and an opcode above 9 is malformed. Bytes after the last op +/// are the caller's to judge. +fn parse_oplog(b: &[u8]) -> Result<(Vec, usize), QuantumError> { if b.len() < 4 { return Err(QuantumError::Malformed("oplog header".into())); } - let n_ops = u32::from_le_bytes([b[0], b[1], b[2], b[3]]) as usize; + let n_ops = u32::from_le_bytes([b[0], b[1], b[2], b[3]]); + let mut ops = Vec::with_capacity(oplog_capacity(n_ops, b.len())); let mut pos = 4; - let mut ops = Vec::with_capacity(n_ops); for _ in 0..n_ops { - if pos + 5 > b.len() { + if b.len() - pos < 5 { return Err(QuantumError::Malformed("op header".into())); } let base = BaseGate::from_opcode(b[pos]) .ok_or_else(|| QuantumError::Malformed(format!("opcode {}", b[pos])))?; - let n_ctrl = b[pos + 1] as usize; + let n_ctrl = usize::from(b[pos + 1]); let target = b[pos + 2]; let param = u16::from_le_bytes([b[pos + 3], b[pos + 4]]); pos += 5; - if pos + n_ctrl > b.len() { + if b.len() - pos < n_ctrl { return Err(QuantumError::Malformed("op controls".into())); } let controls = b[pos..pos + n_ctrl].to_vec(); pos += n_ctrl; ops.push(Gate { base, controls, target, param }); } - let _ = n_qubits; - Ok(ops) + Ok((ops, pos)) } -/// Serialize a circuit (and optional measurement) to a `WQC1` container. -pub fn to_wqc(c: &Circuit, measure: Option) -> Vec { - let contract = contract_json(c.n_qubits); - let oplog = oplog_bytes(c); - let measure_bytes = measure.map(|m| { - let v = serde_json::json!({ "seed": m.seed, "shots": m.shots, "basis": "computational" }); - serde_json::to_vec(&v).expect("measure json") - }); +/// The byte range of a section, `blob_start + off` for `len` bytes, or `None` +/// if it runs past `total` or its arithmetic overflows. Checked, so that it +/// cannot wrap where `usize` is 32 bits. +fn section_range(blob_start: usize, off: u32, len: u32, total: usize) -> Option> { + let start = blob_start.checked_add(usize::try_from(off).ok()?)?; + let end = start.checked_add(usize::try_from(len).ok()?)?; + (end <= total).then_some(start..end) +} - let mut sections: Vec<(u8, Vec)> = vec![(SECT_CONTRACT, contract), (SECT_OPLOG, oplog)]; - if let Some(mb) = measure_bytes { - sections.push((SECT_MEASURE, mb)); +/// How a member of a contract or measurement object reads in an error: its +/// JSON, cut at 64 characters, since it comes from an untrusted file. +fn shown(v: Option<&serde_json::Value>) -> String { + let Some(v) = v else { + return "missing".to_string(); + }; + let s = v.to_string(); + match s.char_indices().nth(64) { + Some((cut, _)) => format!("{}…", &s[..cut]), + None => s, + } +} + +/// Read a contract section under the gateset its container's magic names, and +/// return its `n_qubits`. +/// +/// The section must be a JSON object, or it is a contract mismatch, and +/// `n_qubits` must be a JSON unsigned integer of at most 255, or the container +/// is malformed. circuit/1 requires `ext`, `numeric`, `frac` (the integer 30) +/// and `gateset` to carry its values, and leaves member order, whitespace and +/// other members free. circuit2 requires the canonical bytes exactly. +fn read_contract(gateset: Gateset, raw: &[u8]) -> Result { + let cv: serde_json::Value = serde_json::from_slice(raw) + .map_err(|e| QuantumError::Contract(format!("the contract is not JSON: {e}")))?; + let Some(obj) = cv.as_object() else { + return Err(QuantumError::Contract(format!("the contract is not a JSON object: {}", shown(Some(&cv))))); + }; + let n_qubits = || { + obj.get("n_qubits") + .and_then(serde_json::Value::as_u64) + .and_then(|n| u8::try_from(n).ok()) + .ok_or_else(|| { + QuantumError::Malformed(format!("n_qubits {} is not an integer in 0..=255", shown(obj.get("n_qubits")))) + }) + }; + match gateset { + Gateset::V1 => { + let text = |key: &str, value: &str| { + if obj.get(key).and_then(serde_json::Value::as_str) == Some(value) { + Ok(()) + } else { + Err(QuantumError::Contract(format!("{key} is {}, not {value:?}", shown(obj.get(key))))) + } + }; + text("ext", gateset.ext())?; + text("numeric", gateset.numeric())?; + if obj.get("frac").and_then(serde_json::Value::as_u64) != Some(u64::from(FRAC)) { + return Err(QuantumError::Contract(format!("frac is {}, not {FRAC}", shown(obj.get("frac"))))); + } + text("gateset", gateset.name())?; + n_qubits() + } + Gateset::V2 => { + let n = n_qubits()?; + if raw != gateset.contract_bytes(n).as_slice() { + return Err(QuantumError::Contract(format!( + "not the canonical {} contract for {n} qubits", + gateset.capability() + ))); + } + Ok(n) + } + } +} + +/// Read a measurement section. circuit/1 reads it as it always has: the JSON +/// must parse, and a missing or non-integer `seed` or `shots` reads as 0. +/// circuit2 requires a `u64` `seed` and `shots` and the canonical bytes for +/// them, which fixes the basis as `computational`. +fn read_measure(gateset: Gateset, raw: &[u8]) -> Result { + let mv: serde_json::Value = + serde_json::from_slice(raw).map_err(|e| QuantumError::Malformed(format!("measure json: {e}")))?; + match gateset { + Gateset::V1 => Ok(Measure { seed: mv["seed"].as_u64().unwrap_or(0), shots: mv["shots"].as_u64().unwrap_or(0) }), + Gateset::V2 => { + let field = |key: &str| { + mv.get(key) + .and_then(serde_json::Value::as_u64) + .ok_or_else(|| QuantumError::Malformed(format!("measure {key} is {}, not a u64", shown(mv.get(key))))) + }; + let m = Measure { seed: field("seed")?, shots: field("shots")? }; + if raw != measure_bytes(m).as_slice() { + return Err(QuantumError::Malformed("not the canonical measure bytes".into())); + } + Ok(m) + } + } +} + +/// Serialize a circuit, and optionally a measurement, to the container its +/// gateset names: `WQC1` for [`Gateset::V1`], `WQC2` for [`Gateset::V2`]. +/// +/// The layout is the same for both: the magic, `u16le` section count, a table +/// of `(u8 kind, u32le offset, u32le len)` with offsets relative to the end of +/// the table, then the contract (`0x01`), the op-log (`0x02`) and the +/// measurement (`0x03`). A circuit/1 circuit is written byte for byte as every +/// earlier release wrote it. +/// +/// Fails, with the error `validate` gives, on a circuit its gateset does not +/// allow. A circuit that has simulated cannot fail here, unless its op-log runs +/// past what a `u32` section length can address. +pub fn to_wqc(c: &Circuit, measure: Option) -> Result, QuantumError> { + c.validate()?; + let mut sections = vec![(SECT_CONTRACT, c.gateset.contract_bytes(c.n_qubits)), (SECT_OPLOG, oplog_bytes(c))]; + if let Some(m) = measure { + sections.push((SECT_MEASURE, measure_bytes(m))); } let mut out = Vec::new(); - out.extend_from_slice(b"WQC1"); + out.extend_from_slice(&c.gateset.magic()); + // At most three sections. out.extend_from_slice(&(sections.len() as u16).to_le_bytes()); - // section table: u8 kind, u32 off (relative to start of the sections blob), u32 len - let mut off: u32 = 0; + let mut off: u64 = 0; for (kind, data) in §ions { + let (Ok(off32), Ok(len32)) = (u32::try_from(off), u32::try_from(data.len())) else { + return Err(QuantumError::Invalid(format!( + "section {kind:#04x}, {} bytes at offset {off}, does not fit the container's u32 offsets", + data.len() + ))); + }; out.push(*kind); - out.extend_from_slice(&off.to_le_bytes()); - out.extend_from_slice(&(data.len() as u32).to_le_bytes()); - off += data.len() as u32; + out.extend_from_slice(&off32.to_le_bytes()); + out.extend_from_slice(&len32.to_le_bytes()); + off += u64::from(len32); } for (_, data) in §ions { out.extend_from_slice(data); } - out + Ok(out) } -/// Parse a `WQC1` container back into a circuit and optional measurement. +/// Parse a `WQC1` or `WQC2` container into a circuit, in the gateset its magic +/// names, and its optional measurement. +/// +/// The checks run in this order, and the first to fail is the error: +/// +/// 1. The magic is `WQC1` or `WQC2`, else malformed. +/// 2. Every section lies inside the file, with arithmetic that cannot wrap; no +/// known section (`0x01`, `0x02`, `0x03`) appears twice; the contract and +/// op-log are present. Unknown sections are ignored. Else malformed. +/// 3. The contract declares the capability the magic names. In `WQC1` it must +/// be a JSON object carrying circuit/1's `ext`, `numeric`, `frac` and +/// `gateset`, in any formatting; in `WQC2` it must be the canonical bytes. +/// Else a contract mismatch, except that an `n_qubits` that is not an +/// integer in `0..=255` is malformed. +/// 4. The op-log parses within its section: every op lies inside it and has +/// an opcode of at most 9. In `WQC2` it must end exactly after its last op; +/// in `WQC1` any bytes after it are ignored. Else malformed. +/// 5. The circuit passes `validate` under its gateset, else invalid. +/// 6. The measurement, if present, is JSON, and in `WQC2` it is the canonical +/// bytes. Else malformed. pub fn from_wqc(bytes: &[u8]) -> Result<(Circuit, Option), QuantumError> { - if bytes.len() < 6 || &bytes[0..4] != b"WQC1" { + if bytes.len() < 6 { return Err(QuantumError::Malformed("magic".into())); } - let n_sections = u16::from_le_bytes([bytes[4], bytes[5]]) as usize; + let gateset = Gateset::from_magic(&bytes[0..4]).ok_or_else(|| QuantumError::Malformed("magic".into()))?; + let strict = match gateset { + Gateset::V1 => false, + Gateset::V2 => true, + }; + + let n_sections = usize::from(u16::from_le_bytes([bytes[4], bytes[5]])); let table_start = 6; - let table_len = n_sections * 9; - if bytes.len() < table_start + table_len { - return Err(QuantumError::Malformed("section table".into())); - } - let blob_start = table_start + table_len; + let blob_start = n_sections + .checked_mul(9) + .and_then(|len| len.checked_add(table_start)) + .filter(|&end| end <= bytes.len()) + .ok_or_else(|| QuantumError::Malformed("section table".into()))?; + let u32_at = |p: usize| u32::from_le_bytes([bytes[p], bytes[p + 1], bytes[p + 2], bytes[p + 3]]); let mut contract: Option<&[u8]> = None; let mut oplog: Option<&[u8]> = None; let mut measure_raw: Option<&[u8]> = None; for s in 0..n_sections { + // Inside the table, which ends at blob_start ≤ bytes.len(). let p = table_start + s * 9; let kind = bytes[p]; - let off = u32::from_le_bytes([bytes[p + 1], bytes[p + 2], bytes[p + 3], bytes[p + 4]]) as usize; - let len = u32::from_le_bytes([bytes[p + 5], bytes[p + 6], bytes[p + 7], bytes[p + 8]]) as usize; - let start = blob_start + off; - if start + len > bytes.len() { - return Err(QuantumError::Malformed("section bounds".into())); - } - let data = &bytes[start..start + len]; - match kind { - SECT_CONTRACT => contract = Some(data), - SECT_OPLOG => oplog = Some(data), - SECT_MEASURE => measure_raw = Some(data), - _ => {} // unknown section: ignore (forward-compatible) + let range = section_range(blob_start, u32_at(p + 1), u32_at(p + 5), bytes.len()) + .ok_or_else(|| QuantumError::Malformed(format!("section {kind:#04x} bounds")))?; + let slot = match kind { + SECT_CONTRACT => &mut contract, + SECT_OPLOG => &mut oplog, + SECT_MEASURE => &mut measure_raw, + _ => continue, // unknown section: ignore (forward-compatible) + }; + if slot.replace(&bytes[range]).is_some() { + return Err(QuantumError::Malformed(format!("duplicate section {kind:#04x}"))); } } let contract = contract.ok_or_else(|| QuantumError::Malformed("missing contract".into()))?; let oplog = oplog.ok_or_else(|| QuantumError::Malformed("missing oplog".into()))?; - let cv: serde_json::Value = - serde_json::from_slice(contract).map_err(|e| QuantumError::Malformed(format!("contract json: {e}")))?; - let n_qubits = cv["n_qubits"].as_u64().ok_or_else(|| QuantumError::Malformed("n_qubits".into()))? as u8; + let n_qubits = read_contract(gateset, contract)?; - let ops = parse_oplog(n_qubits, oplog)?; - // The magic is WQC1, so the circuit is circuit/1. - let circuit = Circuit { n_qubits, ops, gateset: Gateset::V1 }; + let (ops, used) = parse_oplog(oplog)?; + if strict && used != oplog.len() { + return Err(QuantumError::Malformed(format!("{} bytes after the last op", oplog.len() - used))); + } + + let circuit = Circuit { n_qubits, ops, gateset }; circuit.validate()?; - let measure = match measure_raw { - Some(mb) => { - let mv: serde_json::Value = - serde_json::from_slice(mb).map_err(|e| QuantumError::Malformed(format!("measure json: {e}")))?; - Some(Measure { - seed: mv["seed"].as_u64().unwrap_or(0), - shots: mv["shots"].as_u64().unwrap_or(0), - }) - } - None => None, - }; + let measure = measure_raw.map(|raw| read_measure(gateset, raw)).transpose()?; Ok((circuit, measure)) } -/// **circuit-equivalence** — the portable BLAKE3 identity of the circuit itself -/// (contract + op-log). The receipt binds this alongside the statevector hash. -pub fn circuit_hash(c: &Circuit) -> [u8; 32] { +/// **circuit-equivalence** — the portable BLAKE3 identity of a circuit: +/// `BLAKE3("wai:quantum-circuit\x01" ‖ contract ‖ op-log)`, over the canonical +/// contract bytes of its gateset and its op-log. The contract names the +/// capability, so the hash binds the gateset and its table: the same ops in +/// circuit/1 and circuit2 are different circuits. The receipt binds this +/// alongside the statevector hash. +/// +/// Fails, as [`to_wqc`] does, on a circuit its gateset does not allow; a +/// circuit that has simulated cannot fail. +pub fn circuit_hash(c: &Circuit) -> Result<[u8; 32], QuantumError> { + c.validate()?; let mut h = blake3::Hasher::new(); h.update(DOMAIN_CIRCUIT); - h.update(&contract_json(c.n_qubits)); + h.update(&c.gateset.contract_bytes(c.n_qubits)); h.update(&oplog_bytes(c)); - *h.finalize().as_bytes() + Ok(*h.finalize().as_bytes()) } #[cfg(test)] @@ -1393,7 +1543,7 @@ mod tests { /// The identifiers each gateset names map back to it, and its contract bytes /// are the canonical template. circuit/1's must equal the serializer render - /// every existing circuit hash was taken over. + /// every existing circuit hash was taken over (`serde_contract_v1`). #[test] fn gateset_identifiers_are_consistent() { assert_eq!(Gateset::default(), Gateset::V2); @@ -1417,10 +1567,29 @@ mod tests { ); assert_eq!((Gateset::V1.contract_bytes(5).len(), Gateset::V2.contract_bytes(5).len()), (112, 114)); for n in 0..=u8::MAX { - assert_eq!(Gateset::V1.contract_bytes(n), contract_json(n), "n_qubits {n}"); + assert_eq!(Gateset::V1.contract_bytes(n), serde_contract_v1(n), "n_qubits {n}"); } } + /// circuit/1's contract as 0.3.x rendered it, through the serializer with + /// member order preserved. Every circuit/1 hash and file was taken over + /// these bytes. + fn serde_contract_v1(n_qubits: u8) -> Vec { + serde_json::to_vec(&serde_json::json!({ + "ext": "wai.quantum.circuit/1", + "n_qubits": n_qubits, + "numeric": "wai.det.fixed64", + "frac": FRAC, + "gateset": "cliffordT+dyadicP", + })) + .unwrap() + } + + /// The measurement section as 0.3.x rendered it, through the serializer. + fn serde_measure(m: Measure) -> Vec { + serde_json::to_vec(&serde_json::json!({ "seed": m.seed, "shots": m.shots, "basis": "computational" })).unwrap() + } + #[test] fn simulation_is_deterministic_byte_for_byte() { // The core claim: same circuit → identical amplitudes, twice. @@ -1499,21 +1668,359 @@ mod tests { assert_eq!(counts[0b00] + counts[0b11], 10_000); } + /// The four circuits of the circuit/1 corpus, built in `gs`: bell, with + /// the corpus measurement, then ghz3, qft3 and qft5. + fn corpus_circuits(gs: Gateset) -> [(&'static str, Circuit, Option); 4] { + let mut bell = Circuit::with_gateset(2, gs); + bell.h(0).cx(0, 1); + let mut ghz3 = Circuit::with_gateset(3, gs); + ghz3.h(0).cx(0, 1).cx(1, 2); + let qft = |n| { + let mut c = Circuit::qft(n); + c.gateset = gs; + c + }; + [ + ("bell", bell, Some(Measure { seed: 12_648_430, shots: 100_000 })), + ("ghz3", ghz3, None), + ("qft3", qft(3), None), + ("qft5", qft(5), None), + ] + } + + /// A container from raw table entries `(kind, offset, len)` and a blob. + fn wqc_table(magic: &[u8; 4], table: &[(u8, u32, u32)], blob: &[u8]) -> Vec { + let mut out = magic.to_vec(); + out.extend_from_slice(&(table.len() as u16).to_le_bytes()); + for &(kind, off, len) in table { + out.push(kind); + out.extend_from_slice(&off.to_le_bytes()); + out.extend_from_slice(&len.to_le_bytes()); + } + out.extend_from_slice(blob); + out + } + + /// A container holding `sections` in order, each table entry pointing at + /// its own bytes. + fn wqc_raw(magic: &[u8; 4], sections: &[(u8, &[u8])]) -> Vec { + let (mut table, mut blob) = (Vec::new(), Vec::new()); + for &(kind, data) in sections { + table.push((kind, blob.len() as u32, data.len() as u32)); + blob.extend_from_slice(data); + } + wqc_table(magic, &table, &blob) + } + + /// One op-log entry, written as the wire has it. + fn op(opcode: u8, controls: &[u8], target: u8, param: u16) -> Vec { + let mut out = vec![opcode, controls.len() as u8, target]; + out.extend_from_slice(¶m.to_le_bytes()); + out.extend_from_slice(controls); + out + } + + /// An op-log holding `ops`. + fn oplog_of(ops: &[Vec]) -> Vec { + let mut out = (ops.len() as u32).to_le_bytes().to_vec(); + for o in ops { + out.extend_from_slice(o); + } + out + } + + /// `bytes` with the first `from` replaced by `to`; `from` must be there. + fn swap(bytes: &[u8], from: &str, to: &str) -> Vec { + let s = std::str::from_utf8(bytes).unwrap(); + assert!(s.contains(from), "{from:?} is not in {s}"); + s.replacen(from, to, 1).into_bytes() + } + + /// A circuit/1 container is written byte for byte as 0.3.x wrote it: the + /// four corpus files reproduce, and so do their manifest circuit hashes. + /// The measurement, now rendered directly, matches the serializer's render. + #[test] + fn circuit1_files_are_written_as_before() { + // (file, bytes, BLAKE3 of the file, manifest circuit_hash) + let want = [ + ("bell", 216, "97b294a85afe628060a51f12e7f9c2104ebc0b7d04ef36ff15b154ed1c54728e", + "894d9a9582b19f27b8d08d06938357237fc00f7f30bf4bfc2be0c04fa5e47a4e"), + ("ghz3", 157, "8f2d924416a19bc96407f36669ca3f55bcd6a57b3256bd16a669dc96bd3fb490", + "34e44f7d68037cbb3aee677f56b894dc48df6d408aa068a16d3b18076e5e7c5c"), + ("qft3", 191, "e1a7e49e2804130d802d8e927f63d6633b966c8e6b7cd3eb7278e01fd1f884c5", + "7e8414be4410557a05b52bcc1084e1c5f93b6caa0f28fa45f4d2ab661efd2a60"), + ("qft5", 261, "92b4e1497f52109922a0f2ea7ec8de2b1d9e92f155dafbf6e5f69eca4427878c", + "f36a9af99a251cae6f42144b2be3c57523f15e1e2ab3492cf15cac709fc048be"), + ]; + for ((name, c, m), (file, len, digest, hash)) in corpus_circuits(Gateset::V1).into_iter().zip(want) { + assert_eq!(name, file); + let bytes = to_wqc(&c, m).unwrap(); + assert_eq!(bytes.len(), len, "{name}.wqc"); + assert_eq!(hx(blake3::hash(&bytes).as_bytes()), digest, "{name}.wqc"); + assert_eq!(hx(&circuit_hash(&c).unwrap()), hash, "{name}"); + } + let values = [0, 1, 42, 1024, 100_000, 12_648_430, u64::from(u32::MAX), u64::MAX]; + for seed in values { + for shots in values { + let m = Measure { seed, shots }; + assert_eq!(measure_bytes(m), serde_measure(m), "{m:?}"); + } + } + } + + /// Both containers round-trip: the magic names the gateset, and the + /// decoded circuit equals the written one, gateset included, with the same + /// hash and state. circuit2's corpus circuits have the plan's sizes and + /// circuit hashes. circuit/1 keeps reading what it always read: its + /// contract in any member order, whitespace or with extra members, hashed + /// as the canonical one; bytes after the op-log; a measurement that is + /// spaced or lacks `shots`. Unknown sections are ignored in both. #[test] fn wqc_round_trips() { - // The container writes and reads circuit/1 (WQC1) only, so the round - // trip is a circuit/1 one. - let mut c = Circuit::qft(5); - c.gateset = Gateset::V1; - let m = Some(Measure { seed: 42, shots: 1024 }); - let bytes = to_wqc(&c, m); - assert_eq!(&bytes[0..4], b"WQC1"); - let (c2, m2) = from_wqc(&bytes).unwrap(); - assert_eq!(c, c2, "circuit must round-trip through WQC"); - assert_eq!(m, m2); - // and the reconstructed circuit hashes + simulates identically - assert_eq!(circuit_hash(&c), circuit_hash(&c2)); - assert_eq!(c.simulate().unwrap().amps, c2.simulate().unwrap().amps); + for gs in Gateset::ALL { + for (name, c, m) in corpus_circuits(gs) { + for m in [None, m.or(Some(Measure { seed: 42, shots: 1024 }))] { + let bytes = to_wqc(&c, m).unwrap(); + assert_eq!(bytes[0..4], gs.magic(), "{gs:?} {name}"); + let (c2, m2) = from_wqc(&bytes).unwrap(); + assert_eq!((&c2, m2), (&c, m), "{gs:?} {name}"); + assert_eq!(circuit_hash(&c2).unwrap(), circuit_hash(&c).unwrap(), "{gs:?} {name}"); + assert_eq!(c2.simulate().unwrap(), c.simulate().unwrap(), "{gs:?} {name}"); + } + } + } + // circuit2's four: (file, bytes, circuit_hash). QFT-5 is 263 bytes + // here and 261 in circuit/1. + let want = [ + ("bell", 218, "6f6199943059fc4d90554d7c33f1ac69723603f5128962a6e3cac9c765484d2b"), + ("ghz3", 159, "c5911b898a899c999ce01730bb825c84678569493ac513760e15dcbccd84227f"), + ("qft3", 193, "c23a1b1e31b87705999cbc54866f4c9e3e4c4d2b274d6260672121abb5ca89b8"), + ("qft5", 263, "8559c83769ac8c0cb3aabc64672233aa569cd4ce92aa6545f1449982b9aa8b8c"), + ]; + for ((name, c, m), (file, len, hash)) in corpus_circuits(Gateset::V2).into_iter().zip(want) { + assert_eq!(name, file); + assert_eq!(to_wqc(&c, m).unwrap().len(), len, "circuit2 {name}.wqc"); + assert_eq!(hx(&circuit_hash(&c).unwrap()), hash, "circuit2 {name}"); + } + + let [(_, bell, _), ..] = corpus_circuits(Gateset::V1); + let oplog = oplog_bytes(&bell); + let lax: [&[u8]; 3] = [ + br#"{"n_qubits":2,"gateset":"cliffordT+dyadicP","frac":30,"numeric":"wai.det.fixed64","ext":"wai.quantum.circuit/1"}"#, + b"{ \"ext\" : \"wai.quantum.circuit/1\",\n \"n_qubits\" : 2,\t\"numeric\":\"wai.det.fixed64\", \"frac\":30, \"gateset\":\"cliffordT+dyadicP\" }\n", + br#"{"ext":"wai.quantum.circuit/1","n_qubits":2,"numeric":"wai.det.fixed64","frac":30,"gateset":"cliffordT+dyadicP","note":"x"}"#, + ]; + for contract in lax { + let (back, m) = from_wqc(&wqc_raw(b"WQC1", &[(SECT_CONTRACT, contract), (SECT_OPLOG, &oplog)])).unwrap(); + assert_eq!((&back, m), (&bell, None), "{}", String::from_utf8_lossy(contract)); + assert_eq!(circuit_hash(&back).unwrap(), circuit_hash(&bell).unwrap()); + } + let canon = Gateset::V1.contract_bytes(2); + let trailing = [oplog.as_slice(), &[0, 1, 2]].concat(); + let (back, _) = from_wqc(&wqc_raw(b"WQC1", &[(SECT_CONTRACT, &canon), (SECT_OPLOG, &trailing)])).unwrap(); + assert_eq!(back, bell); + let lax_measures: [(&[u8], u64, u64); 3] = [ + (br#"{"seed": 1, "shots": 10, "basis": "computational"}"#, 1, 10), + (br#"{"seed":1,"basis":"computational"}"#, 1, 0), + (br#"{"seed":1,"shots":10,"basis":"x"}"#, 1, 10), + ]; + for (measure, seed, shots) in lax_measures { + let file = wqc_raw(b"WQC1", &[(SECT_CONTRACT, &canon), (SECT_OPLOG, &oplog), (SECT_MEASURE, measure)]); + assert_eq!(from_wqc(&file).unwrap(), (bell.clone(), Some(Measure { seed, shots }))); + } + for gs in Gateset::ALL { + let mut c = bell.clone(); + c.gateset = gs; + let file = wqc_raw( + &gs.magic(), + &[(0x7f, b"later"), (SECT_CONTRACT, &gs.contract_bytes(2)), (SECT_OPLOG, &oplog), (0x04, b"")], + ); + assert_eq!(from_wqc(&file).unwrap(), (c, None), "{gs:?}"); + } + } + + /// The reject corpus: each case is an accepted file with one defect, named + /// with the error class a reader must give it. + fn reject_cases() -> Vec<(&'static str, Vec, &'static str)> { + let bell = oplog_of(&[op(4, &[], 0, 0), op(1, &[0], 1, 0)]); + let canon1 = Gateset::V1.contract_bytes(2); + let canon2 = Gateset::V2.contract_bytes(2); + let v1 = |contract: &[u8]| wqc_raw(b"WQC1", &[(SECT_CONTRACT, contract), (SECT_OPLOG, &bell)]); + let v1_oplog = |oplog: &[u8]| wqc_raw(b"WQC1", &[(SECT_CONTRACT, &canon1), (SECT_OPLOG, oplog)]); + let v2 = |contract: &[u8], oplog: &[u8], measure: Option<&[u8]>| { + let mut sections = vec![(SECT_CONTRACT, contract), (SECT_OPLOG, oplog)]; + sections.extend(measure.map(|m| (SECT_MEASURE, m))); + wqc_raw(b"WQC2", §ions) + }; + let v2_contract = |contract: &[u8]| v2(contract, &bell, None); + let v2_oplog = |oplog: &[u8]| v2(&canon2, oplog, None); + let v2_measure = |measure: &[u8]| v2(&canon2, &bell, Some(measure)); + let mut magic_wqc3 = v2_contract(&canon2); + magic_wqc3[0..4].copy_from_slice(b"WQC3"); + let n_ops_u32max = [u32::MAX.to_le_bytes().as_slice(), &op(4, &[], 0, 0)].concat(); + assert_eq!(n_ops_u32max.len(), 9); + let wrap = wqc_table( + b"WQC1", + &[(SECT_CONTRACT, 0, canon1.len() as u32), (SECT_OPLOG, 0xFFFF_FFF0, 0x20)], + &[canon1.as_slice(), &bell].concat(), + ); + let reordered2 = format!( + r#"{{"n_qubits":2,"ext":"{}","numeric":"{}","frac":30,"gateset":"{}"}}"#, + Gateset::V2.ext(), + Gateset::V2.numeric(), + Gateset::V2.name() + ); + vec![ + ("magic_wqc3", magic_wqc3, "malformed"), + ("v1_ext_other", v1(&swap(&canon1, "circuit/1", "circuit/2")), "contract"), + ("v1_numeric_other", v1(&swap(&canon1, "wai.det.fixed64", "wai.det.amp64")), "contract"), + ("v1_frac_31", v1(&swap(&canon1, "\"frac\":30", "\"frac\":31")), "contract"), + ("v1_frac_float", v1(&swap(&canon1, "\"frac\":30", "\"frac\":30.0")), "contract"), + ("v1_gateset_rn", v1(&swap(&canon1, "dyadicP\"", "dyadicP.rn\"")), "contract"), + ("v1_contract2_in_wqc1", v1(&canon2), "contract"), + ("v1_fields_missing", v1(br#"{"n_qubits":3}"#), "contract"), + ("v1_contract_not_object", v1(&[b"[".as_slice(), &canon1, b"]"].concat()), "contract"), + ("v1_n_qubits_256", v1(&swap(&canon1, "\"n_qubits\":2", "\"n_qubits\":256")), "malformed"), + ("v1_n_qubits_258", v1(&swap(&canon1, "\"n_qubits\":2", "\"n_qubits\":258")), "malformed"), + ( + "v1_contract_twice", + wqc_raw(b"WQC1", &[(SECT_CONTRACT, &canon1), (SECT_OPLOG, &bell), (SECT_CONTRACT, &Gateset::V1.contract_bytes(3))]), + "malformed", + ), + ( + "v1_oplog_twice", + wqc_raw(b"WQC1", &[(SECT_CONTRACT, &canon1), (SECT_OPLOG, &bell), (SECT_OPLOG, &oplog_of(&[op(1, &[], 0, 0)]))]), + "malformed", + ), + ("v1_n_ops_u32max", v1_oplog(&n_ops_u32max), "malformed"), + ("v1_offset_wrap", wrap, "malformed"), + ("v2_contract_whitespace", v2_contract(&swap(&canon2, "\"n_qubits\":2", "\"n_qubits\": 2")), "contract"), + ("v2_contract_key_order", v2_contract(reordered2.as_bytes()), "contract"), + ("v2_contract_extra_member", v2_contract(&swap(&canon2, "}", ",\"note\":\"x\"}")), "contract"), + ("v2_contract1_in_wqc2", v2_contract(&canon1), "contract"), + ( + "v2_duplicate_controls", + v2(&Gateset::V2.contract_bytes(3), &oplog_of(&[op(1, &[0, 0], 2, 0)]), None), + "invalid", + ), + ("v2_param_on_h", v2_oplog(&oplog_of(&[op(4, &[], 0, 7), op(1, &[0], 1, 0)])), "invalid"), + ("v2_oplog_trailing_byte", v2_oplog(&[bell.as_slice(), &[0]].concat()), "malformed"), + ("v2_opcode_10", v2_oplog(&oplog_of(&[op(10, &[], 0, 0)])), "malformed"), + ("v2_measure_spaced", v2_measure(br#"{"seed": 1, "shots": 10, "basis": "computational"}"#), "malformed"), + ("v2_measure_no_shots", v2_measure(br#"{"seed":1,"basis":"computational"}"#), "malformed"), + ("v2_measure_basis_x", v2_measure(br#"{"seed":1,"shots":10,"basis":"x"}"#), "malformed"), + ] + } + + /// Every case of the reject corpus fails with its class; so do the + /// structural edge cases; the rules apply in the order the spec gives. + /// More than 255 controls is refused everywhere a circuit is checked, and + /// the widest circuit2 gate round-trips. An op-log that claims `u32::MAX` + /// ops reserves for the ops its bytes can hold, not for the claim, and no + /// section range can wrap. + #[test] + fn container_rejects() { + let bell = oplog_of(&[op(4, &[], 0, 0), op(1, &[0], 1, 0)]); + let (canon1, canon2) = (Gateset::V1.contract_bytes(2), Gateset::V2.contract_bytes(2)); + let measure = measure_bytes(Measure { seed: 1, shots: 10 }); + // The bases the cases are built from are accepted. + for file in [ + wqc_raw(b"WQC1", &[(SECT_CONTRACT, &canon1), (SECT_OPLOG, &bell)]), + wqc_raw(b"WQC2", &[(SECT_CONTRACT, &canon2), (SECT_OPLOG, &bell)]), + wqc_raw(b"WQC2", &[(SECT_CONTRACT, &canon2), (SECT_OPLOG, &bell), (SECT_MEASURE, &measure)]), + ] { + from_wqc(&file).unwrap(); + } + + let cases = reject_cases(); + assert_eq!(cases.len(), 26); + let names: std::collections::BTreeSet<_> = cases.iter().map(|c| c.0).collect(); + assert_eq!(names.len(), cases.len(), "case names repeat"); + for (name, file, kind) in &cases { + match from_wqc(file) { + Ok(c) => panic!("{name} was accepted: {c:?}"), + Err(e) => assert_eq!(e.kind(), *kind, "{name}: {e}"), + } + } + + // Structure, beyond the corpus. + let structural: [(&str, Vec, &str); 9] = [ + ("short", b"WQC1\x00".to_vec(), "malformed"), + ("table_cut", wqc_raw(b"WQC1", &[(SECT_CONTRACT, &canon1), (SECT_OPLOG, &bell)])[..20].to_vec(), "malformed"), + ("no_contract", wqc_raw(b"WQC2", &[(SECT_OPLOG, &bell)]), "malformed"), + ("no_oplog", wqc_raw(b"WQC2", &[(SECT_CONTRACT, &canon2)]), "malformed"), + ("past_end", wqc_table(b"WQC1", &[(SECT_CONTRACT, 0, 112), (SECT_OPLOG, 112, 16)], &[canon1.as_slice(), &bell].concat()), "malformed"), + ("contract_not_json", wqc_raw(b"WQC1", &[(SECT_CONTRACT, b"{\"ext\":"), (SECT_OPLOG, &bell)]), "contract"), + ( + "v2_n_qubits_256", + wqc_raw(b"WQC2", &[(SECT_CONTRACT, &swap(&canon2, "\"n_qubits\":2", "\"n_qubits\":256")), (SECT_OPLOG, &bell)]), + "malformed", + ), + ( + "v1_n_qubits_negative", + wqc_raw(b"WQC1", &[(SECT_CONTRACT, &swap(&canon1, "\"n_qubits\":2", "\"n_qubits\":-2")), (SECT_OPLOG, &bell)]), + "malformed", + ), + ( + "v1_measure_not_json", + wqc_raw(b"WQC1", &[(SECT_CONTRACT, &canon1), (SECT_OPLOG, &bell), (SECT_MEASURE, b"seed=1")]), + "malformed", + ), + ]; + for (name, file, kind) in &structural { + assert_eq!(from_wqc(file).map(|_| ()).unwrap_err().kind(), *kind, "{name}"); + } + // An error quotes at most 64 characters of what the file holds. + let long = swap(&canon1, "wai.quantum.circuit/1", &"é".repeat(10_000)); + let e = from_wqc(&wqc_raw(b"WQC1", &[(SECT_CONTRACT, &long), (SECT_OPLOG, &bell)])).unwrap_err(); + assert_eq!(e.kind(), "contract"); + assert!(e.to_string().chars().count() < 160, "{e}"); + + // The order: a repeated section before the contract, the contract + // before the op-log, the op-log before validation, validation before + // the measurement. + let first = |sections: &[(u8, &[u8])]| from_wqc(&wqc_raw(b"WQC2", sections)).unwrap_err().kind(); + let canon3 = Gateset::V2.contract_bytes(3); + let spaced = swap(&canon3, ":3", ": 3"); + let repeats = oplog_of(&[op(1, &[0, 0], 2, 0)]); + assert_eq!(first(&[(SECT_CONTRACT, &spaced), (SECT_OPLOG, &repeats), (SECT_OPLOG, &repeats)]), "malformed"); + assert_eq!(first(&[(SECT_CONTRACT, &spaced), (SECT_OPLOG, &[0, 0])]), "contract"); + assert_eq!(first(&[(SECT_CONTRACT, &canon3), (SECT_OPLOG, &[repeats.as_slice(), &[0]].concat())]), "malformed"); + assert_eq!(first(&[(SECT_CONTRACT, &canon3), (SECT_OPLOG, &repeats), (SECT_MEASURE, b"[]")]), "invalid"); + + // More than 255 controls: refused by validate, to_wqc and circuit_hash + // alike, in both gatesets. + for gs in Gateset::ALL { + let mut c = Circuit::with_gateset(2, gs); + c.ops.push(Gate { base: BaseGate::X, controls: vec![0; 256], target: 1, param: 0 }); + let e = c.validate().unwrap_err(); + assert_eq!(e.kind(), "invalid", "{gs:?}"); + assert_eq!(to_wqc(&c, None).unwrap_err(), e, "{gs:?}"); + assert_eq!(circuit_hash(&c).unwrap_err(), e, "{gs:?}"); + } + // 254 distinct controls on 255 qubits is the widest circuit2 gate. + let mut wide = Circuit::with_gateset(255, Gateset::V2); + wide.ops.push(Gate { base: BaseGate::X, controls: (0..254).collect(), target: 254, param: 0 }); + let bytes = to_wqc(&wide, None).unwrap(); + let (back, _) = from_wqc(&bytes).unwrap(); + assert_eq!(back, wide); + assert_eq!(circuit_hash(&back).unwrap(), circuit_hash(&wide).unwrap()); + + // A 9-byte op-log claiming u32::MAX ops: the reservation is what 9 + // bytes can hold, one op, and the decode is malformed at the second. + assert_eq!(oplog_capacity(u32::MAX, 9), 1); + assert_eq!(oplog_capacity(u32::MAX, 3), 0); + assert_eq!(oplog_capacity(3, 1 << 20), 3); + let n_ops_u32max = [u32::MAX.to_le_bytes().as_slice(), &op(4, &[], 0, 0)].concat(); + assert_eq!(parse_oplog(&n_ops_u32max).unwrap_err(), QuantumError::Malformed("op header".into())); + + // Section ranges are checked, so an offset that would wrap a 32-bit + // usize is out of bounds there, as it is here. + assert_eq!(section_range(24, 0, 112, 136), Some(24..136)); + assert_eq!(section_range(24, 0, 113, 136), None); + assert_eq!(section_range(usize::MAX - 8, 0xFFFF_FFF0, 0x20, usize::MAX), None); + assert_eq!(section_range(usize::MAX - 0x10, 0x8, 0x20, usize::MAX), None); + assert_eq!(section_range(usize::MAX - 0x40, 0x10, 0x20, usize::MAX), Some(usize::MAX - 0x30..usize::MAX - 0x10)); } diff --git a/wai/wai-quantum/src/quantum_receipt.rs b/wai/wai-quantum/src/quantum_receipt.rs index 3efbafe74..ba7feeaf3 100644 --- a/wai/wai-quantum/src/quantum_receipt.rs +++ b/wai/wai-quantum/src/quantum_receipt.rs @@ -181,7 +181,7 @@ impl QuantumReceipt { histogram_hash: sv.histogram_hash(m.seed, m.shots), }); let mut r = QuantumReceipt { - circuit_hash: circuit_hash(circuit), + circuit_hash: circuit_hash(circuit)?, statevector_hash, n_qubits: circuit.n_qubits, n_ops: circuit.ops.len() as u32, @@ -240,7 +240,7 @@ impl QuantumReceipt { /// The re-simulation half of [`Self::verify_reconstruction`], shared with /// the labelled form (`crate::quantum_energy::Labelled`). pub(crate) fn reconstruction_matches(&self, circuit: &Circuit) -> bool { - if circuit_hash(circuit) != self.circuit_hash { + if circuit_hash(circuit).ok() != Some(self.circuit_hash) { return false; } if circuit.n_qubits != self.n_qubits || circuit.ops.len() as u32 != self.n_ops { diff --git a/wai/wai-quantum/src/quantum_toolchain.rs b/wai/wai-quantum/src/quantum_toolchain.rs index c9ffd399c..9bd7bd97a 100644 --- a/wai/wai-quantum/src/quantum_toolchain.rs +++ b/wai/wai-quantum/src/quantum_toolchain.rs @@ -26,7 +26,7 @@ //! Continuous rotations (`RX(θ)` for irrational θ) are out of the exact set by //! construction, not omission. -use crate::quantum::{circuit_hash, Amp, BaseGate, Circuit, Gate, StateVector, FRAC}; +use crate::quantum::{circuit_hash, Amp, BaseGate, Circuit, Gate, QuantumError, StateVector, FRAC}; // =========================================================================== // Algorithm library @@ -1036,8 +1036,10 @@ fn push_ry(c: &mut Circuit, target: u8, t: Turn) { c.ops.push(one(BaseGate::S)); } -/// The content hash of a circuit — the identity a run receipt binds. -pub fn circuit_id(c: &Circuit) -> [u8; 32] { +/// The content hash of a circuit — the identity a run receipt binds. It is +/// [`circuit_hash`], so it covers the circuit's gateset, and fails as that +/// does on a circuit its gateset does not allow. +pub fn circuit_id(c: &Circuit) -> Result<[u8; 32], QuantumError> { circuit_hash(c) } -- 2.54.0 (Apple Git-157) From e546a87a2fcac01ea741fb18a65e4fb8590466d6 Mon Sep 17 00:00:00 2001 From: Transaction Science Date: Fri, 9 Oct 2026 11:19:49 -0400 Subject: [PATCH 04/13] 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 --- wai/wai-quantum/examples/bell.rs | 14 +- wai/wai-quantum/examples/receipt.rs | 12 +- wai/wai-quantum/src/quantum_fabric.rs | 193 +++++++++++++++++------ wai/wai-quantum/src/quantum_qir.rs | 16 ++ wai/wai-quantum/src/quantum_receipt.rs | 162 ++++++++++++++++++- wai/wai-quantum/src/quantum_spd.rs | 48 +++++- wai/wai-quantum/src/quantum_toolchain.rs | 163 +++++++++++++++---- 7 files changed, 512 insertions(+), 96 deletions(-) diff --git a/wai/wai-quantum/examples/bell.rs b/wai/wai-quantum/examples/bell.rs index 0045ac07f..538bb24a8 100644 --- a/wai/wai-quantum/examples/bell.rs +++ b/wai/wai-quantum/examples/bell.rs @@ -3,16 +3,26 @@ //! //! cargo run --example bell -use wai_quantum::quantum::Circuit; +use wai_quantum::quantum::{circuit_hash, Circuit}; use wai_quantum::quantum_toolchain as qt; fn main() { + // A new circuit is in the circuit2 gateset, whose phase table is correctly + // rounded; `Circuit::with_gateset(2, Gateset::V1)` reproduces circuit/1. let mut c = Circuit::new(2); c.h(0).cx(0, 1); // a Bell pair let sv = c.simulate().expect("simulate"); + // The circuit's identity covers its capability; a circuit that simulates + // always has one. + let id = circuit_hash(&c).expect("a valid circuit has a hash"); - println!("circuit {} qubits, {} gates", c.n_qubits, c.ops.len()); + println!("circuit {} qubits, {} gates, {}", c.n_qubits, c.ops.len(), c.gateset.capability()); + print!("circuit-id "); + for b in id { + print!("{b:02x}"); + } + println!(); let (backend, why) = qt::recommend_backend(&c); println!("backend {backend:?} — {why}"); diff --git a/wai/wai-quantum/examples/receipt.rs b/wai/wai-quantum/examples/receipt.rs index 68914bbad..68749b0aa 100644 --- a/wai/wai-quantum/examples/receipt.rs +++ b/wai/wai-quantum/examples/receipt.rs @@ -4,7 +4,7 @@ //! cargo run --example receipt use wai_quantum::ed25519_dalek::SigningKey; -use wai_quantum::quantum::Measure; +use wai_quantum::quantum::{Gateset, Measure}; use wai_quantum::quantum_receipt::QuantumReceipt; use wai_quantum::quantum_toolchain as qt; @@ -22,8 +22,18 @@ fn main() { ) .expect("seal"); + println!("capability {}", circuit.gateset.capability()); println!("verifies {}", receipt.verify()); println!("re-simulates to claim {}", receipt.verify_reconstruction(&circuit)); + + // The receipt binds the circuit's gateset: the same ops in circuit/1 do not + // reconstruct it, and the receipt says which gateset they need. + let mut v1 = circuit.clone(); + v1.gateset = Gateset::V1; + println!("same ops, circuit/1 {}", receipt.verify_reconstruction(&v1)); + if let Some(note) = receipt.gateset_note(&v1) { + println!(" {note}"); + } println!("work {} amplitude updates", receipt.work_amp_updates); println!("energy {} uJ (0 = unmetered)", receipt.joules_micro); diff --git a/wai/wai-quantum/src/quantum_fabric.rs b/wai/wai-quantum/src/quantum_fabric.rs index 5d37fe4ad..65cbcf42b 100644 --- a/wai/wai-quantum/src/quantum_fabric.rs +++ b/wai/wai-quantum/src/quantum_fabric.rs @@ -28,14 +28,20 @@ //! //! Numerics decide how much parity a fabric can earn. Most accelerators carry //! no 64-bit float, and some no 64-bit integer. The gate kernel of the -//! fixed-point core ([`KERNEL_CIRCUIT_APPLY`]) needs neither: for the -//! amplitudes and gate entries of a valid circuit, each product fits in 64 -//! bits and each result in 32. So a fabric with only 32-bit integer lanes can -//! reproduce it bit for bit. [`lanes`] is that kernel written with only the -//! operations such a fabric has, and its certificate against the native -//! simulator is bit-exact. The floating-point modules can earn bit-exactness -//! only on a fabric with 64-bit floats and the same order of operations; -//! elsewhere their parity is a measured bound. +//! fixed-point core needs neither: for the amplitudes and gate entries of a +//! valid circuit, each product fits in 64 bits and each result in 32. So a +//! fabric with only 32-bit integer lanes can reproduce it bit for bit. +//! [`lanes`] is that kernel written with only the operations such a fabric +//! has, and its certificate against the native simulator is bit-exact. The +//! floating-point modules can earn bit-exactness only on a fabric with 64-bit +//! floats and the same order of operations; elsewhere their parity is a +//! measured bound. +//! +//! The gate kernel is named once per capability, because the capabilities' +//! phase tables differ: [`KERNEL_CIRCUIT_APPLY`] for `wai.quantum.circuit` +//! ([`Gateset::V1`]) and [`KERNEL_CIRCUIT2_APPLY`] for `wai.quantum.circuit2` +//! ([`Gateset::V2`]). A certificate names its kernel, so it holds for one +//! table only. //! //! # Checked //! @@ -46,7 +52,9 @@ //! for every input including the most negative 32-bit value. //! - **The lane simulator is the native one.** Random circuits of every gate //! the core supports, dyadic phases to `P(32)` and up to three controls, on -//! up to ten qubits: every final amplitude bit-identical. The lane kernel +//! up to ten qubits, under each gateset: every final amplitude +//! bit-identical. The corpus digest of each gateset's certificate is pinned, +//! so a certificate issued under either one can be re-earned. The lane kernel //! runs the general 2×2 path for every gate, where the native one takes //! shortcuts; the shortcuts are exact by construction, and the certificate //! confirms it. The lane simulator refuses every circuit the native one @@ -63,7 +71,8 @@ //! implementation and this member,** earned on a corpus unless it is the CPU //! reference itself; it prefers fewer joules per operation, a known figure //! over none, and is deterministic on ties. A bound does not stand in for a -//! fidelity. +//! fidelity, and a certificate earned under one gateset does not serve the +//! other gateset's kernel. //! - **Receipts verify, refuse tampering, refuse an uncertified kernel, and //! refuse a joule figure without its acquisition class.** Moving bytes //! between the named fields changes what is signed. @@ -86,7 +95,7 @@ //! - **Only the gate kernel is in lanes.** Sampling, probability weights and //! fidelities in the core use 128-bit sums and are not ported. -use crate::quantum::{Circuit, FRAC, MAX_QUBITS, ONE, QuantumError}; +use crate::quantum::{Circuit, FRAC, Gateset, MAX_QUBITS, ONE, QuantumError}; use crate::quantum_energy::{EnergyClass, check_label}; use crate::quantum_ops::content_hash; use ed25519_dalek::{Signature, Signer, SigningKey, Verifier, VerifyingKey}; @@ -95,15 +104,41 @@ use serde_json::{Map, Value, json}; pub use crate::quantum_ops::CAP_QUANTUM_FABRIC; /// The kernel that applies one controlled one-qubit gate to the fixed-point -/// state vector, the step every circuit of the core is made of. +/// state vector, the step every circuit of the core is made of, with the +/// `wai.quantum.circuit` table ([`Gateset::V1`]). pub const KERNEL_CIRCUIT_APPLY: &str = "wai.quantum.circuit/apply"; +/// The same kernel with the `wai.quantum.circuit2` table ([`Gateset::V2`]). +pub const KERNEL_CIRCUIT2_APPLY: &str = "wai.quantum.circuit2/apply"; + /// The implementation name of the crate's own kernel. pub const REFERENCE: &str = "reference"; -/// The corpus [`certify_lanes`] draws, by name and version. +/// The corpus [`certify_lanes`] draws for [`Gateset::V1`], by name and version. pub const GENERATOR_RANDOM_CIRCUITS: &str = "wai.quantum.circuit/random-v1"; +/// The corpus [`certify_lanes`] draws for [`Gateset::V2`]: the same circuits as +/// [`GENERATOR_RANDOM_CIRCUITS`], in the circuit2 gateset. +pub const GENERATOR_RANDOM_CIRCUITS2: &str = "wai.quantum.circuit2/random-v1"; + +/// The gate kernel's name for circuits in `gs`: [`KERNEL_CIRCUIT_APPLY`] or +/// [`KERNEL_CIRCUIT2_APPLY`]. +pub const fn apply_kernel(gs: Gateset) -> &'static str { + match gs { + Gateset::V1 => KERNEL_CIRCUIT_APPLY, + Gateset::V2 => KERNEL_CIRCUIT2_APPLY, + } +} + +/// The name of the random corpus [`certify_lanes`] draws in `gs`: +/// [`GENERATOR_RANDOM_CIRCUITS`] or [`GENERATOR_RANDOM_CIRCUITS2`]. +pub const fn random_generator(gs: Gateset) -> &'static str { + match gs { + Gateset::V1 => GENERATOR_RANDOM_CIRCUITS, + Gateset::V2 => GENERATOR_RANDOM_CIRCUITS2, + } +} + const DOMAIN_FABRIC: &[u8] = b"wai:quantum-fabric\x01"; const DOMAIN_FABRIC_ID: &[u8] = b"wai:quantum-fabric-id\x01"; const DOMAIN_CERT: &[u8] = b"wai:quantum-fabric-certificate\x01"; @@ -347,16 +382,18 @@ fn vector_units() -> (Vec<&'static str>, &'static str) { } /// The member this process runs on: the CPU, offering the reference -/// implementation of [`KERNEL_CIRCUIT_APPLY`]. A port such as the 32-bit -/// lanes joins it once [`certify_lanes`] has certified it. +/// implementation of the gate kernel for each gateset, [`KERNEL_CIRCUIT_APPLY`] +/// and [`KERNEL_CIRCUIT2_APPLY`]. A port such as the 32-bit lanes joins it +/// once [`certify_lanes`] has certified it. pub fn detect_local() -> Member { let arch = std::env::consts::ARCH; let (units, how) = vector_units(); let threads = if cfg!(target_arch = "wasm32") { 1 } else { std::thread::available_parallelism().map_or(1, |n| n.get()) }; let units = if units.is_empty() { "none found".to_string() } else { units.join(", ") }; let id = format!("local/cpu-{arch}"); + let reference = |kernel: &str| KernelEntry { kernel: kernel.into(), implementation: REFERENCE.into(), certificate: Certificate::reference(kernel, &id), energy: None }; Member { - kernels: vec![KernelEntry { kernel: KERNEL_CIRCUIT_APPLY.into(), implementation: REFERENCE.into(), certificate: Certificate::reference(KERNEL_CIRCUIT_APPLY, &id), energy: None }], + kernels: Gateset::ALL.into_iter().map(|gs| reference(apply_kernel(gs))).collect(), id, class: FabricClass::Cpu, presence: Presence::Detected, @@ -560,20 +597,22 @@ pub fn certify(kernel: &str, implementation: &str, member: &str, generator: &str } } -/// Certify the 32-bit-lane port of [`KERNEL_CIRCUIT_APPLY`], as -/// implementation `"lanes32"` on `member`, against the native simulator on -/// `circuits` random circuits drawn from `seed`. -pub fn certify_lanes(member: &str, circuits: usize, seed: u64) -> Certificate { +/// Certify the 32-bit-lane port of the gate kernel for `gs` +/// ([`apply_kernel`]), as implementation `"lanes32"` on `member`, against the +/// native simulator on `circuits` random circuits in `gs` drawn from `seed` +/// ([`random_generator`]). The two gatesets draw the same ops from the same +/// seed; their certificates differ in kernel, generator and corpus. +pub fn certify_lanes(gs: Gateset, member: &str, circuits: usize, seed: u64) -> Certificate { let mut s = seed; let cases = (0..circuits).map(move |_| { - let c = lanes::random_circuit(&mut s); + let c = lanes::random_circuit(&mut s, gs); let native = c.simulate_from(0).expect("random circuits are valid"); let lane = lanes::simulate(&c).expect("random circuits are valid"); let reference = Output::Fixed { values: native.amps.iter().flat_map(|a| [a.re, a.im]).collect(), frac: FRAC }; let port = Output::Fixed { values: lane.iter().flat_map(|a| [a[0] as i64, a[1] as i64]).collect(), frac: FRAC }; (lanes::circuit_bytes(&c), reference, port) }); - certify(KERNEL_CIRCUIT_APPLY, "lanes32", member, &format!("{GENERATOR_RANDOM_CIRCUITS} seed={seed} cases={circuits}"), cases) + certify(apply_kernel(gs), "lanes32", member, &format!("{} seed={seed} cases={circuits}", random_generator(gs)), cases) } /// Certify a sampling implementation of `kernel` on `member`: each case is an @@ -891,12 +930,13 @@ pub mod lanes { z ^ (z >> 31) } - /// A random circuit of the core's gates: two to ten qubits, up to three - /// controls, dyadic phases up to `P(32)`. It is a circuit/1 circuit, as the - /// kernel and generator it is certified under name. - pub fn random_circuit(s: &mut u64) -> Circuit { + /// A random circuit of the core's gates in `gs`: two to ten qubits, up to + /// three distinct controls, dyadic phases up to `P(32)` and no parameter + /// on any other gate, so it is valid in either gateset. The ops drawn from + /// a seed are the same in both. + pub fn random_circuit(s: &mut u64, gs: Gateset) -> Circuit { let n = 2 + (next(s) % 9) as u8; - let mut c = Circuit::with_gateset(n, Gateset::V1); + let mut c = Circuit::with_gateset(n, gs); let bases = [BaseGate::I, BaseGate::X, BaseGate::Y, BaseGate::Z, BaseGate::H, BaseGate::S, BaseGate::Sdg, BaseGate::T, BaseGate::Tdg, BaseGate::P]; for _ in 0..(10 + next(s) % 50) { let base = bases[(next(s) % bases.len() as u64) as usize]; @@ -982,18 +1022,72 @@ mod tests { #[test] fn the_lane_simulator_is_the_native_one() { - let cert = certify_lanes("local/test", 300, 7); - assert_eq!(cert.parity, Parity::BitExact, "{cert:?}"); - assert_eq!(cert.cases, 300); - assert_eq!(cert.generator, format!("{GENERATOR_RANDOM_CIRCUITS} seed=7 cases=300")); - // The same corpus, the same certificate; another seed, another corpus. - assert_eq!(certify_lanes("local/test", 300, 7).hash(), cert.hash()); - assert_ne!(certify_lanes("local/test", 300, 8).corpus, cert.corpus); - // The corpus is not trivial: some final states spread over many - // amplitudes, with phases. - let mut s = 7u64; - let spread = (0..300).filter(|_| lanes::simulate(&lanes::random_circuit(&mut s)).unwrap().iter().filter(|a| a[0] != 0 || a[1] != 0).count() >= 4).count(); - assert!(spread >= 100, "{spread} of 300 circuits spread over four amplitudes or more"); + for gs in Gateset::ALL { + let cert = certify_lanes(gs, "local/test", 300, 7); + assert_eq!(cert.parity, Parity::BitExact, "{cert:?}"); + assert_eq!(cert.cases, 300); + assert_eq!(cert.generator, format!("{} seed=7 cases=300", random_generator(gs))); + // The same corpus, the same certificate; another seed, another corpus. + assert_eq!(certify_lanes(gs, "local/test", 300, 7).hash(), cert.hash()); + assert_ne!(certify_lanes(gs, "local/test", 300, 8).corpus, cert.corpus); + // The corpus is not trivial: some final states spread over many + // amplitudes, with phases. + let mut s = 7u64; + let spread = (0..300).filter(|_| lanes::simulate(&lanes::random_circuit(&mut s, gs)).unwrap().iter().filter(|a| a[0] != 0 || a[1] != 0).count() >= 4).count(); + assert!(spread >= 100, "{gs:?}: {spread} of 300 circuits spread over four amplitudes or more"); + } + } + + fn hex(b: &[u8]) -> String { + b.iter().map(|x| format!("{x:02x}")).collect() + } + + /// The lanes32 corpus of each gateset, seed 7, 200 cases. The circuit/1 + /// digest is the one 0.3.41 certified, so its certificates can still be + /// re-earned; the circuit2 digest is computed with the correctly rounded + /// table. The digests must never be edited to make this test pass. + #[test] + fn the_lanes32_corpus_of_each_gateset_is_pinned() { + let v1 = certify_lanes(Gateset::V1, "local/test", 200, 7); + assert_eq!(v1.parity, Parity::BitExact, "{v1:?}"); + assert_eq!(hex(&v1.corpus), "d18454c639acfd9c7b66450f4ded1aac4ab46cc10a7bedbcf4958a96e204fb36"); + let v2 = certify_lanes(Gateset::V2, "local/test", 200, 7); + assert_eq!(v2.parity, Parity::BitExact, "{v2:?}"); + assert_eq!(hex(&v2.corpus), "3dbde276c1095bf827faec71d5980e050f8a55a5ba7d97cd3bbdaec3f43a13b2"); + } + + #[test] + fn a_v2_certificate_is_its_own() { + let v1 = certify_lanes(Gateset::V1, "local/test", 40, 7); + let v2 = certify_lanes(Gateset::V2, "local/test", 40, 7); + assert_eq!((v1.kernel.as_str(), v2.kernel.as_str()), (KERNEL_CIRCUIT_APPLY, KERNEL_CIRCUIT2_APPLY)); + assert_eq!(v1.generator, format!("{GENERATOR_RANDOM_CIRCUITS} seed=7 cases=40")); + assert_eq!(v2.generator, format!("{GENERATOR_RANDOM_CIRCUITS2} seed=7 cases=40")); + assert_eq!(v2.parity, Parity::BitExact, "{v2:?}"); + // The two gatesets draw the same ops from a seed; only the gateset, + // and so the outputs, differ. + let (mut a, mut b) = (7u64, 7u64); + for _ in 0..40 { + let (x, y) = (lanes::random_circuit(&mut a, Gateset::V1), lanes::random_circuit(&mut b, Gateset::V2)); + assert_eq!((x.n_qubits, &x.ops), (y.n_qubits, &y.ops)); + assert_eq!((x.gateset, y.gateset), (Gateset::V1, Gateset::V2)); + y.validate().unwrap(); + } + assert_ne!(v1.corpus, v2.corpus); + assert_ne!(v1.hash(), v2.hash()); + // Each serves its own kernel only, even filed under the other's. + let mut member = detect_local(); + member.id = "local/test".into(); + let filed = |kernel: &str, cert: &Certificate| KernelEntry { kernel: kernel.into(), implementation: "lanes32".into(), certificate: cert.clone(), energy: Some((1, EnergyClass::ModelBased)) }; + for (gs, cert) in [(Gateset::V1, &v1), (Gateset::V2, &v2)] { + let other = Gateset::ALL.into_iter().find(|&o| o != gs).unwrap(); + let mut m = member.clone(); + m.kernels = vec![filed(apply_kernel(gs), cert)]; + assert_eq!(place(std::slice::from_ref(&m), apply_kernel(gs), &Parity::BitExact).unwrap().1.implementation, "lanes32"); + m.kernels = vec![filed(apply_kernel(other), cert)]; + assert!(place(std::slice::from_ref(&m), apply_kernel(other), &Parity::BitExact).is_none(), "a {gs:?} certificate served {}", apply_kernel(other)); + assert!(place(std::slice::from_ref(&m), apply_kernel(gs), &Parity::BitExact).is_none()); + } } #[test] @@ -1144,7 +1238,7 @@ mod tests { let mut local = detect_local(); assert_eq!(local.presence, Presence::Detected); let id = local.id.clone(); - local.kernels.push(KernelEntry { kernel: KERNEL_CIRCUIT_APPLY.into(), implementation: "lanes32".into(), certificate: certify_lanes(&id, 5, 1), energy: None }); + local.kernels.push(KernelEntry { kernel: KERNEL_CIRCUIT_APPLY.into(), implementation: "lanes32".into(), certificate: certify_lanes(Gateset::V1, &id, 5, 1), energy: None }); assert_eq!(place(std::slice::from_ref(&local), KERNEL_CIRCUIT_APPLY, &Parity::BitExact).unwrap().1.implementation, REFERENCE); } @@ -1176,7 +1270,7 @@ mod tests { fn receipts_verify_and_refuse() { let signer = SigningKey::from_bytes(&[7u8; 32]); let mut member = detect_local(); - let cert = certify_lanes(&member.id, 20, 3); + let cert = certify_lanes(Gateset::V1, &member.id, 20, 3); member.kernels.push(KernelEntry { kernel: KERNEL_CIRCUIT_APPLY.into(), implementation: "lanes32".into(), certificate: cert.clone(), energy: None }); let entry = member.kernels.last().unwrap().clone(); let counter = EnergyClass::OnChipCounter { uncertainty: Uncertainty { relative_ppm: 50_000, window_us: 1_000 }, filtering: CounterFiltering::Off }; @@ -1184,8 +1278,9 @@ mod tests { let r = FabricReceipt::seal(&signer, "fabric-test", [9; 32], &member, &entry, joules, class); assert!(r.verify()); assert!(r.certificate_matches(&cert)); - assert!(!r.certificate_matches(&certify_lanes(&member.id, 20, 4))); - assert!(!r.certificate_matches(&certify_lanes("local/other", 20, 3))); + assert!(!r.certificate_matches(&certify_lanes(Gateset::V1, &member.id, 20, 4))); + assert!(!r.certificate_matches(&certify_lanes(Gateset::V1, "local/other", 20, 3))); + assert!(!r.certificate_matches(&certify_lanes(Gateset::V2, &member.id, 20, 3))); let back = FabricReceipt::from_json(&r.to_json()).unwrap(); assert_eq!(back, r); assert!(back.verify()); @@ -1236,7 +1331,15 @@ mod tests { let local = detect_local(); assert_eq!(local.class, FabricClass::Cpu); assert!(local.detail.contains("asked of the CPU") || local.detail.contains("compiled for"), "{}", local.detail); - assert_eq!(local.kernels.len(), 1); - assert_eq!(local.kernels[0].certificate.member, local.id); + // The reference kernel of each gateset, each placeable on its own. + let kernels: Vec<&str> = local.kernels.iter().map(|k| k.kernel.as_str()).collect(); + assert_eq!(kernels, [KERNEL_CIRCUIT_APPLY, KERNEL_CIRCUIT2_APPLY]); + for k in &local.kernels { + assert_eq!((k.certificate.member.as_str(), k.certificate.kernel.as_str(), k.implementation.as_str()), (local.id.as_str(), k.kernel.as_str(), REFERENCE)); + } + for gs in Gateset::ALL { + let (_, k) = place(std::slice::from_ref(&local), apply_kernel(gs), &Parity::BitExact).unwrap(); + assert_eq!((k.kernel.as_str(), k.implementation.as_str()), (apply_kernel(gs), REFERENCE)); + } } } diff --git a/wai/wai-quantum/src/quantum_qir.rs b/wai/wai-quantum/src/quantum_qir.rs index 9833b8e94..0e9951838 100644 --- a/wai/wai-quantum/src/quantum_qir.rs +++ b/wai/wai-quantum/src/quantum_qir.rs @@ -34,6 +34,17 @@ //! global phase, which is physically unobservable — every measurement //! distribution is identical. //! +//! # Gatesets +//! +//! The export writes the ideal angle `2π/2ᵏ` for `P(k)`, whatever the circuit's +//! gateset, and `T` as the `t` intrinsic. That is the circuit2 table's meaning +//! to within its rounding. A circuit/1 circuit ([`Gateset::V1`](crate::quantum::Gateset::V1)) exports the +//! same ideal angles, not its own numerics: circuit/1's `P(k)` deviates from +//! `2π/2ᵏ` by up to 12868 units of `2⁻³⁰` for `k ≥ 8`, and applies no phase at +//! all for `k ≥ 19`, so a module exported from such a circuit is not what the +//! circuit simulates here. Ingest ([`from_qir`]) builds circuits in +//! [`Gateset::V2`](crate::quantum::Gateset::V2). +//! //! # How this is checked //! //! - The exact rewrites (`CY`, `CCX`, `CCZ`) are verified against the simulator @@ -198,6 +209,10 @@ fn declaration(name: &str) -> String { /// Emit `c` as a QIR module. `measure_all` appends an `mz` + result record for /// every qubit (what a base-profile consumer expects to find). +/// +/// Angles are the ideal ones in every gateset: a [`Gateset::V1`](crate::quantum::Gateset::V1) circuit +/// exports angles its own table does not simulate for `P(k ≥ 8)` (see the +/// module's notes on gatesets). pub fn to_qir(c: &Circuit, measure_all: bool) -> String { let mut e = Emitter::new(); for g in &c.ops { @@ -338,6 +353,7 @@ fn arg_double(line: &str) -> Option { /// is calls to the standard `__quantum__qis__*` intrinsics with constant qubit /// operands. Measurement calls (`mz`, `result_record_output`) are recognised and /// skipped — a `Circuit` is unitary, the measurement is the caller's business. +/// The circuit is built in [`Gateset::V2`](crate::quantum::Gateset::V2). /// /// # Errors /// diff --git a/wai/wai-quantum/src/quantum_receipt.rs b/wai/wai-quantum/src/quantum_receipt.rs index ba7feeaf3..e7832dd92 100644 --- a/wai/wai-quantum/src/quantum_receipt.rs +++ b/wai/wai-quantum/src/quantum_receipt.rs @@ -1,5 +1,6 @@ //! Quantum-circuit receipt — the energy-accounted seal for a -//! `wai.quantum.circuit` reconstruction (extensions/quantum-sim § Receipt). +//! `wai.quantum.circuit` or `wai.quantum.circuit2` reconstruction +//! (extensions/quantum-sim § Receipt). //! //! This is the artifact WAI's determinism uniquely enables here: a signed record //! that cleanly **separates the two halves** of a classical quantum-simulation's @@ -25,6 +26,13 @@ //! is unlabelled, with exactly the legacy bytes, and //! `quantum_energy::Labelled::seal` labels and re-signs it (`quantum_energy`). //! +//! The receipt's format is the same for both capabilities. It names its +//! capability through `circuit_hash`, which covers the circuit's contract and +//! so its [`Gateset`]: a circuit/1 receipt re-simulates only against the +//! circuit/1 circuit, never against its circuit2 twin with the same ops. +//! [`QuantumReceipt::gateset_of`] says which gateset a receipt was sealed +//! under, as a diagnosis when a reconstruction does not match. +//! //! It is a JWP profile like every other receipt in this crate — the //! worlds/provenance Merkle (`crate::merkle::merkle_root`) + Ed25519, //! reused, not reinvented — and chains across a derivation via @@ -32,7 +40,7 @@ //! the receipt's value is exactly this: it makes the *cost* of the reconstruction //! auditable, and the *result* reproducible. -use crate::quantum::{circuit_hash, Circuit, Measure, QuantumError}; +use crate::quantum::{circuit_hash, Circuit, Gateset, Measure, QuantumError}; use crate::merkle::merkle_root; use ed25519_dalek::{Signature, Signer, SigningKey, Verifier, VerifyingKey}; @@ -85,10 +93,12 @@ pub struct MeasureRecord { pub histogram_hash: [u8; 32], } -/// A signed, energy-accounted seal over one `wai.quantum.circuit` reconstruction. +/// A signed, energy-accounted seal over one `wai.quantum.circuit` or +/// `wai.quantum.circuit2` reconstruction. #[derive(Clone, Debug, PartialEq)] pub struct QuantumReceipt { - /// Identity: the WQC circuit hash (`crate::quantum::circuit_hash`). + /// Identity: the WQC circuit hash (`crate::quantum::circuit_hash`), which + /// binds the circuit's capability and phase table. pub circuit_hash: [u8; 32], /// The reconstruction result: the `2^n`-amplitude statevector's portable hash. pub statevector_hash: [u8; 32], @@ -165,6 +175,10 @@ impl QuantumReceipt { /// `statevector_hash` is always the true reconstruction, never a caller /// claim. `joules_micro` is the sink's measured energy (`0` if unmetered); /// pass `measure` to also vouch for a shot histogram. + /// + /// The circuit simulates under its own gateset, and the receipt binds that + /// gateset's contract through `circuit_hash`. Fails as + /// [`Circuit::simulate`] does. pub fn seal( signer: &SigningKey, signer_id: impl Into, @@ -180,6 +194,7 @@ impl QuantumReceipt { shots: m.shots, histogram_hash: sv.histogram_hash(m.seed, m.shots), }); + // `simulate` validated the circuit, which bounds its ops by `u32::MAX`. let mut r = QuantumReceipt { circuit_hash: circuit_hash(circuit)?, statevector_hash, @@ -233,16 +248,50 @@ impl QuantumReceipt { /// claimed statevector is not what the circuit actually reconstructs to. /// (The measured `joules_micro` remains attested-only — re-simulation checks /// the *result and cost*, never the signer's energy draw.) + /// + /// The circuit is re-simulated under its own gateset, as decoded from its + /// container. A receipt sealed under one gateset does not verify against + /// the same ops in another; [`Self::gateset_of`] tells which one it was. pub fn verify_reconstruction(&self, circuit: &Circuit) -> bool { self.reconstruction_matches(circuit) && self.verify() } + /// The gateset under which `circuit`'s ops hash to this receipt's + /// `circuit_hash`, whatever gateset `circuit` itself carries; `None` when + /// no gateset does. + /// + /// A migration aid, not a check: it matches the circuit hash alone and + /// re-simulates nothing, and [`Self::verify_reconstruction`] stays strict. + /// Use it to explain a failed reconstruction, as [`Self::gateset_note`] + /// does. + pub fn gateset_of(&self, circuit: &Circuit) -> Option { + let mut twin = circuit.clone(); + Gateset::ALL.into_iter().find(|&gs| { + twin.gateset = gs; + circuit_hash(&twin).ok() == Some(self.circuit_hash) + }) + } + + /// When `circuit`'s ops match this receipt under a gateset other than + /// `circuit`'s own, the text that says so, for example `"this is a + /// circuit/1 receipt; rebuild with Gateset::V1"`. `None` when the gateset + /// is not the reason the reconstruction fails. + pub fn gateset_note(&self, circuit: &Circuit) -> Option { + let gs = self.gateset_of(circuit).filter(|&gs| gs != circuit.gateset)?; + let (name, variant) = match gs { + Gateset::V1 => ("circuit/1", "V1"), + Gateset::V2 => ("circuit2", "V2"), + }; + Some(format!("this is a {name} receipt; rebuild with Gateset::{variant}")) + } + /// The re-simulation half of [`Self::verify_reconstruction`], shared with /// the labelled form (`crate::quantum_energy::Labelled`). pub(crate) fn reconstruction_matches(&self, circuit: &Circuit) -> bool { if circuit_hash(circuit).ok() != Some(self.circuit_hash) { return false; } + // A circuit with a hash is valid, so its ops fit a `u32`. if circuit.n_qubits != self.n_qubits || circuit.ops.len() as u32 != self.n_ops { return false; } @@ -307,6 +356,8 @@ impl QuantumReceipt { } /// Parse a receipt from its canonical JSON (for a verifying sink). + /// `n_qubits` must fit a `u8` and `n_ops` a `u32`; a value that does not + /// is refused, never truncated. pub fn from_json(s: &str) -> Option { let v: serde_json::Value = serde_json::from_str(s).ok()?; let o = v.as_object()?; @@ -326,8 +377,8 @@ impl QuantumReceipt { Some(QuantumReceipt { circuit_hash: from_hex32(o.get("circuit_hash")?.as_str()?)?, statevector_hash: from_hex32(o.get("statevector_hash")?.as_str()?)?, - n_qubits: u("n_qubits")? as u8, - n_ops: u("n_ops")? as u32, + n_qubits: u8::try_from(u("n_qubits")?).ok()?, + n_ops: u32::try_from(u("n_ops")?).ok()?, measurement, merkle_root: from_hex32(o.get("root_hash")?.as_str()?)?, work_amp_updates: u("work_amp_updates")?, @@ -423,6 +474,105 @@ mod tests { assert!(child.verify()); assert_eq!(child.parent_receipt_hash, Some(root.receipt_hash())); } + + /// GHZ-3 in `gs`. Its `H` reads the gateset's table, so the two gatesets + /// give different statevectors as well as different circuit hashes. + fn ghz3(gs: Gateset) -> Circuit { + let mut c = Circuit::with_gateset(3, gs); + c.h(0).cx(0, 1).cx(1, 2); + c + } + + #[test] + fn a_receipt_reconstructs_only_under_its_own_gateset() { + let m = Measure { seed: 7, shots: 1024 }; + for gs in Gateset::ALL { + let r = QuantumReceipt::seal(&key(7), "m", &ghz3(gs), Some(m), 0, None).unwrap(); + assert!(r.verify_reconstruction(&ghz3(gs))); + assert_eq!(r.gateset_of(&ghz3(gs)), Some(gs)); + assert_eq!(r.gateset_note(&ghz3(gs)), None); + for other in Gateset::ALL.into_iter().filter(|&o| o != gs) { + // The twin has the same ops and fails cleanly: no panic, a + // false, and a diagnosis naming the receipt's gateset. + let twin = ghz3(other); + assert!(!r.verify_reconstruction(&twin), "{gs:?} receipt against its {other:?} twin"); + assert_eq!(r.gateset_of(&twin), Some(gs)); + assert!(r.gateset_note(&twin).is_some()); + } + } + // A circuit/1 receipt against its circuit2 twin, by name. + let v1 = QuantumReceipt::seal(&key(7), "m", &ghz3(Gateset::V1), None, 0, None).unwrap(); + let v2_twin = ghz3(Gateset::V2); + assert!(!v1.verify_reconstruction(&v2_twin)); + assert_eq!(v1.gateset_of(&v2_twin), Some(Gateset::V1)); + assert_eq!(v1.gateset_note(&v2_twin).as_deref(), Some("this is a circuit/1 receipt; rebuild with Gateset::V1")); + let v2 = QuantumReceipt::seal(&key(7), "m", &v2_twin, None, 0, None).unwrap(); + assert_eq!(v2.gateset_note(&ghz3(Gateset::V1)).as_deref(), Some("this is a circuit2 receipt; rebuild with Gateset::V2")); + // The gateset is bound even where it changes no amplitude: X and CX + // read no table, so the twins reconstruct to one statevector, and the + // receipt still holds only its own. + let clifford = |gs| { + let mut c = Circuit::with_gateset(2, gs); + c.x(0).cx(0, 1); + c + }; + assert_eq!(clifford(Gateset::V1).simulate().unwrap(), clifford(Gateset::V2).simulate().unwrap()); + let r = QuantumReceipt::seal(&key(7), "m", &clifford(Gateset::V1), None, 0, None).unwrap(); + assert!(r.verify_reconstruction(&clifford(Gateset::V1))); + assert!(!r.verify_reconstruction(&clifford(Gateset::V2))); + // Ops that match under no gateset get no diagnosis. + let mut unrelated = Circuit::with_gateset(3, Gateset::V1); + unrelated.h(1); + assert_eq!(v1.gateset_of(&unrelated), None); + assert_eq!(v1.gateset_note(&unrelated), None); + // Ops only circuit/1 allows still get their diagnosis: repeated + // controls hash in circuit/1, and circuit2 refuses them. + let mut repeated = Circuit::with_gateset(3, Gateset::V1); + repeated.ops.push(crate::quantum::Gate { base: crate::quantum::BaseGate::X, controls: vec![0, 0], target: 1, param: 0 }); + let r = QuantumReceipt::seal(&key(7), "m", &repeated, None, 0, None).unwrap(); + let mut repeated_v2 = repeated.clone(); + repeated_v2.gateset = Gateset::V2; + assert!(!r.verify_reconstruction(&repeated_v2)); + assert_eq!(r.gateset_of(&repeated_v2), Some(Gateset::V1)); + } + + /// A receipt that names one gateset's circuit and claims the other's + /// statevector: well signed and internally consistent, and reconstructed + /// by neither circuit. + #[test] + fn a_cross_gateset_forgery_is_rejected() { + let forger = key(9); + let (seed, shots) = (7, 1024); + for (named, simulated) in [(Gateset::V1, Gateset::V2), (Gateset::V2, Gateset::V1)] { + let mut r = QuantumReceipt::seal(&forger, "m", &ghz3(named), Some(Measure { seed, shots }), 0, None).unwrap(); + let sv = ghz3(simulated).simulate().unwrap(); + assert_ne!(sv.statevector_hash(), r.statevector_hash); + r.statevector_hash = sv.statevector_hash(); + r.measurement = Some(MeasureRecord { seed, shots, histogram_hash: sv.histogram_hash(seed, shots) }); + r.merkle_root = merkle_root(&r.leaves()); + r.sig = forger.sign(&r.signing_payload()).to_bytes(); + assert!(r.verify(), "the forgery is well signed"); + for gs in Gateset::ALL { + assert!(!r.verify_reconstruction(&ghz3(gs)), "named {named:?}, simulated {simulated:?}, checked {gs:?}"); + } + } + } + + #[test] + fn json_refuses_counts_that_do_not_fit() { + let r = QuantumReceipt::seal(&key(5), "m", &ghz3(Gateset::V2), None, 0, None).unwrap(); + let j = r.to_json(); + assert!(j.contains("\"n_qubits\":3,") && j.contains("\"n_ops\":3,"), "{j}"); + // 258 is 2 modulo 256, and 2^32 + 3 is 3 modulo 2^32: neither may be + // read as the smaller number. + assert!(QuantumReceipt::from_json(&j.replace("\"n_qubits\":3,", "\"n_qubits\":258,")).is_none()); + assert!(QuantumReceipt::from_json(&j.replace("\"n_qubits\":3,", "\"n_qubits\":256,")).is_none()); + assert!(QuantumReceipt::from_json(&j.replace("\"n_ops\":3,", "\"n_ops\":4294967299,")).is_none()); + // The largest values that fit still parse. + let widest = j.replace("\"n_qubits\":3,", "\"n_qubits\":255,").replace("\"n_ops\":3,", "\"n_ops\":4294967295,"); + let back = QuantumReceipt::from_json(&widest).expect("fits"); + assert_eq!((back.n_qubits, back.n_ops), (255, u32::MAX)); + } } crate::quantum_energy::labellable!(QuantumReceipt, DOMAIN_RECEIPT_ID); diff --git a/wai/wai-quantum/src/quantum_spd.rs b/wai/wai-quantum/src/quantum_spd.rs index 695034b60..6c6e19729 100644 --- a/wai/wai-quantum/src/quantum_spd.rs +++ b/wai/wai-quantum/src/quantum_spd.rs @@ -831,16 +831,16 @@ pub fn from_dyadic(c: &crate::quantum::Circuit) -> Result } (B::X, _) => { out.push(Op::H(t)); - controlled_phase(&mut out, &ctrls, t, 0.5 * tau); + 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); + 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), + Some(turns) => controlled_phase(&mut out, &ctrls, t, turns * tau)?, None => return Err(SpdError::Unsupported(format!("{base:?} on {} controls", ctrls.len()))), }, } @@ -852,16 +852,24 @@ pub fn from_dyadic(c: &crate::quantum::Circuit) -> Result /// 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) { +fn controlled_phase(out: &mut RotCircuit, controls: &[u32], target: u32, phi: f64) -> Result<(), SpdError> { let mut qs = controls.to_vec(); qs.push(target); - let m = qs.len() as u32; - for mask in 1u32..(1 << m) { + 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(1u32 << m) }); + out.push(Op::Rot { axis, theta: -2.0 * phi * sign / f64::from(subsets) }); } + Ok(()) } #[cfg(test)] @@ -1324,6 +1332,32 @@ mod tests { // ---- 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 = (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::>(), 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() { diff --git a/wai/wai-quantum/src/quantum_toolchain.rs b/wai/wai-quantum/src/quantum_toolchain.rs index 9bd7bd97a..f597de04e 100644 --- a/wai/wai-quantum/src/quantum_toolchain.rs +++ b/wai/wai-quantum/src/quantum_toolchain.rs @@ -25,6 +25,11 @@ //! representable, which is *why* the result is byte-identical on every machine. //! Continuous rotations (`RX(θ)` for irrational θ) are out of the exact set by //! construction, not omission. +//! +//! Gatesets: the algorithms and the OpenQASM importer build circuits from +//! nothing, so in [`crate::quantum::Gateset::V2`], `wai.quantum.circuit2`. A +//! circuit derived from another (a noisy trajectory, a routed circuit) keeps +//! the other's gateset. use crate::quantum::{circuit_hash, Amp, BaseGate, Circuit, Gate, QuantumError, StateVector, FRAC}; @@ -386,7 +391,8 @@ pub fn top_outcomes(sv: &StateVector, top: usize) -> Vec<(usize, f64)> { /// `⟨Z_q⟩` under a per-gate **depolarizing** channel, by averaging over /// `trajectories` random error insertions: after each gate, each qubit it touched /// suffers a uniformly random `X`/`Y`/`Z` with probability `depol_permille`/1000. -/// Each trajectory reuses the byte-exact dense simulator; deterministic in `seed`. +/// Each trajectory reuses the byte-exact dense simulator, in the circuit's own +/// gateset; deterministic in `seed`. /// `⟨Z_q⟩` decays toward 0 as noise or depth grows — decoherence, made measurable. pub fn noisy_expect_z( circuit: &Circuit, @@ -399,7 +405,7 @@ pub fn noisy_expect_z( let mut acc = 0.0; let traj = trajectories.max(1); for _ in 0..traj { - let mut noisy = Circuit::new(circuit.n_qubits); + let mut noisy = circuit.empty_like(); for g in &circuit.ops { noisy.ops.push(g.clone()); for &qb in g.controls.iter().chain(std::iter::once(&g.target)) { @@ -484,11 +490,12 @@ pub struct Routed { /// Route `src` onto a line topology `0-1-…-(n-1)`, inserting SWAPs (as 3·CX) so /// every one-control gate acts on adjacent physical qubits. Gates with ≥2 controls /// are placed on the mapped qubits directly (still correct, not made local). +/// The routed circuit keeps `src`'s gateset. pub fn route_line(src: &Circuit) -> Routed { let n = src.n_qubits; let mut phys: Vec = (0..n).collect(); let mut logq: Vec = (0..n).collect(); - let mut out = Circuit::new(n); + let mut out = src.empty_like(); let mut swaps = 0u32; let (mut s2q, mut nonlocal) = (0u32, 0u32); let do_swap = |out: &mut Circuit, phys: &mut [u8], logq: &mut [u8], a: u8, b: u8| { @@ -585,6 +592,11 @@ pub fn routing_equivalent(src: &Circuit, routed: &Routed) -> bool { /// Emit OpenQASM 3.0 for a circuit (the dyadic gate set maps to standard gates; /// `P(k)` becomes `p(2*pi/2^k)`). +/// +/// The angles are the ideal ones, whatever the circuit's gateset. A +/// [`crate::quantum::Gateset::V1`] circuit exports angles its own table does +/// not simulate: circuit/1's `P(k)` deviates from `2π/2^k` for `k ≥ 8` and +/// applies no phase for `k ≥ 19`. pub fn to_qasm(c: &Circuit) -> String { let mut s = String::from("OPENQASM 3.0;\ninclude \"stdgates.inc\";\n"); s.push_str(&format!("qubit[{}] q;\n", c.n_qubits)); @@ -637,6 +649,13 @@ pub fn to_qasm(c: &Circuit) -> String { /// `rz`, `rx`, `ry`, `sx` and `u*` are exact up to a global phase, which no /// measurement can observe; `crz` is exact outright, because its leftover factor /// lands on the control where it *would* be observable. +/// +/// # Gateset +/// +/// The circuit is built in [`crate::quantum::Gateset::V2`]. A gate that names +/// one qubit twice (`ccx q[0],q[0],q[1]`, `cx q[1],q[1]`) is refused as an +/// invalid circuit when it is read, never built, so the circuit stays valid +/// if its gateset is then changed. pub fn from_qasm(src: &str) -> Result { let mut n: Option = None; // first pass: qubit count @@ -675,36 +694,40 @@ pub fn from_qasm(src: &str) -> Result { _ => {} } let qs = parse_qubits(&line); - let push = |c: &mut Circuit, base: BaseGate, controls: Vec, target: u8, param: u16| { + let push = |c: &mut Circuit, base: BaseGate, controls: Vec, target: u8, param: u16| -> Result<(), String> { + distinct_qubits(&controls, target)?; c.ops.push(Gate { base, controls, target, param }); + Ok(()) }; match head { "id" | "i" => {} - "x" => require1(&qs).map(|t| push(&mut c, BaseGate::X, vec![], t, 0))?, - "y" => require1(&qs).map(|t| push(&mut c, BaseGate::Y, vec![], t, 0))?, - "z" => require1(&qs).map(|t| push(&mut c, BaseGate::Z, vec![], t, 0))?, - "h" => require1(&qs).map(|t| push(&mut c, BaseGate::H, vec![], t, 0))?, - "s" => require1(&qs).map(|t| push(&mut c, BaseGate::S, vec![], t, 0))?, - "sdg" => require1(&qs).map(|t| push(&mut c, BaseGate::Sdg, vec![], t, 0))?, - "t" => require1(&qs).map(|t| push(&mut c, BaseGate::T, vec![], t, 0))?, - "tdg" => require1(&qs).map(|t| push(&mut c, BaseGate::Tdg, vec![], t, 0))?, - "cx" | "cnot" => require2(&qs).map(|(a, t)| push(&mut c, BaseGate::X, vec![a], t, 0))?, - "cy" => require2(&qs).map(|(a, t)| push(&mut c, BaseGate::Y, vec![a], t, 0))?, - "cz" => require2(&qs).map(|(a, t)| push(&mut c, BaseGate::Z, vec![a], t, 0))?, - "swap" => require2(&qs).map(|(a, b)| { + "x" => require1(&qs).and_then(|t| push(&mut c, BaseGate::X, vec![], t, 0))?, + "y" => require1(&qs).and_then(|t| push(&mut c, BaseGate::Y, vec![], t, 0))?, + "z" => require1(&qs).and_then(|t| push(&mut c, BaseGate::Z, vec![], t, 0))?, + "h" => require1(&qs).and_then(|t| push(&mut c, BaseGate::H, vec![], t, 0))?, + "s" => require1(&qs).and_then(|t| push(&mut c, BaseGate::S, vec![], t, 0))?, + "sdg" => require1(&qs).and_then(|t| push(&mut c, BaseGate::Sdg, vec![], t, 0))?, + "t" => require1(&qs).and_then(|t| push(&mut c, BaseGate::T, vec![], t, 0))?, + "tdg" => require1(&qs).and_then(|t| push(&mut c, BaseGate::Tdg, vec![], t, 0))?, + "cx" | "cnot" => require2(&qs).and_then(|(a, t)| push(&mut c, BaseGate::X, vec![a], t, 0))?, + "cy" => require2(&qs).and_then(|(a, t)| push(&mut c, BaseGate::Y, vec![a], t, 0))?, + "cz" => require2(&qs).and_then(|(a, t)| push(&mut c, BaseGate::Z, vec![a], t, 0))?, + "swap" => require2(&qs).and_then(|(a, b)| { + distinct_qubits(&[a], b)?; c.swap(a, b); + Ok(()) })?, - "ccx" | "toffoli" => require3(&qs).map(|(a, b, t)| push(&mut c, BaseGate::X, vec![a, b], t, 0))?, - "ccz" => require3(&qs).map(|(a, b, t)| push(&mut c, BaseGate::Z, vec![a, b], t, 0))?, + "ccx" | "toffoli" => require3(&qs).and_then(|(a, b, t)| push(&mut c, BaseGate::X, vec![a, b], t, 0))?, + "ccz" => require3(&qs).and_then(|(a, b, t)| push(&mut c, BaseGate::Z, vec![a, b], t, 0))?, "p" | "u1" | "phase" => { let t = parse_angle(&arg_n(&line, 0)?)?; let q = require1(&qs)?; - push_turn(&mut c, &[], q, t); + push_turn(&mut c, &[], q, t)?; } "cp" | "cu1" | "cphase" => { let t = parse_angle(&arg_n(&line, 0)?)?; let (a, q) = require2(&qs)?; - push_turn(&mut c, &[a], q, t); + push_turn(&mut c, &[a], q, t)?; } // Rotations. These are exact for dyadic angles but carry an // unobservable global phase relative to the textbook `r*` matrices: @@ -713,17 +736,17 @@ pub fn from_qasm(src: &str) -> Result { "rz" => { let t = parse_angle(&arg_n(&line, 0)?)?; let q = require1(&qs)?; - push_turn(&mut c, &[], q, t); + push_turn(&mut c, &[], q, t)?; } "rx" => { let t = parse_angle(&arg_n(&line, 0)?)?; let q = require1(&qs)?; - push_rx(&mut c, q, t); + push_rx(&mut c, q, t)?; } "ry" => { let t = parse_angle(&arg_n(&line, 0)?)?; let q = require1(&qs)?; - push_ry(&mut c, q, t); + push_ry(&mut c, q, t)?; } // `sx` is `rx(π/2)` — a quarter turn, so `P(2)` between two `H`s. "sx" | "sxdg" => { @@ -733,7 +756,7 @@ pub fn from_qasm(src: &str) -> Result { } else { Turn { m: 3, k: 2 } }; - push_rx(&mut c, q, t); + push_rx(&mut c, q, t)?; } // `u(θ,φ,λ) = rz(φ)·ry(θ)·rz(λ)`; the rightmost applies first. "u" | "u3" | "u2" => { @@ -748,9 +771,9 @@ pub fn from_qasm(src: &str) -> Result { ) }; let q = require1(&qs)?; - push_turn(&mut c, &[], q, lam); - push_ry(&mut c, q, theta); - push_turn(&mut c, &[], q, phi); + push_turn(&mut c, &[], q, lam)?; + push_ry(&mut c, q, theta)?; + push_turn(&mut c, &[], q, phi)?; } // Controlled-rz is exact with no phase caveat: `CP(θ)` preceded by // `P(−θ/2)` on the control reproduces `diag(1,1,e^{−iθ/2},e^{iθ/2})`. @@ -762,8 +785,9 @@ pub fn from_qasm(src: &str) -> Result { return Err("crz angle is finer than the dyadic floor".into()); } let (a, q) = require2(&qs)?; - push_turn(&mut c, &[], a, to_turn(-p, den * 2)); - push_turn(&mut c, &[a], q, to_turn(p, den)); + distinct_qubits(&[a], q)?; + push_turn(&mut c, &[], a, to_turn(-p, den * 2))?; + push_turn(&mut c, &[a], q, to_turn(p, den))?; } other => { return Err(format!( @@ -777,6 +801,20 @@ pub fn from_qasm(src: &str) -> Result { Ok(c) } +/// Refuse a gate that names one qubit twice, a control repeated or equal to +/// the target, as an invalid circuit in every gateset. +fn distinct_qubits(controls: &[u8], target: u8) -> Result<(), String> { + for (i, &q) in controls.iter().enumerate() { + if q == target { + return Err(QuantumError::Invalid(format!("control {q} equals target")).to_string()); + } + if controls[..i].contains(&q) { + return Err(QuantumError::Invalid(format!("control {q} repeated")).to_string()); + } + } + Ok(()) +} + fn strip(line: &str) -> String { let no_comment = line.split("//").next().unwrap_or(""); no_comment.trim().trim_end_matches(';').trim().to_string() @@ -1003,8 +1041,11 @@ fn arg_n(line: &str, i: usize) -> Result { .ok_or_else(|| format!("expected at least {} argument(s)", i + 1)) } -/// Emit an exact dyadic phase as `P` gates, one per set bit of `m`. -fn push_turn(c: &mut Circuit, controls: &[u8], target: u8, t: Turn) { +/// Emit an exact dyadic phase as `P` gates, one per set bit of `m`. A gate +/// naming one qubit twice is refused, even when the phase is a whole turn and +/// emits nothing. +fn push_turn(c: &mut Circuit, controls: &[u8], target: u8, t: Turn) -> Result<(), String> { + distinct_qubits(controls, target)?; for i in 0..=t.k { if (t.m >> i) & 1 == 1 { let k = t.k - i; @@ -1019,21 +1060,24 @@ fn push_turn(c: &mut Circuit, controls: &[u8], target: u8, t: Turn) { } } } + Ok(()) } /// `rx(θ) = H · rz(θ) · H`, exactly, in the dyadic basis. -fn push_rx(c: &mut Circuit, target: u8, t: Turn) { +fn push_rx(c: &mut Circuit, target: u8, t: Turn) -> Result<(), String> { c.h(target); - push_turn(c, &[], target, t); + push_turn(c, &[], target, t)?; c.h(target); + Ok(()) } /// `ry(θ) = S · rx(θ) · S†`. Circuit order applies the rightmost factor first. -fn push_ry(c: &mut Circuit, target: u8, t: Turn) { +fn push_ry(c: &mut Circuit, target: u8, t: Turn) -> Result<(), String> { let one = |b: BaseGate| Gate { base: b, controls: vec![], target, param: 0 }; c.ops.push(one(BaseGate::Sdg)); - push_rx(c, target, t); + push_rx(c, target, t)?; c.ops.push(one(BaseGate::S)); + Ok(()) } /// The content hash of a circuit — the identity a run receipt binds. It is @@ -1358,4 +1402,53 @@ mod tests { assert!(is_clifford(&rc), "random Clifford must be Clifford"); assert_eq!(random_clifford(6, 60, 7), random_clifford(6, 60, 7), "deterministic in seed"); } + + /// A circuit derived from another keeps its gateset: a noisy trajectory + /// and a routed circuit run under the source's table, not the default. + #[test] + fn derived_circuits_keep_their_gateset() { + use crate::quantum::Gateset; + // H·T·H reads ⟨Z⟩ through the table, so the two gatesets give + // different expectations. + let htz = |gs| { + let mut c = Circuit::with_gateset(1, gs); + c.h(0).t(0).h(0); + c + }; + let exact = |gs| expect_z(&htz(gs).simulate().unwrap(), 0); + assert_ne!(exact(Gateset::V1).to_bits(), exact(Gateset::V2).to_bits()); + for gs in Gateset::ALL { + // No noise: each trajectory is the circuit itself. + assert_eq!(noisy_expect_z(&htz(gs), 0, 0, 3, 1).to_bits(), exact(gs).to_bits(), "{gs:?}"); + let mut src = Circuit::qft(5); + src.gateset = gs; + let r = route_line(&src); + assert_eq!(r.circuit.gateset, gs); + assert!(routing_equivalent(&src, &r)); + } + } + + /// A QASM gate naming one qubit twice is refused as an invalid circuit, + /// never built, whatever gateset the circuit is later given. + #[test] + fn qasm_gates_that_repeat_a_qubit_are_invalid() { + for (gate, want) in [ + ("ccx q[0],q[0],q[1];", "control 0 repeated"), + ("ccz q[2],q[2],q[0];", "control 2 repeated"), + ("cx q[1],q[1];", "control 1 equals target"), + ("cz q[0],q[0];", "control 0 equals target"), + ("ccx q[0],q[1],q[1];", "control 1 equals target"), + ("swap q[1],q[1];", "control 1 equals target"), + ("cp(pi/2) q[0],q[0];", "control 0 equals target"), + // A whole turn emits no gate, and is still a gate naming q[0] twice. + ("cp(0) q[0],q[0];", "control 0 equals target"), + ("crz(pi/2) q[2],q[2];", "control 2 equals target"), + ] { + let e = from_qasm(&format!("qreg q[3];\n{gate}")).expect_err(gate); + assert_eq!(e, format!("invalid circuit: {want}"), "{gate}"); + } + let ok = from_qasm("qreg q[3];\nccx q[0],q[1],q[2];\ncp(pi/2) q[0],q[2];").unwrap(); + assert_eq!(ok.gateset, crate::quantum::Gateset::V2); + assert_eq!(ok.ops.len(), 2); + } } -- 2.54.0 (Apple Git-157) From 007b3fb3cee33b1f178ce6a86301f2aed69821e0 Mon Sep 17 00:00:00 2001 From: Transaction Science Date: Fri, 9 Oct 2026 11:43:15 -0400 Subject: [PATCH 05/13] wai-quantum circuit2: the circuit2 pins beside the frozen circuit/1 pins The legacy pins keep their values and build their circuits in circuit/1. No pinned hex or float changed. New circuit2 pins: - receipt_pins::quantum_circuit_v2_bytes seals GHZ3 at seed 7, 1024 shots. It asserts the circuit, statevector and histogram leaves first, then the receipt hash and JSON hash, which were frozen after the leaves matched. The circuit/1 twin of its ops does not verify against it. - bit_identity::mps_truncation_v2 pins 0xe408_42e9_2c89_20ed. - quantum_source pins the circuit2 Bell pair's entropy at exactly 1.0. - quantum_channel pins the circuit2 half-depolarized |+> entropy at 0.811278124459133, next to the ideal H2(1/4). the_conformance_corpora_are_pinned rebuilds every vector of both corpora from code and checks its file size, op count, circuit hash, statevector hash and, for bell, histogram hash. The circuit/1 rows include the nine manifest hashes of the four original files. The other rows are the values an independent integer-only implementation computed. The new pins pass natively and under wasm32-wasip2. Co-Authored-By: Claude Opus 5.5 --- wai/wai-quantum/src/quantum.rs | 78 ++++++++++++++++++++++++++ wai/wai-quantum/src/quantum_channel.rs | 13 +++++ wai/wai-quantum/src/quantum_source.rs | 14 +++++ wai/wai-quantum/src/receipt_pins.rs | 32 +++++++++++ wai/wai-quantum/tests/bit_identity.rs | 10 ++++ 5 files changed, 147 insertions(+) diff --git a/wai/wai-quantum/src/quantum.rs b/wai/wai-quantum/src/quantum.rs index ae928534c..ce9105118 100644 --- a/wai/wai-quantum/src/quantum.rs +++ b/wai/wai-quantum/src/quantum.rs @@ -1768,6 +1768,84 @@ mod tests { } } + /// Every hash of both conformance corpora, reproduced from code, so the + /// unit suite alone catches a drift in either gateset. The circuit/1 rows + /// for bell, ghz3, qft3 and qft5 are the manifest of the files 0.3.x + /// wrote, nine hashes in all; the other rows were computed by an + /// integer-only implementation that shares no code with this crate. None + /// of these values may be edited to make this test pass. + #[test] + fn the_conformance_corpora_are_pinned() { + // (file, bytes, n_ops, circuit_hash, statevector_hash) + type Row = (&'static str, usize, usize, &'static str, &'static str); + let v1: [Row; 7] = [ + ("bell", 216, 2, "894d9a9582b19f27b8d08d06938357237fc00f7f30bf4bfc2be0c04fa5e47a4e", + "bd5df69662f4e61878d17baee1f3cf5705c3726d6740d1a4bf7be5133c099efb"), + ("ghz3", 157, 3, "34e44f7d68037cbb3aee677f56b894dc48df6d408aa068a16d3b18076e5e7c5c", + "08c5e6fb50b91a00d28795506cc5d99ce1e9a3a8b4c41e4bfd05f5d829af9542"), + ("qft3", 191, 9, "7e8414be4410557a05b52bcc1084e1c5f93b6caa0f28fa45f4d2ab661efd2a60", + "0fe1e5b8e2ac8e9afbfa926b3877967ebfead92efb7ed8e1ff6b117668926d6e"), + ("qft5", 261, 21, "f36a9af99a251cae6f42144b2be3c57523f15e1e2ab3492cf15cac709fc048be", + "c268d77f2ddf84fecb028e80ce989123912870f04e5b9a1e3d885c9ce235c27d"), + ("qft5_x22", 276, 24, "40c847fe9f85b6d79d245b6e65420197e6bbedb9c0b1ab77baaf1cfe077ff4f5", + "95a2d2fd41f6a13c19562729a4b553ee12db6adfaa361b5061c16d801354d0cd"), + ("dyadic_ladder", 564, 72, "2fa5b02c952dcaf55ab4cfd2ea5b3fae999ac912a32a4e06dd2b6ee96bb58d4f", + "9754af24edd026601297a0f3334eb7171174978841a2938c9b372db942a3851d"), + ("qft8_ones", 460, 56, "5ca94d3a960b8ec65a1eb802699f1ed8e20f00e7fbd9297849f0c60047404a1a", + "fd683498744552e9e2e8657a2bdc244e4f03d4ee4dd292c9705ba38b19fb709c"), + ]; + let v2: [Row; 7] = [ + ("bell", 218, 2, "6f6199943059fc4d90554d7c33f1ac69723603f5128962a6e3cac9c765484d2b", + "db68a50b08a8c2eabc89a881db9fcb130fff89dc36182d4c7f90da5bf4720b15"), + ("ghz3", 159, 3, "c5911b898a899c999ce01730bb825c84678569493ac513760e15dcbccd84227f", + "51850f28817200e9e03980b60f34879c7bd0147a07b0c1746528b801cae2e2f8"), + ("qft3", 193, 9, "c23a1b1e31b87705999cbc54866f4c9e3e4c4d2b274d6260672121abb5ca89b8", + "70fe25ec22008f86076328b2c0908ede778e6d8227e563e8c7dcd89f7e844bf2"), + ("qft5", 263, 21, "8559c83769ac8c0cb3aabc64672233aa569cd4ce92aa6545f1449982b9aa8b8c", + "50b01fab0bfee966e553a947aea6aa09cebf8f21ba201cd548d703048d66f2ba"), + ("qft5_x22", 278, 24, "c126604030b16f4f1e88e6683894f9316262f49b3419be06ab74c7dbd2441651", + "5def2fca1d29e9e2b9ca2020cd2fa2dc056d07ce2e21b6e101b6116a03efd61c"), + ("dyadic_ladder", 566, 72, "8a6dcf0d41ecefd1a9761fef49b385d7dffe7543a9382efda02bfe4f7a0896fe", + "28bdb7265c10d798012ac78b604327700acd8cdac0e1f88ed4045f8b0b78bc7c"), + ("qft8_ones", 462, 56, "286f044750b19937f6e7bf6f534053aa74a21777516c773dbd7c254df7301b34", + "e059d43a574780d819a872f7904bf96918ba5880e1c7c32b38159306acd559fe"), + ]; + for (gs, rows) in [(Gateset::V1, v1), (Gateset::V2, v2)] { + // The four originals, then the three table-sensitive vectors. + let x_then_qft = |n: u8, xs: &[u8]| { + let mut c = Circuit::with_gateset(n, gs); + for &q in xs { + c.x(q); + } + c.ops.extend(Circuit::qft(n).ops); + c + }; + let mut vectors: Vec<(&str, Circuit, Option)> = corpus_circuits(gs).into(); + vectors.push(("qft5_x22", x_then_qft(5, &[1, 2, 4]), None)); + vectors.push(("dyadic_ladder", dyadic_ladder(gs), None)); + vectors.push(("qft8_ones", x_then_qft(8, &[0, 1, 2, 3, 4, 5, 6, 7]), None)); + for ((name, c, m), (file, len, n_ops, circuit, statevector)) in vectors.into_iter().zip(rows) { + assert_eq!(name, file); + assert_eq!(c.gateset, gs, "{name}"); + assert_eq!(to_wqc(&c, m).unwrap().len(), len, "{gs:?} {name}.wqc"); + assert_eq!(c.ops.len(), n_ops, "{gs:?} {name}"); + assert_eq!(hx(&circuit_hash(&c).unwrap()), circuit, "{gs:?} {name}"); + let sv = c.simulate().unwrap(); + assert_eq!(hx(&sv.statevector_hash()), statevector, "{gs:?} {name}"); + // Bell is the measured vector. Its sampled outcomes do not + // move between the tables, so neither does its histogram. + if let Some(Measure { seed, shots }) = m { + assert_eq!(name, "bell"); + assert_eq!( + hx(&sv.histogram_hash(seed, shots)), + "0893f48ef32ca9f2865f6617537f0a4a5334f67e3c944bba845beb2f377acdb3", + "{gs:?} {name}" + ); + } + } + } + } + /// Both containers round-trip: the magic names the gateset, and the /// decoded circuit equals the written one, gateset included, with the same /// hash and state. circuit2's corpus circuits have the plan's sizes and diff --git a/wai/wai-quantum/src/quantum_channel.rs b/wai/wai-quantum/src/quantum_channel.rs index faf70c20b..41a0d58f3 100644 --- a/wai/wai-quantum/src/quantum_channel.rs +++ b/wai/wai-quantum/src/quantum_channel.rs @@ -389,6 +389,19 @@ mod tests { ); } + /// The same spectrum from circuit2's |+⟩. Its eigenvalues are 3/4 and + /// 1/4 up to fixed-point rounding, so the entropy lands next to the ideal + /// H2(1/4) = 0.8112781244591328. Confirmed identical on + /// `aarch64-apple-darwin` and `wasm32-wasip2` under wasmtime. + #[test] + fn the_circuit2_spectrum_is_pinned() { + let out = Channel::depolarizing(ONE / 2).apply(&plus_in(Gateset::V2)).unwrap(); + let s = von_neumann_entropy(&out); + assert_eq!(s.to_bits(), 0x3fe9_f5fd_8a90_63e5, "entropy drifted from the pinned bits: {s:.17}"); + assert_eq!(s.to_bits(), 0.811278124459133f64.to_bits()); + assert!((s - 0.8112781244591328).abs() < 1e-12, "{s:.17} is not next to H2(1/4)"); + } + /// A channel that does nothing costs nothing — the boundary case of the same law. #[test] fn a_perfect_wire_loses_nothing() { diff --git a/wai/wai-quantum/src/quantum_source.rs b/wai/wai-quantum/src/quantum_source.rs index 76ecf96f2..c2ce13746 100644 --- a/wai/wai-quantum/src/quantum_source.rs +++ b/wai/wai-quantum/src/quantum_source.rs @@ -275,6 +275,20 @@ mod tests { ); } + /// circuit2's Bell pair. Its correctly rounded 1/√2 makes the reduced + /// state exactly half the identity at this scale, so the entropy is + /// exactly one bit. Confirmed identical on `aarch64-apple-darwin` and + /// `wasm32-wasip2` under wasmtime. + #[test] + fn entropy_of_the_circuit2_bell_pair_is_exactly_one_bit() { + let mut c = Circuit::with_gateset(2, crate::quantum::Gateset::V2); + c.h(0).cx(0, 1); + let half = DensityMatrix::from_pure(&c.simulate().unwrap()).partial_trace(&[0]); + let s = von_neumann_entropy(&half); + assert_eq!(s.to_bits(), 0x3ff0_0000_0000_0000, "entropy drifted from the pinned bits: {s:.17}"); + assert_eq!(s.to_bits(), 1.0f64.to_bits()); + } + #[test] fn entropy_is_reproducible() { let mut c = Circuit::new(3); diff --git a/wai/wai-quantum/src/receipt_pins.rs b/wai/wai-quantum/src/receipt_pins.rs index 6973e00a6..1cd55ebd7 100644 --- a/wai/wai-quantum/src/receipt_pins.rs +++ b/wai/wai-quantum/src/receipt_pins.rs @@ -46,6 +46,38 @@ fn quantum_circuit_legacy_bytes() { assert_eq!(json_hash(&r.to_json()), "bc19d790962cecf2efea9d275f1b9494eb5a44bc1948e1f1bbaf2134f222b37d"); } +/// The same receipt sealed by circuit2. The three leaves are checked first. +/// The circuit and statevector leaves are the GHZ3 entries of the circuit2 +/// conformance corpus, and all three were computed by an integer-only +/// implementation that shares no code with this crate. The receipt and JSON +/// hashes were frozen only after the leaves matched, and must never be edited +/// to make this test pass. +#[cfg(feature = "quantum_receipt")] +#[test] +fn quantum_circuit_v2_bytes() { + use crate::quantum::{Circuit, Gateset, Measure}; + use crate::quantum_receipt::QuantumReceipt; + let mut c = Circuit::with_gateset(3, Gateset::V2); + c.h(0).cx(0, 1).cx(1, 2); + let r = QuantumReceipt::seal(&key(1), "did:key:pin", &c, Some(Measure { seed: 7, shots: 1024 }), 117_000, Some([0x21; 32])) + .unwrap(); + assert!(r.verify()); + assert!(r.verify_reconstruction(&c)); + assert_eq!(hx(&r.circuit_hash), "c5911b898a899c999ce01730bb825c84678569493ac513760e15dcbccd84227f"); + assert_eq!(hx(&r.statevector_hash), "51850f28817200e9e03980b60f34879c7bd0147a07b0c1746528b801cae2e2f8"); + let m = r.measurement.expect("the receipt vouches for a histogram"); + assert_eq!((m.seed, m.shots), (7, 1024)); + assert_eq!(hx(&m.histogram_hash), "8e119ee752f82a5035015472e751cf3a773ffe0d486b6541a14899a37264a8eb"); + assert_eq!(hx(&r.receipt_hash()), "102b06b6734a31bdc12eb33eae6694c20c048bb613f49800b066c59addf6134c"); + assert_eq!(json_hash(&r.to_json()), "8525056275ac518a959e8aadf7d76d7346bc317173eea67d9c617c0da1f51d59"); + // The circuit/1 twin of the same ops is a different circuit, so the + // receipt names circuit2 and refuses the twin. + let mut v1 = Circuit::with_gateset(3, Gateset::V1); + v1.h(0).cx(0, 1).cx(1, 2); + assert!(!r.verify_reconstruction(&v1)); + assert_eq!(r.gateset_of(&v1), Some(Gateset::V2)); +} + #[cfg(feature = "quantum_ops")] mod ops { use super::*; diff --git a/wai/wai-quantum/tests/bit_identity.rs b/wai/wai-quantum/tests/bit_identity.rs index 795acac12..a3ffd2579 100644 --- a/wai/wai-quantum/tests/bit_identity.rs +++ b/wai/wai-quantum/tests/bit_identity.rs @@ -108,6 +108,16 @@ fn mps_truncation() { assert_eq!(digest(&[quantum_mps::fidelity(&mps, &c.simulate().unwrap())]), 0xa807_44ca_a97f_bff8); } +/// The same truncation against the circuit2 reference statevector. The MPS +/// itself is an ideal-angle simulation and does not read the phase table, so +/// only the reference it is compared with changes. +#[test] +fn mps_truncation_v2() { + let c = brickwork(quantum::Gateset::V2); + let (mps, _) = quantum_mps::run(&c, 4).unwrap(); + assert_eq!(digest(&[quantum_mps::fidelity(&mps, &c.simulate().unwrap())]), 0xe408_42e9_2c89_20ed); +} + #[test] fn sparse_pauli_propagation() { let (e, _) = quantum_pauli::expect_z(&brickwork(quantum::Gateset::V1), 3, 1e-6).unwrap(); -- 2.54.0 (Apple Git-157) From 252a9fdeba9c299f3670782cb5305a0b4037ddd5 Mon Sep 17 00:00:00 2001 From: Transaction Science Date: Fri, 9 Oct 2026 12:38:07 -0400 Subject: [PATCH 06/13] wai-quantum circuit2: wai-rs registers circuit2 and carries the corpus in both capabilities The codec registry registers wai.quantum.circuit2 beside wai.quantum.circuit, with the same media class, tier, licence and replicate class. The registry export, the vocabulary and the generated REGISTRY.md table are regenerated from it, and SPEC.md section 5 gains its row and qualifies the circuit/1 row's QFT sentence. wrap_envelope returns a Result and tags the envelope with the circuit's own capability. open_envelope decodes the payload and refuses an envelope whose capability is not the one the container's magic names. wai_quantum_vectors gen writes the circuit/1 corpus with three table-sensitive vectors after the four frozen ones, the seven vectors again in circuit2 under v2/, and 26 reject files with reject.json naming the class each is refused as. verify checks all three trees: each vector decodes to the capability its entry names and reproduces its hashes, and each reject file fails with its listed class. The four original files and their manifest entries are unchanged. The wai-quantum CLI takes --gateset v1|v2 (default v2) on every command that builds a circuit and on fabric, reports the capability in its run and fabric JSON, and prints the circuit/1 table note on standard error only. wai_quantum_meter names the capability of the circuit it meters. Co-Authored-By: Claude Opus 5.5 --- wai/REGISTRY.md | 1 + wai/SPEC.md | 3 +- wai/quantum-conformance/expected_hashes.json | 18 + wai/quantum-conformance/reject/magic_wqc3.wqc | Bin 0 -> 153 bytes wai/quantum-conformance/reject/reject.json | 28 + .../reject/v1_contract2_in_wqc1.wqc | Bin 0 -> 153 bytes .../reject/v1_contract_not_object.wqc | Bin 0 -> 153 bytes .../reject/v1_contract_twice.wqc | Bin 0 -> 272 bytes .../reject/v1_ext_other.wqc | Bin 0 -> 151 bytes .../reject/v1_fields_missing.wqc | Bin 0 -> 53 bytes wai/quantum-conformance/reject/v1_frac_31.wqc | Bin 0 -> 151 bytes .../reject/v1_frac_float.wqc | Bin 0 -> 153 bytes .../reject/v1_gateset_rn.wqc | Bin 0 -> 154 bytes .../reject/v1_n_ops_u32max.wqc | Bin 0 -> 145 bytes .../reject/v1_n_qubits_256.wqc | Bin 0 -> 153 bytes .../reject/v1_n_qubits_258.wqc | Bin 0 -> 153 bytes .../reject/v1_numeric_other.wqc | Bin 0 -> 149 bytes .../reject/v1_offset_wrap.wqc | Bin 0 -> 151 bytes .../reject/v1_oplog_twice.wqc | Bin 0 -> 169 bytes .../reject/v2_contract1_in_wqc2.wqc | Bin 0 -> 151 bytes .../reject/v2_contract_extra_member.wqc | Bin 0 -> 164 bytes .../reject/v2_contract_key_order.wqc | Bin 0 -> 153 bytes .../reject/v2_contract_whitespace.wqc | Bin 0 -> 154 bytes .../reject/v2_duplicate_controls.wqc | Bin 0 -> 149 bytes .../reject/v2_measure_basis_x.wqc | Bin 0 -> 195 bytes .../reject/v2_measure_no_shots.wqc | Bin 0 -> 196 bytes .../reject/v2_measure_spaced.wqc | Bin 0 -> 212 bytes .../reject/v2_opcode_10.wqc | Bin 0 -> 147 bytes .../reject/v2_oplog_trailing_byte.wqc | Bin 0 -> 154 bytes .../reject/v2_param_on_h.wqc | Bin 0 -> 153 bytes .../v2/expected_hashes.json | 56 ++ wai/quantum-conformance/v2/vectors/bell.wqc | Bin 0 -> 218 bytes .../v2/vectors/dyadic_ladder.wqc | Bin 0 -> 566 bytes wai/quantum-conformance/v2/vectors/ghz3.wqc | Bin 0 -> 159 bytes wai/quantum-conformance/v2/vectors/qft3.wqc | Bin 0 -> 193 bytes wai/quantum-conformance/v2/vectors/qft5.wqc | Bin 0 -> 263 bytes .../v2/vectors/qft5_x22.wqc | Bin 0 -> 278 bytes .../v2/vectors/qft8_ones.wqc | Bin 0 -> 462 bytes .../vectors/dyadic_ladder.wqc | Bin 0 -> 564 bytes wai/quantum-conformance/vectors/qft5_x22.wqc | Bin 0 -> 276 bytes wai/quantum-conformance/vectors/qft8_ones.wqc | Bin 0 -> 460 bytes wai/registry/capabilities.json | 14 + wai/vocab/v2.shapes.ttl | 4 + wai/vocab/v2.ttl | 12 + wai/wai-rs/Cargo.toml | 3 +- wai/wai-rs/src/bin/wai_quantum.rs | 101 ++-- wai/wai-rs/src/bin/wai_quantum_meter.rs | 10 +- wai/wai-rs/src/bin/wai_quantum_vectors.rs | 549 ++++++++++++++++-- wai/wai-rs/src/codecs/glossary_check.rs | 1 + wai/wai-rs/src/codecs/mod.rs | 38 +- wai/wai-rs/src/codecs/registry.rs | 4 +- wai/wai-rs/src/lib.rs | 13 +- wai/wai-rs/src/quantum_envelope.rs | 120 +++- 53 files changed, 842 insertions(+), 133 deletions(-) create mode 100644 wai/quantum-conformance/reject/magic_wqc3.wqc create mode 100644 wai/quantum-conformance/reject/reject.json create mode 100644 wai/quantum-conformance/reject/v1_contract2_in_wqc1.wqc create mode 100644 wai/quantum-conformance/reject/v1_contract_not_object.wqc create mode 100644 wai/quantum-conformance/reject/v1_contract_twice.wqc create mode 100644 wai/quantum-conformance/reject/v1_ext_other.wqc create mode 100644 wai/quantum-conformance/reject/v1_fields_missing.wqc create mode 100644 wai/quantum-conformance/reject/v1_frac_31.wqc create mode 100644 wai/quantum-conformance/reject/v1_frac_float.wqc create mode 100644 wai/quantum-conformance/reject/v1_gateset_rn.wqc create mode 100644 wai/quantum-conformance/reject/v1_n_ops_u32max.wqc create mode 100644 wai/quantum-conformance/reject/v1_n_qubits_256.wqc create mode 100644 wai/quantum-conformance/reject/v1_n_qubits_258.wqc create mode 100644 wai/quantum-conformance/reject/v1_numeric_other.wqc create mode 100644 wai/quantum-conformance/reject/v1_offset_wrap.wqc create mode 100644 wai/quantum-conformance/reject/v1_oplog_twice.wqc create mode 100644 wai/quantum-conformance/reject/v2_contract1_in_wqc2.wqc create mode 100644 wai/quantum-conformance/reject/v2_contract_extra_member.wqc create mode 100644 wai/quantum-conformance/reject/v2_contract_key_order.wqc create mode 100644 wai/quantum-conformance/reject/v2_contract_whitespace.wqc create mode 100644 wai/quantum-conformance/reject/v2_duplicate_controls.wqc create mode 100644 wai/quantum-conformance/reject/v2_measure_basis_x.wqc create mode 100644 wai/quantum-conformance/reject/v2_measure_no_shots.wqc create mode 100644 wai/quantum-conformance/reject/v2_measure_spaced.wqc create mode 100644 wai/quantum-conformance/reject/v2_opcode_10.wqc create mode 100644 wai/quantum-conformance/reject/v2_oplog_trailing_byte.wqc create mode 100644 wai/quantum-conformance/reject/v2_param_on_h.wqc create mode 100644 wai/quantum-conformance/v2/expected_hashes.json create mode 100644 wai/quantum-conformance/v2/vectors/bell.wqc create mode 100644 wai/quantum-conformance/v2/vectors/dyadic_ladder.wqc create mode 100644 wai/quantum-conformance/v2/vectors/ghz3.wqc create mode 100644 wai/quantum-conformance/v2/vectors/qft3.wqc create mode 100644 wai/quantum-conformance/v2/vectors/qft5.wqc create mode 100644 wai/quantum-conformance/v2/vectors/qft5_x22.wqc create mode 100644 wai/quantum-conformance/v2/vectors/qft8_ones.wqc create mode 100644 wai/quantum-conformance/vectors/dyadic_ladder.wqc create mode 100644 wai/quantum-conformance/vectors/qft5_x22.wqc create mode 100644 wai/quantum-conformance/vectors/qft8_ones.wqc diff --git a/wai/REGISTRY.md b/wai/REGISTRY.md index 0d75ba01d..bfe36bfe7 100644 --- a/wai/REGISTRY.md +++ b/wai/REGISTRY.md @@ -211,6 +211,7 @@ and a role are fixed once registered. | `wai.prior.set` | payload | registered | binary | standalone | royalty-free | standard-defined | — | §5 Prior artifacts, general tensors and derivation operations | | `wai.quantum.calibration` | record | registered | — | — | — | — | — | §5 Interactive, temporal & computed content | | `wai.quantum.circuit` | payload | registered | binary | standalone | royalty-free | standard-defined | — | §5 Interactive, temporal & computed content | +| `wai.quantum.circuit2` | payload | registered | binary | standalone | royalty-free | standard-defined | — | §5 Interactive, temporal & computed content | | `wai.quantum.job` | record | registered | — | — | — | — | — | §5 Interactive, temporal & computed content | | `wai.semantic.clip` | payload | registered | image | standalone | royalty-free | behavioral | — | §5 Neural capabilities | | `wai.semantic.perco2` | payload | registered | image | standalone | non-commercial | behavioral | — | §5 Neural capabilities | diff --git a/wai/SPEC.md b/wai/SPEC.md index a38e0a598..9acd1ee22 100644 --- a/wai/SPEC.md +++ b/wai/SPEC.md @@ -729,7 +729,8 @@ standard. See the linked extension for each. | `wai.film.linear` | WFLM container (sealed world + camera track + frame schedule) | film-equivalence · [linear-film](extensions/linear-film.md) | | `wai.film.reel` | WREL container (sealed world + a sequence of shots = camera track + schedule each) | reel-equivalence (film-equivalence with cuts) · [linear-film](extensions/linear-film.md) | | `wai.ternary.tt` | WQTT container (balanced-ternary MPS cores — packed trits + integer per-column scales) | a **quantum-inspired** (tensor-network) transform ported from the ternary/MPS fabric with ONE change: the per-column scale is an **integer**, so the whole decode is `i64`/`i128` and the reconstruction is byte-identical on every machine — **reconstruction-equivalence** (a portable recon hash), receiptable. Scope: a **provenance** transform, measured at 13.3 dB SNR for 3.8× with six residual layers (`Lossy` tier; the extension's §5). Encoder-side float SVD only · [ternary-tt](extensions/ternary-tt.md) | -| `wai.quantum.circuit` | WQC container (gate op-log — Clifford+T + dyadic phase) | the wire carries a **circuit** (kilobytes, linear in gate count), the sink CLASSICALLY simulates it in a fixed-point **complex** floor (`Amp` = i64 re/im), and the reconstructed `2^n`-amplitude statevector is byte-identical on every machine — **statevector-equivalence** (a portable statevector hash); measurement is sampled by the pinned `splitmix64` so a shot histogram at a pinned `(seed, shots)` converges too (**shot-histogram-equivalence**). The dyadic phase ladder gives the **QFT** ("quantum FFT"), transported as a circuit and reconstructed byte-exact. No quantum hardware — NISQ noise breaks the determinism the standard rests on; this makes a quantum *computation* transportable + receiptable, with **no** speedup claimed · [quantum-sim](extensions/quantum-sim.md) | +| `wai.quantum.circuit` | WQC container (gate op-log — Clifford+T + dyadic phase) | the wire carries a **circuit** (kilobytes, linear in gate count), the sink CLASSICALLY simulates it in a fixed-point **complex** floor (`Amp` = i64 re/im), and the reconstructed `2^n`-amplitude statevector is byte-identical on every machine — **statevector-equivalence** (a portable statevector hash); measurement is sampled by the pinned `splitmix64` so a shot histogram at a pinned `(seed, shots)` converges too (**shot-histogram-equivalence**). The dyadic phase ladder transports the **QFT** ("quantum FFT") as a circuit, reconstructed byte-exact; the phase table is frozen as existing content was made with it, so `P(k)` deviates from the ideal phase for k ≥ 8 and applies none for k ≥ 19, and new content uses `wai.quantum.circuit2`. No quantum hardware — NISQ noise breaks the determinism the standard rests on; this makes a quantum *computation* transportable + receiptable, with **no** speedup claimed · [quantum-sim](extensions/quantum-sim.md) | +| `wai.quantum.circuit2` | WQC container under magic `WQC2` (the same gate op-log) | the successor of `wai.quantum.circuit`, with the same statevector- and shot-histogram-equivalence: a correctly rounded phase table (every `P(k)` within half a unit of 2^30·e^{2πi/2^k}), so the dyadic phase ladder gives the **QFT**; a byte-canonical contract; an op-log that ends exactly after its last op. A new capability, not a change to the old one, because a registered capability's payload format is fixed (§8): a reader that predates it rejects a `WQC2` payload at the magic · [quantum-sim](extensions/quantum-sim.md) | And the verify half — a signed, energy-accounted lineage for opaque captured content WAI does **not** reconstruct (content-addressing + diff --git a/wai/quantum-conformance/expected_hashes.json b/wai/quantum-conformance/expected_hashes.json index 3afca1bbd..df6b4e8e3 100644 --- a/wai/quantum-conformance/expected_hashes.json +++ b/wai/quantum-conformance/expected_hashes.json @@ -27,5 +27,23 @@ "n_ops": 21, "circuit_hash": "f36a9af99a251cae6f42144b2be3c57523f15e1e2ab3492cf15cac709fc048be", "statevector_hash": "c268d77f2ddf84fecb028e80ce989123912870f04e5b9a1e3d885c9ce235c27d" + }, + "qft5_x22.wqc": { + "n_qubits": 5, + "n_ops": 24, + "circuit_hash": "40c847fe9f85b6d79d245b6e65420197e6bbedb9c0b1ab77baaf1cfe077ff4f5", + "statevector_hash": "95a2d2fd41f6a13c19562729a4b553ee12db6adfaa361b5061c16d801354d0cd" + }, + "dyadic_ladder.wqc": { + "n_qubits": 2, + "n_ops": 72, + "circuit_hash": "2fa5b02c952dcaf55ab4cfd2ea5b3fae999ac912a32a4e06dd2b6ee96bb58d4f", + "statevector_hash": "9754af24edd026601297a0f3334eb7171174978841a2938c9b372db942a3851d" + }, + "qft8_ones.wqc": { + "n_qubits": 8, + "n_ops": 56, + "circuit_hash": "5ca94d3a960b8ec65a1eb802699f1ed8e20f00e7fbd9297849f0c60047404a1a", + "statevector_hash": "fd683498744552e9e2e8657a2bdc244e4f03d4ee4dd292c9705ba38b19fb709c" } } diff --git a/wai/quantum-conformance/reject/magic_wqc3.wqc b/wai/quantum-conformance/reject/magic_wqc3.wqc new file mode 100644 index 0000000000000000000000000000000000000000..05b15a486285b10e5af471133e58242b72c0dc85 GIT binary patch literal 153 zcmWF!bT(#UU}OM-A|Pf05&S@0t(00(qGY91o|vguSelquQktumoLQ7ynpt9`Z>XfB zlowxEnv_{mtYl>bXfB zlowxEnv_{mtYl>bXfB zlowxEnv_{mtYl>bP{E{ literal 0 HcmV?d00001 diff --git a/wai/quantum-conformance/reject/v1_ext_other.wqc b/wai/quantum-conformance/reject/v1_ext_other.wqc new file mode 100644 index 0000000000000000000000000000000000000000..76c6db5abce0a4facad55f6748383dfc4f1a803d GIT binary patch literal 151 zcmWF!bT(vSU}OM-0w87r5&S@0t(00(qGY91o|vguSelquQktumoLQ7ynpvW6q@<&i z7hhPKlvz@&WMu^8l;)-uWhO(Fr=*tXrDay6rkI%k6{Zy>0ws(Mbd=H)OHzwdL53#h gWTvI%7o~(~r&K1UWF`kF)iMF?WC3CZMn;fh0I^mlp#T5? literal 0 HcmV?d00001 diff --git a/wai/quantum-conformance/reject/v1_fields_missing.wqc b/wai/quantum-conformance/reject/v1_fields_missing.wqc new file mode 100644 index 0000000000000000000000000000000000000000..e3560526020cefa893f0b83bf4ad0cae5005bbc3 GIT binary patch literal 53 wcmWF!bT(vSU}OLSJ|JcS5&S@0t&|sESelesQmkZUT+0L$VF6+WMn;fQ0JIkdWB>pF literal 0 HcmV?d00001 diff --git a/wai/quantum-conformance/reject/v1_frac_31.wqc b/wai/quantum-conformance/reject/v1_frac_31.wqc new file mode 100644 index 0000000000000000000000000000000000000000..febe3f82783ec75c46b3483a396eb128468456e7 GIT binary patch literal 151 zcmWF!bT(vSU}OM-0w87r5&S@0t(00(qGY91o|vguSelquQktumoLQ7ynpvW6sHCHm z7hhPKlvz@&WMu^8l;)-uWhO(Fr=*tXrDay6rkI%k6{Zy>0ws(Mb(GQ*OHzwdL53#h gWTvI%7o~(~r&K1UWF`kF)iMF?WC3CZMn;fh0I?z`p#T5? literal 0 HcmV?d00001 diff --git a/wai/quantum-conformance/reject/v1_frac_float.wqc b/wai/quantum-conformance/reject/v1_frac_float.wqc new file mode 100644 index 0000000000000000000000000000000000000000..a471531b423b066b0e30b80cc00b85049a9b664d GIT binary patch literal 153 zcmWF!bT(vSU}OM-A|Pf05&S@0t(00(qGY91o|vguSelquQktumoLQ7ynpvW6sHCHm z7hhPKlvz@&WMu^8l;)-uWhO(Fr=*tXrDay6rkI%k6{Zy>0ws(M^bB;A(i2Nki&H_y iCg)_PrR5i;glMN!CZ=R22PoAt0c~XgVg^P=kaGb311JIj literal 0 HcmV?d00001 diff --git a/wai/quantum-conformance/reject/v1_gateset_rn.wqc b/wai/quantum-conformance/reject/v1_gateset_rn.wqc new file mode 100644 index 0000000000000000000000000000000000000000..f4c6cdcad1e8f82659d103ee17fd3c8c3049e5f1 GIT binary patch literal 154 zcmXYr!3qK~3`DoZyZ?}@OLbK|?A0Go5Ijn4Qwi)=woP$G{C87u;01;Z$z0FJPB*F% zWkJ`m6h446Lk}-YW-2yv8I7al3h}yw7UFFxcj65FR!kW&k^9FzvI#V!KkOwKl9k17 i)560tW~@iuKp{MnzpVV*dU7X|B7C*HT?ma*wa*Vc|0uKo literal 0 HcmV?d00001 diff --git a/wai/quantum-conformance/reject/v1_n_ops_u32max.wqc b/wai/quantum-conformance/reject/v1_n_ops_u32max.wqc new file mode 100644 index 0000000000000000000000000000000000000000..7b1ed50bb27c131e334cb02adeb0f1c634650a27 GIT binary patch literal 145 zcmWF!bT(vSU}OM-0w87r5u89=t(00(qGY91o|vguSelquQktumoLQ7ynpvW6sHCHm z7hhPKlvz@&WMu^8l;)-uWhO(Fr=*tXrDay6rkI%k6{Zy>0ws(Mbd=H)OHzwdL53#h cWTvI%7o~(~r&K1UWF`kF)&2(p7NETh0Nx2JkpKVy literal 0 HcmV?d00001 diff --git a/wai/quantum-conformance/reject/v1_n_qubits_256.wqc b/wai/quantum-conformance/reject/v1_n_qubits_256.wqc new file mode 100644 index 0000000000000000000000000000000000000000..2d310260d314488e5eda4cf44fdfdc2420a17329 GIT binary patch literal 153 zcmWF!bT(vSU}OM-A|Pf05&S@0t(00(qGY91o|vguSelquQktumoLQ7ynpvW6sHCHm z7hhPKlvz@&WMyP(2IQ9JrWR!;Lp7wNmguErR-~qwnE;ih6(s^Cj16>@(i2Nki&H_y iCg)_PrR5i;glMN!CZ=R22PoAt0c~XgVg^P=kaGa|lPC`W literal 0 HcmV?d00001 diff --git a/wai/quantum-conformance/reject/v1_n_qubits_258.wqc b/wai/quantum-conformance/reject/v1_n_qubits_258.wqc new file mode 100644 index 0000000000000000000000000000000000000000..707e9786fc523296c27ca3ec289ea47dd065023c GIT binary patch literal 153 zcmWF!bT(vSU}OM-A|Pf05&S@0t(00(qGY91o|vguSelquQktumoLQ7ynpvW6sHCHm z7hhPKlvz@&WMyP(0pynErWR!;Lp7wNmguErR-~qwnE;ih6(s^Cj16>@(i2Nki&H_y iCg)_PrR5i;glMN!CZ=R22PoAt0c~XgVg^P=kaGa}Jtz?X literal 0 HcmV?d00001 diff --git a/wai/quantum-conformance/reject/v1_numeric_other.wqc b/wai/quantum-conformance/reject/v1_numeric_other.wqc new file mode 100644 index 0000000000000000000000000000000000000000..cf449cc5fbba7cf16236870bb98b458ca24fba47 GIT binary patch literal 149 zcmWF!bT(vSU}OM-JRoKQ5&S@0t(00(qGY91o|vguSelquQktumoLQ7ynpvW6sHCHm z7hhPKlvz@&WMu^8l;)-uWhO(Fr=*tXCFT~GnE;ig6(s_NjSX~^(i2Nki&H^HCg)_P erR5i;glMN!CZ=R22PoAt0c~UfVg^P=kW&Clye7&3 literal 0 HcmV?d00001 diff --git a/wai/quantum-conformance/reject/v1_offset_wrap.wqc b/wai/quantum-conformance/reject/v1_offset_wrap.wqc new file mode 100644 index 0000000000000000000000000000000000000000..5f7a84056049b837ce40ba9a6b8555035ef2bba8 GIT binary patch literal 151 zcmWF!bT(vSU}OM-0w8Ak@c;jR1t6nZDYc?R$x5j_F;lOwG%>HFG*>SliAj3CDV0E)FME&u=k literal 0 HcmV?d00001 diff --git a/wai/quantum-conformance/reject/v1_oplog_twice.wqc b/wai/quantum-conformance/reject/v1_oplog_twice.wqc new file mode 100644 index 0000000000000000000000000000000000000000..b45fa740e05c3ce47d947f80d0d54ad232f488f0 GIT binary patch literal 169 zcmWF!bT(vWU}OM-0w87r5&R%VJ&@o8;%cSTiV`I&rSil~y~5JOypqyfz2wZI-Dv ooC-2DIVUqMEx#xwL_4K2F(orOK&h4qXeSF0GcYoOTm{1n0CsyQEdT%j literal 0 HcmV?d00001 diff --git a/wai/quantum-conformance/reject/v2_contract1_in_wqc2.wqc b/wai/quantum-conformance/reject/v2_contract1_in_wqc2.wqc new file mode 100644 index 0000000000000000000000000000000000000000..56f49e20287c7143c700a96499a55a403209d09f GIT binary patch literal 151 zcmWF!bT(pQU}OM-0w87r5&S@0t(00(qGY91o|vguSelquQktumoLQ7ynpvW6sHCHm z7hhPKlvz@&WMu^8l;)-uWhO(Fr=*tXrDay6rkI%k6{Zy>0ws(Mbd=H)OHzwdL53#h gWTvI%7o~(~r&K1UWF`kF)iMF?WC3CZMn;fh0I`QBp#T5? literal 0 HcmV?d00001 diff --git a/wai/quantum-conformance/reject/v2_contract_extra_member.wqc b/wai/quantum-conformance/reject/v2_contract_extra_member.wqc new file mode 100644 index 0000000000000000000000000000000000000000..b757e91c1e603b29361fb89e21144d78a18793e7 GIT binary patch literal 164 zcmXYr%?biB41~AEyYG;zOVuhK_Ua2L2p*;OM*_C%c2iuDeRoqZ@C7CrhPj@PS~jBL znHgEvlKFz42s(HhR6{X|N>moJEl7I3g%;9nD0ie0I?bX)pV9vP&XY3nb=b2lWW(p( oriF(gKl++!pBO@)-DTxI#-Tl_oN8iEp1%{!k_#-DMu^(%2m9PBMF0Q* literal 0 HcmV?d00001 diff --git a/wai/quantum-conformance/reject/v2_contract_key_order.wqc b/wai/quantum-conformance/reject/v2_contract_key_order.wqc new file mode 100644 index 0000000000000000000000000000000000000000..6b546226ca8cc868d76ce188a55e555cbc9b9d91 GIT binary patch literal 153 zcmXYr%?biB41~AEyYG;zrD_!qd-Vkr1dmetBY|yoyD2K-yPJXoUk)=Nne%aPWG6aS zVPqR~<^%aM)+^Eky-^Te1U)=0>ap5PHED;%RV1@qgMwUBDCqtxK9IKQvD?|WcW({I%7H^TAd&BK`0Xd literal 0 HcmV?d00001 diff --git a/wai/quantum-conformance/reject/v2_contract_whitespace.wqc b/wai/quantum-conformance/reject/v2_contract_whitespace.wqc new file mode 100644 index 0000000000000000000000000000000000000000..8acd6773094a52e56e1eb8355535e5398b5272e8 GIT binary patch literal 154 zcmWF!bT(pQU}OM-VjyM$5&S@0t(00(qGY91o|vguSelquQktumoLQ7ynpt9`Z>XfB zlowxEnv_{mtYoEN1muqYbd=H)OHzwdL53#h jWTvI%7o~(~r&K1UWF`mb73C?_G6Ah+0b&M5Mv!{|6UHbt literal 0 HcmV?d00001 diff --git a/wai/quantum-conformance/reject/v2_duplicate_controls.wqc b/wai/quantum-conformance/reject/v2_duplicate_controls.wqc new file mode 100644 index 0000000000000000000000000000000000000000..4df7ce6a8ff9c77f915d1d4816d92933bc746db8 GIT binary patch literal 149 zcmXX=%L)Q95bO5pU(D5|>aKX$t3RM1c$C_Q4A`zaJ4HqOcc)+=frJopKJGiM8YLj1 zuCfq*AiNj`cv>`5v6)LU4#^edZnc3H@?|Plz$ evmEA7H{Bg$9Lb-S{%t+FgP{!H`k~QURQ&){(9k3`P5J?>CsOWwcdXbn6c&2rea^nQFjJ?L4qjy714!%524eRFlU^)V3IVAe@)=DR#2A(xJMszN;YZG J5w|eH`~yxDGPM8z literal 0 HcmV?d00001 diff --git a/wai/quantum-conformance/reject/v2_measure_no_shots.wqc b/wai/quantum-conformance/reject/v2_measure_no_shots.wqc new file mode 100644 index 0000000000000000000000000000000000000000..5eaa1e7db4ce06a7bcba1ce6cdd315221114dbe3 GIT binary patch literal 196 zcmXYr!3x4a3`AS&-G9i{vf3&hdi4hs1do#LZY9v&w%aT!r3e4q6b!t92}36L>$%L5 zG?59yYLShQeMLZ+z`hwOcp<4FqUY$8rl2Enxho(C{}^ITJVI59iOvSn|GcrR(DkRI z#F~JzKJ0UNBHPg}H+4%*(*<+enhzS%7Zp6rD`{N|Nt!NyO)y$(pek~xaU|&h-MKz8 JGIbtXn17CsG=cyC literal 0 HcmV?d00001 diff --git a/wai/quantum-conformance/reject/v2_measure_spaced.wqc b/wai/quantum-conformance/reject/v2_measure_spaced.wqc new file mode 100644 index 0000000000000000000000000000000000000000..ec520bd4cf121c2fcc25efaa951f3b54981d0c91 GIT binary patch literal 212 zcmXYr%?iRm41`N)NudSupSoOc*k`T~1XJ z#SxETRzWsQ@@1)*8PwZB4KJj8^4L4Cb3sOUa@DSc4Ep=z8*&!vijCLGkof1Nayjbp zc;HxTkk|Vng9kcgRk$fSYTLoc>sEeHk~rt4hh@dBYbJ{0@Yf7hDG7C2W+~X`5Sr4m UVAG(bU_cCd^bQ>jJ$A7C0M{8fZU6uP literal 0 HcmV?d00001 diff --git a/wai/quantum-conformance/reject/v2_opcode_10.wqc b/wai/quantum-conformance/reject/v2_opcode_10.wqc new file mode 100644 index 0000000000000000000000000000000000000000..465f1427ac983b05542bbde175ae8aadb6853205 GIT binary patch literal 147 zcmWF!bT(pQU}OM-A|Pf05u89=t(00(qGY91o|vguSelquQktumoLQ7ynpt9`Z>XfB zlowxEnv_{mtYl>bXfB zlowxEnv_{mtYl>b1;L}l{z$-9x7(s3zPl+H_y#5nnVb$gql6SZ znNdw<%m@6dKo3tOJycAkMr|qEO2sTz&_Oy6^+IKY-mt0BXR`mi^F?EPY&RSW8F{^4 jc5ubwN8fJR8-;Ms?l^aEbY%BBC-^FEod_`%Qnq(~_ZBD` literal 0 HcmV?d00001 diff --git a/wai/quantum-conformance/v2/expected_hashes.json b/wai/quantum-conformance/v2/expected_hashes.json new file mode 100644 index 000000000..ddcf98c29 --- /dev/null +++ b/wai/quantum-conformance/v2/expected_hashes.json @@ -0,0 +1,56 @@ +{ + "bell.wqc": { + "capability": "wai.quantum.circuit2", + "n_qubits": 2, + "n_ops": 2, + "circuit_hash": "6f6199943059fc4d90554d7c33f1ac69723603f5128962a6e3cac9c765484d2b", + "statevector_hash": "db68a50b08a8c2eabc89a881db9fcb130fff89dc36182d4c7f90da5bf4720b15", + "measure": { + "seed": 12648430, + "shots": 100000, + "histogram_hash": "0893f48ef32ca9f2865f6617537f0a4a5334f67e3c944bba845beb2f377acdb3" + } + }, + "ghz3.wqc": { + "capability": "wai.quantum.circuit2", + "n_qubits": 3, + "n_ops": 3, + "circuit_hash": "c5911b898a899c999ce01730bb825c84678569493ac513760e15dcbccd84227f", + "statevector_hash": "51850f28817200e9e03980b60f34879c7bd0147a07b0c1746528b801cae2e2f8" + }, + "qft3.wqc": { + "capability": "wai.quantum.circuit2", + "n_qubits": 3, + "n_ops": 9, + "circuit_hash": "c23a1b1e31b87705999cbc54866f4c9e3e4c4d2b274d6260672121abb5ca89b8", + "statevector_hash": "70fe25ec22008f86076328b2c0908ede778e6d8227e563e8c7dcd89f7e844bf2" + }, + "qft5.wqc": { + "capability": "wai.quantum.circuit2", + "n_qubits": 5, + "n_ops": 21, + "circuit_hash": "8559c83769ac8c0cb3aabc64672233aa569cd4ce92aa6545f1449982b9aa8b8c", + "statevector_hash": "50b01fab0bfee966e553a947aea6aa09cebf8f21ba201cd548d703048d66f2ba" + }, + "qft5_x22.wqc": { + "capability": "wai.quantum.circuit2", + "n_qubits": 5, + "n_ops": 24, + "circuit_hash": "c126604030b16f4f1e88e6683894f9316262f49b3419be06ab74c7dbd2441651", + "statevector_hash": "5def2fca1d29e9e2b9ca2020cd2fa2dc056d07ce2e21b6e101b6116a03efd61c" + }, + "dyadic_ladder.wqc": { + "capability": "wai.quantum.circuit2", + "n_qubits": 2, + "n_ops": 72, + "circuit_hash": "8a6dcf0d41ecefd1a9761fef49b385d7dffe7543a9382efda02bfe4f7a0896fe", + "statevector_hash": "28bdb7265c10d798012ac78b604327700acd8cdac0e1f88ed4045f8b0b78bc7c" + }, + "qft8_ones.wqc": { + "capability": "wai.quantum.circuit2", + "n_qubits": 8, + "n_ops": 56, + "circuit_hash": "286f044750b19937f6e7bf6f534053aa74a21777516c773dbd7c254df7301b34", + "statevector_hash": "e059d43a574780d819a872f7904bf96918ba5880e1c7c32b38159306acd559fe" + } +} diff --git a/wai/quantum-conformance/v2/vectors/bell.wqc b/wai/quantum-conformance/v2/vectors/bell.wqc new file mode 100644 index 0000000000000000000000000000000000000000..83badc0cd48304d88a75ecc604569cf6d04c5f73 GIT binary patch literal 218 zcmXX=%L>9k5NoY>|6#6{+SXD^ul|67;8CW#TOH_gAE=Zb{Bvi)Kmr*;GPlbqkAg5@ zXy(Stjv0MfD&_*}ZJ~x2QZhRntm_j&S~#-#A%hr(`|KLB2I`!R>y;+)&r4+!?5DEe zSgVoG$25iqT4j{)6dkqgsO5DpKPX9@6FtDX;ngiO2t)tZ0!Aqbb(WV!Rrmt)9Q|CD Vdc@G6A+{jKesUHqjRSVD{s1AZIsX6v literal 0 HcmV?d00001 diff --git a/wai/quantum-conformance/v2/vectors/dyadic_ladder.wqc b/wai/quantum-conformance/v2/vectors/dyadic_ladder.wqc new file mode 100644 index 0000000000000000000000000000000000000000..48485192699627f23711dbcbc0d1f60ca9734357 GIT binary patch literal 566 zcmdVU%Sr<=6vpw+)ZV<*`(3MKYnj$jTy*QkrGns6VkRR2XKK>~74eOHNRvOlL<9M8 zPWU)EPY<_w+DIDV*l^RxZ)m@T{&Zp@KBUQJUe?Z6nKD*+fFN=3k7Pi%A|D zUukR9f4Q#hUcsCP`pZ~=Q_?*Z>B7#ws^cKzGPw4eP(O%a~FPQ57ueM zx)o_hI+1RKP%lYB8BDQ>OW49TcCd>*T*eh##Wn2XI&R=5Zs9iW;4bdrJ|5s99^o;b O;3*F949{_)_x}S)PA~TW literal 0 HcmV?d00001 diff --git a/wai/quantum-conformance/v2/vectors/ghz3.wqc b/wai/quantum-conformance/v2/vectors/ghz3.wqc new file mode 100644 index 0000000000000000000000000000000000000000..7c7c1853b9e2f1c91827115074abdb90b72b9d8f GIT binary patch literal 159 zcmXYrO$q`r429e36}&*U4pnEw#caKRg5XkWeuP}AG`qJfHV*AU=LFvwx6T=*R`mK|89x~?%^hc*nT)*Ib3Vv7$=YE0V4G$=#CB#QN7g%$eqm@X72Y#VOU-~yTdzI9RQ;l5e(S{F2r zn`MQ)j&5@Krnyqr-GV*N?MquSdlfu>O8#1~pcKG~m@|+?j#dgGvy}kZWisT?e*s>W BD1rb0 literal 0 HcmV?d00001 diff --git a/wai/quantum-conformance/v2/vectors/qft5.wqc b/wai/quantum-conformance/v2/vectors/qft5.wqc new file mode 100644 index 0000000000000000000000000000000000000000..101916660b230190fe3ef12f1b5c2f8c73e25ede GIT binary patch literal 263 zcmXwzO$x#=5QQg`xaa}AK(>~u^=Hwo7f=vfN=#D;*lN=R73tlb0Udac$;abmPKRBI zAb=*2kZmM=w0lKe>8J5UpG+51TUZ}#^0AyRb*{T}pDsQQy4uhqwJv!3%hxUzrhRPJ z6l;PZyjtY?YN8t)ciNk8ntP~^v-)jnZ}&y$^f;wo6$vr{bP>4(Bv&BGC!iFcnF)c6 ig=8!gV`PlT03`#-Kq9Zy$SHR2|L>F#GysyDKzsq<^C^4) literal 0 HcmV?d00001 diff --git a/wai/quantum-conformance/v2/vectors/qft5_x22.wqc b/wai/quantum-conformance/v2/vectors/qft5_x22.wqc new file mode 100644 index 0000000000000000000000000000000000000000..c60de2281e67e134ba352b7f764223e1ea96244e GIT binary patch literal 278 zcmXwzL2d#u3`K1xEanERvjt6C3M|-i0V-AMvI?0@6^R)jB!a5y4SMMMOJK!M_Ser( zKHjzqAttS?Ex@1lkBhhBHtyC zEUh0->!sTI>{Wh!p1Ut!;w2(898%vObN4>$zI~U%uFa$So$9NMpD+nv35Te`&Pqd~ sv}BYMvKHQkSs=54%oeiVZde3@7zhS}5saD$7pSn0SskoJWROJs4_8wum;e9( literal 0 HcmV?d00001 diff --git a/wai/quantum-conformance/v2/vectors/qft8_ones.wqc b/wai/quantum-conformance/v2/vectors/qft8_ones.wqc new file mode 100644 index 0000000000000000000000000000000000000000..fad622ddda083c5228090b07d82e01519f1bfb39 GIT binary patch literal 462 zcmXw!-Acni5Ju-`cVk{-Z!J-)Na?LUfP&zq#3YpkTWy-4BEGxdCw3txlbMs9naBH^ zrHKm8h{uRqe17NQbIR-d(UxXEx4UWHHeDIJxlGHmi@eCYr~Uj~rbAw@@G)YRLrSZpkk4V zB`TPyRyP*iS@gi7M;0+zt-+WM&U6S&WBOX7F%g{!Y*K47hS-PANuWB$+!aS8aX`kn R#JGgSV~lRjg(Pqx@n7gqE0zEN literal 0 HcmV?d00001 diff --git a/wai/quantum-conformance/vectors/dyadic_ladder.wqc b/wai/quantum-conformance/vectors/dyadic_ladder.wqc new file mode 100644 index 0000000000000000000000000000000000000000..2a1f25534e0b5d4bc866b47be8d45ef67c58dc0c GIT binary patch literal 564 zcmdVU%SuBr5XSMIQ+xAT?{_QNT8>u5MYnETDhMtmCOHzY)t)BUBEFFi>Ew?uVIV(d z$j4-!9&QJDEvw1-keerdr2SHE7SvFEYF|vE9)~z8jIT`e;e4R_YWzHjFFs7w@KTyM za+NoKaqB`+`kAvg*Agr%Ee}^0ef6q?o4PdAy!oZxud znu%7Togmc7GAYFz>$rdoY+?)B*uh0y!ev~+Rb0a^uHy!7;udb>4({R}?&AR-;t?L> L3HI<5|MmXANRcmu literal 0 HcmV?d00001 diff --git a/wai/quantum-conformance/vectors/qft5_x22.wqc b/wai/quantum-conformance/vectors/qft5_x22.wqc new file mode 100644 index 0000000000000000000000000000000000000000..ca8b8a58430a143f0d24d9b063988e3ba32830d3 GIT binary patch literal 276 zcmXwzL23gr3`Hf`UCa%-_BLVK&@_wL<^l;6vJ9^6DTd6nGqFiR=nZ=4>O(86hxGL= z`>@}xgqSqGGzIw4yt{NQu5%Zk+o}4o)S(S|4mFpjm0P;;IMw%D&aQjarVeS&;m;mZ zY5ROlakExqKYJa#JTKjcFX^1HG@NqZf6no35pO=`uyfy!`X=KaOafTKA!^XG(vT=E r8RY|63va_Lkl8?H3t4YBECN9c1OvecO5F$-sIdRFI#`3qU=#5#*fl97 literal 0 HcmV?d00001 diff --git a/wai/quantum-conformance/vectors/qft8_ones.wqc b/wai/quantum-conformance/vectors/qft8_ones.wqc new file mode 100644 index 0000000000000000000000000000000000000000..163d10ef9c2020e0006ea545b8fa78a301bda50c GIT binary patch literal 460 zcmXw!+e*Vg5QgWlyD_h^x0Y&Ar1VxFKtb?QVh*xkt4$NEi0|(Ii(SZ<$;_9XnWu-_ zm5B<@2OcBt@cEtl8x*A1wiW(?tq^OCaRus{-Rx>K*R4h=jNW~Hr z%v7rzi|#CXV9_Isn5@=dOb2H=1g0^4t&IC58H5o(f!{#JVonr2aBa%2EV_aff PLgFb#H|IhUxRCfS7b7bp literal 0 HcmV?d00001 diff --git a/wai/registry/capabilities.json b/wai/registry/capabilities.json index f8d98874c..3e8d28181 100644 --- a/wai/registry/capabilities.json +++ b/wai/registry/capabilities.json @@ -923,6 +923,20 @@ "edition": null, "registered": "2026-07-25" }, + { + "capability": "wai.quantum.circuit2", + "kind": "payload", + "status": "registered", + "media": "binary", + "role": "standalone", + "tier": "lossless", + "licence": "royalty-free", + "replicate": "standard-defined", + "sets": [], + "spec_section": "§5 Interactive, temporal & computed content", + "edition": null, + "registered": "2026-10-09" + }, { "capability": "wai.quantum.job", "kind": "record", diff --git a/wai/vocab/v2.shapes.ttl b/wai/vocab/v2.shapes.ttl index dd4bfb0e9..ff62ab10f 100644 --- a/wai/vocab/v2.shapes.ttl +++ b/wai/vocab/v2.shapes.ttl @@ -70,6 +70,7 @@ wai:ManifestShape a sh:NodeShape ; wai:wai.prior.nnc wai:wai.prior.set wai:wai.quantum.circuit + wai:wai.quantum.circuit2 wai:wai.semantic.clip wai:wai.semantic.perco2 wai:wai.splat.4d @@ -150,6 +151,7 @@ wai:ManifestShape a sh:NodeShape ; wai:wai.prior.nnc wai:wai.prior.set wai:wai.quantum.circuit + wai:wai.quantum.circuit2 wai:wai.semantic.clip wai:wai.semantic.perco2 wai:wai.splat.4d @@ -244,6 +246,7 @@ wai:ComponentShape a sh:NodeShape ; wai:wai.prior.nnc wai:wai.prior.set wai:wai.quantum.circuit + wai:wai.quantum.circuit2 wai:wai.semantic.clip wai:wai.semantic.perco2 wai:wai.splat.4d @@ -324,6 +327,7 @@ wai:ComponentShape a sh:NodeShape ; wai:wai.prior.nnc wai:wai.prior.set wai:wai.quantum.circuit + wai:wai.quantum.circuit2 wai:wai.semantic.clip wai:wai.semantic.perco2 wai:wai.splat.4d diff --git a/wai/vocab/v2.ttl b/wai/vocab/v2.ttl index 9747b7ac6..77495b0fc 100644 --- a/wai/vocab/v2.ttl +++ b/wai/vocab/v2.ttl @@ -847,6 +847,18 @@ wai:wai.quantum.circuit a wai:Capability ; wai:specSection "§5 Interactive, temporal & computed content" ; wai:registeredOn "2026-07-25"^^xsd:date . +wai:wai.quantum.circuit2 a wai:Capability ; + rdfs:label "wai.quantum.circuit2" ; + wai:kind wai:kind-payload ; + wai:status wai:status-registered ; + wai:media wai:media-binary ; + wai:role wai:role-standalone ; + wai:tier wai:tier-lossless ; + wai:licence wai:licence-royalty-free ; + wai:replicateClass wai:replicate-standard-defined ; + wai:specSection "§5 Interactive, temporal & computed content" ; + wai:registeredOn "2026-10-09"^^xsd:date . + wai:wai.quantum.job a wai:Capability ; rdfs:label "wai.quantum.job" ; wai:kind wai:kind-record ; diff --git a/wai/wai-rs/Cargo.toml b/wai/wai-rs/Cargo.toml index 46ec1caa4..925f47882 100644 --- a/wai/wai-rs/Cargo.toml +++ b/wai/wai-rs/Cargo.toml @@ -234,7 +234,8 @@ world_model = ["world", "provenance"] # (one envelope = movie + sound). No codec/ML deps (the render is the sink's). film = ["world", "score"] # Deterministic quantum-circuit SIMULATION (extensions/quantum-sim, -# `wai.quantum.circuit`): the wire carries a gate op-log (WQC container), the sink +# `wai.quantum.circuit2` and the frozen `wai.quantum.circuit`): the wire carries a +# gate op-log (WQC container), the sink # classically simulates it in a fixed-point complex floor, the statevector is # byte-identical on every machine (statevector-equivalence). Pure i64, no float — # blake3 for the conformance/receipt hashes, same discipline as the other floor diff --git a/wai/wai-rs/src/bin/wai_quantum.rs b/wai/wai-rs/src/bin/wai_quantum.rs index bdec6038e..a2073ede9 100644 --- a/wai/wai-rs/src/bin/wai_quantum.rs +++ b/wai/wai-rs/src/bin/wai_quantum.rs @@ -19,6 +19,12 @@ //! wai-quantum verify # read a receipt JSON on stdin //! ``` //! +//! A circuit is built in `wai.quantum.circuit2`, whose phase table is correctly +//! rounded. `--gateset v1` builds it in the frozen `wai.quantum.circuit` instead, +//! to reproduce or re-verify content made with it. Every command that builds a +//! circuit from an algorithm, OpenQASM or QIR (`run`, `receipt`, `qasm`, `qir`, +//! `entropy`) takes it, and so does `fabric`. +//! //! Beyond running circuits, the same binary answers what a state *carries* and //! what transport does to it: //! @@ -68,7 +74,7 @@ use wai_quantum::ed25519_dalek::SigningKey; use std::io::Read; -use wai::quantum::{Circuit, Measure, ONE}; +use wai::quantum::{BaseGate, Circuit, Gateset, Measure, ONE}; use wai::quantum_channel::{tomography, Channel}; use wai::quantum_comm::{bb84, clone_fidelity}; use wai::quantum_info::{chsh, DensityMatrix}; @@ -138,15 +144,16 @@ fn usage() -> ! { "wai-quantum — deterministic quantum computation (native + cloud WASM)\n\n\ USAGE:\n \ wai-quantum list\n \ - wai-quantum run [-n QUBITS] [-p PARAM] [--top K] [--json]\n \ - wai-quantum run --qasm [--top K] [--json] (OpenQASM on stdin)\n \ - wai-quantum run --qir [--top K] [--json] (QIR on stdin)\n \ - wai-quantum qasm [-n QUBITS] [-p PARAM]\n \ - wai-quantum qir [-n QUBITS] [-p PARAM] [--no-measure]\n \ - wai-quantum receipt [-n QUBITS] [-p PARAM] [--shots N] [--seed S]\n \ - wai-quantum verify (receipt JSON on stdin)\n\n\ + wai-quantum run [-n QUBITS] [-p PARAM] [--top K] [--json] [--gateset v1|v2]\n \ + wai-quantum run --qasm [--top K] [--json] [--gateset v1|v2] (OpenQASM on stdin)\n \ + wai-quantum run --qir [--top K] [--json] [--gateset v1|v2] (QIR on stdin)\n \ + wai-quantum qasm [-n QUBITS] [-p PARAM] [--gateset v1|v2]\n \ + wai-quantum qir [-n QUBITS] [-p PARAM] [--no-measure] [--gateset v1|v2]\n \ + wai-quantum receipt [-n QUBITS] [-p PARAM] [--shots N] [--seed S] [--gateset v1|v2]\n \ + wai-quantum verify (receipt JSON on stdin)\n \ + --gateset v2 (the default) is wai.quantum.circuit2; v1 is the frozen wai.quantum.circuit.\n\n\ INFORMATION AND TRANSPORT:\n \ - wai-quantum entropy [-n QUBITS] [--keep 0,1] [--json]\n \ + wai-quantum entropy [-n QUBITS] [--keep 0,1] [--json] [--gateset v1|v2]\n \ wai-quantum channel [--prob P] [--state zero|one|plus] [--json]\n \ wai-quantum bb84 [--rounds N] [--seed S] [--eavesdrop] [--json]\n \ wai-quantum chsh [--json]\n \ @@ -181,7 +188,7 @@ fn usage() -> ! { [--odd]] [--alpha A] [--steps N] [--samples S] [--lr R]\n \ [--init W] [--guide ε] [--exact] [--seed S] [--json]\n\n\ THE FABRIC (members, parity against the CPU reference, placement):\n \ - wai-quantum fabric [--circuits N] [--seed S] [--json]\n\n\ + wai-quantum fabric [--circuits N] [--seed S] [--gateset v1|v2] [--json]\n\n\ OPTIMISATION (simulated bifurcation, annealing, exact):\n \ wai-quantum qio (--gset FILE | --sk N [--instance S]) [--method dsb|bsb|hdsb|hbsb|sa|exact]\n \ [--steps N] [--trials T] [--dt D] [--c1 C] [--gamma G] [--beta0 B --beta1 B] [--descend]\n \ @@ -231,21 +238,43 @@ fn num(args: &[String], long: &str, short: &str, dflt: T) } } +/// What `--gateset v1` prints on standard error, never on standard output or in +/// JSON, for a circuit whose phases circuit/1 does not reproduce. +const V1_NOTE: &str = + "circuit/1 table: P(k≥8) deviates by up to 12868 units of 2^-30, P(k≥19) applies no phase; use --gateset v2"; + +/// The gateset `--gateset` names: `v2` (`wai.quantum.circuit2`, the default) or +/// `v1` (the frozen `wai.quantum.circuit`). +fn gateset_from(args: &[String]) -> Gateset { + match opt(args, "--gateset", "").as_deref() { + None | Some("v2") => Gateset::V2, + Some("v1") => Gateset::V1, + Some(other) => die(&format!("unknown gateset '{other}' (v1, v2)")), + } +} + /// Build the circuit a command operates on: either a named algorithm or OpenQASM -/// read from stdin (`--qasm`). +/// read from stdin (`--qasm`), in the gateset `--gateset` names. A circuit/1 +/// circuit with a `P(k ≥ 8)` gets the note on standard error. fn circuit_from(args: &[String], algo: Option<&String>) -> Circuit { - if flag(args, "--qasm") { - return qt::from_qasm(&read_stdin()).unwrap_or_else(|e| die(&format!("QASM parse: {e}"))); + let gs = gateset_from(args); + let mut c = if flag(args, "--qasm") { + qt::from_qasm(&read_stdin()).unwrap_or_else(|e| die(&format!("QASM parse: {e}"))) + } else if flag(args, "--qir") { + wai::quantum_qir::from_qir(&read_stdin()).unwrap_or_else(|e| die(&format!("QIR parse: {e}"))) + } else { + let id = algo.unwrap_or_else(|| usage()); + let n: u8 = num(args, "--qubits", "-n", 3); + let p: u32 = num(args, "--param", "-p", 0); + qt::build_algorithm(id, n, p) + .unwrap_or_else(|| die(&format!("unknown algorithm '{id}' (try `wai-quantum list`)"))) + }; + // Every circuit2 circuit is a valid circuit/1 circuit, so the switch is safe. + c.gateset = gs; + if gs == Gateset::V1 && c.ops.iter().any(|g| g.base == BaseGate::P && g.param >= 8) { + eprintln!("{V1_NOTE}"); } - if flag(args, "--qir") { - return wai::quantum_qir::from_qir(&read_stdin()) - .unwrap_or_else(|e| die(&format!("QIR parse: {e}"))); - } - let id = algo.unwrap_or_else(|| usage()); - let n: u8 = num(args, "--qubits", "-n", 3); - let p: u32 = num(args, "--param", "-p", 0); - qt::build_algorithm(id, n, p) - .unwrap_or_else(|| die(&format!("unknown algorithm '{id}' (try `wai-quantum list`)"))) + c } fn describe(c: &Circuit) -> (String, String, u64, String) { @@ -254,7 +283,7 @@ fn describe(c: &Circuit) -> (String, String, u64, String) { format!("{backend:?}"), why.to_string(), qt::statevector_work(c), - hx(&qt::circuit_id(c)), + hx(&qt::circuit_id(c).unwrap_or_else(|e| die(&format!("circuit id: {e}")))), ) } @@ -451,14 +480,16 @@ fn main() { .map(|(k, p)| format!("{{\"state\":\"{k:0n$b}\",\"p\":{p}}}", n = n)) .collect(); println!( - "{{\"circuit_id\":\"{cid}\",\"n_qubits\":{},\"n_gates\":{},\"backend\":\"{backend}\",\ + "{{\"circuit_id\":\"{cid}\",\"capability\":\"{}\",\"n_qubits\":{},\"n_gates\":{},\"backend\":\"{backend}\",\ \"work_amp_updates\":{work},\"statevector_hash\":\"{hash}\",\"top\":[{}]}}", + c.gateset.capability(), c.n_qubits, c.ops.len(), items.join(",") ); } else { println!("circuit-id {cid}"); + println!("capability {}", c.gateset.capability()); println!("qubits {}", c.n_qubits); println!("gates {}", c.ops.len()); println!("backend {backend} ({why})"); @@ -1305,17 +1336,20 @@ fn main() { // The fabric this process stands on: its detected members, and the // fixed-point core's 32-bit-lane port certified against the native - // simulator on random circuits, then placed. The member lines and the - // certificate's member describe the host, so they differ between - // platforms; the parity and the corpus digest do not. + // simulator on random circuits of the gateset `--gateset` names, then + // placed. The member lines and the certificate's member describe the + // host, so they differ between platforms; the parity and the corpus + // digest do not. "fabric" => { use wai::quantum_fabric::*; let circuits: usize = num(args, "--circuits", "", 200); let seed: u64 = num(args, "--seed", "", 7); + let gs = gateset_from(args); + let kernel = apply_kernel(gs); let mut member = detect_local(); - let cert = certify_lanes(&member.id, circuits, seed); - member.kernels.push(KernelEntry { kernel: KERNEL_CIRCUIT_APPLY.into(), implementation: "lanes32".into(), certificate: cert, energy: None }); - let placed = place(std::slice::from_ref(&member), KERNEL_CIRCUIT_APPLY, &Parity::BitExact); + let cert = certify_lanes(gs, &member.id, circuits, seed); + member.kernels.push(KernelEntry { kernel: kernel.into(), implementation: "lanes32".into(), certificate: cert, energy: None }); + let placed = place(std::slice::from_ref(&member), kernel, &Parity::BitExact); let parity = |p: &Parity| match p { Parity::BitExact => "bit-exact".to_string(), Parity::Within { max_ulp, max_abs } => format!("within {max_ulp} ulp ({max_abs:.3e})"), @@ -1330,7 +1364,8 @@ fn main() { .map(|k| format!("{{\"implementation\":{:?},\"kernel\":{:?},\"parity\":{:?},\"generator\":{:?},\"cases\":{},\"corpus\":\"{}\"}}", k.implementation, k.kernel, parity(&k.certificate.parity), k.certificate.generator, k.certificate.cases, hex(&k.certificate.corpus))) .collect(); println!( - "{{\"member\":{:?},\"class\":\"{}\",\"presence\":\"{}\",\"detail\":{:?},\"kernels\":[{}],\"placed\":{}}}", + "{{\"capability\":\"{}\",\"member\":{:?},\"class\":\"{}\",\"presence\":\"{}\",\"detail\":{:?},\"kernels\":[{}],\"placed\":{}}}", + gs.capability(), member.id, member.class.label(), member.presence.label(), @@ -1341,8 +1376,8 @@ fn main() { } else { println!("member {} ({}, {})", member.id, member.class.label(), member.presence.label()); println!("detail {}", member.detail); - println!("kernel {KERNEL_CIRCUIT_APPLY}"); - for k in &member.kernels { + println!("kernel {kernel}"); + for k in member.kernels.iter().filter(|k| k.kernel == kernel) { if k.implementation == REFERENCE { println!(" {:<11}{} (the reference)", k.implementation, parity(&k.certificate.parity)); } else { diff --git a/wai/wai-rs/src/bin/wai_quantum_meter.rs b/wai/wai-rs/src/bin/wai_quantum_meter.rs index a9898193a..f2b7e693e 100644 --- a/wai/wai-rs/src/bin/wai_quantum_meter.rs +++ b/wai/wai-rs/src/bin/wai_quantum_meter.rs @@ -1,5 +1,8 @@ //! `wai_quantum_meter` — the native sink-metering path for a -//! `wai.quantum.circuit` reconstruction's energy (extensions/quantum-sim § Receipt). +//! `wai.quantum.circuit2` or `wai.quantum.circuit` reconstruction's energy +//! (extensions/quantum-sim § Receipt). The circuit is read from its `WQC2` or +//! `WQC1` container and keeps that container's gateset, so the receipt's +//! circuit hash binds the capability the file was written under. //! //! Measures the **real CPU energy** a classical sink spends reconstructing a WQC //! circuit's statevector, with no elevated privileges. On arm64 macOS the SoC @@ -62,8 +65,9 @@ fn main() { let work = work_amp_updates(n, n_ops); eprintln!( - "metering {path} ({n} qubits, {n_ops} ops, work {work} amp-updates), \ - no sudo — IOReport via macmon" + "metering {path} ({}, {n} qubits, {n_ops} ops, work {work} amp-updates), \ + no sudo — IOReport via macmon", + circuit.gateset.capability() ); // The §3 protocol (energy_burst): an idle bracket, then reconstructions on diff --git a/wai/wai-rs/src/bin/wai_quantum_vectors.rs b/wai/wai-rs/src/bin/wai_quantum_vectors.rs index e665bdbe4..58a6e1ee1 100644 --- a/wai/wai-rs/src/bin/wai_quantum_vectors.rs +++ b/wai/wai-rs/src/bin/wai_quantum_vectors.rs @@ -5,16 +5,46 @@ //! hash and statevector hash (and, where a measurement is pinned, a shot-histogram //! hash — shot-histogram-equivalence). //! +//! The corpus has three trees: +//! +//! - `/`: `wai.quantum.circuit` (`WQC1`), `vectors/*.wqc` and +//! `expected_hashes.json`. The four original vectors are frozen; three +//! table-sensitive vectors follow them. An entry with no `capability` is a +//! `wai.quantum.circuit` vector, as every entry of the original manifest is. +//! - `/v2/`: `wai.quantum.circuit2` (`WQC2`), the same layout and the same +//! seven circuits, each entry naming its `capability`. +//! - `/reject/`: containers a conforming reader MUST refuse, and +//! `reject.json`, which maps each file to the class it is refused as +//! (`malformed`, `invalid` or `contract`). +//! +//! `verify` checks all three: each vector decodes to the capability its entry +//! names and reproduces its hashes, and each reject file fails with its class. +//! //! wai_quantum_vectors gen | verify +use std::collections::BTreeSet; use std::path::Path; -use wai::quantum::{circuit_hash, from_wqc, to_wqc, Circuit, Measure}; +use wai::quantum::{circuit_hash, from_wqc, to_wqc, BaseGate, Circuit, Gate, Gateset, Measure}; + +/// The circuit2 tree, under the corpus directory. +const V2_DIR: &str = "v2"; +/// The reject tree, under the corpus directory. +const REJECT_DIR: &str = "reject"; +/// The reject tree's listing: file → class. +const REJECT_LISTING: &str = "reject.json"; fn main() { let a: Vec = std::env::args().collect(); match (a.get(1).map(String::as_str), a.get(2)) { (Some("gen"), Some(d)) => generate(Path::new(d)), - (Some("verify"), Some(d)) => verify(Path::new(d)), + (Some("verify"), Some(d)) => { + let fails = verify(Path::new(d)); + if fails > 0 { + eprintln!("{fails} failed — NOT conformant"); + std::process::exit(1); + } + println!("all vectors pass: statevector-equivalence holds, and every reject file is refused as listed"); + } _ => { eprintln!("usage: wai_quantum_vectors "); std::process::exit(2); @@ -26,81 +56,484 @@ fn hex(b: &[u8]) -> String { b.iter().map(|x| format!("{x:02x}")).collect() } -/// The canonical vectors: a Bell pair, a 3-qubit GHZ, and QFTs of 3 and 5 qubits -/// (the "quantum FFT" — Hadamard + controlled-phase ladder + swaps). The Bell pair +/// The canonical vectors, in the gateset `gs`. +/// +/// The first four: a Bell pair, a 3-qubit GHZ, and QFTs of 3 and 5 qubits (the +/// "quantum FFT" — Hadamard + controlled-phase ladder + swaps). The Bell pair /// also pins a measurement so the corpus exercises shot-histogram-equivalence. -fn vectors() -> Vec<(&'static str, Circuit, Option)> { - let mut bell = Circuit::new(2); +/// Every controlled phase of a QFT from |0…0⟩ acts while its control is |0⟩, so +/// these four see `H` but no `P(k)`. +/// +/// The three that follow see the phase table. `qft5_x22` and `qft8_ones` run +/// the QFT from a basis state with set bits. `dyadic_ladder` sets qubit 0, +/// applies Y, Z, S, S†, T, T† and I to qubit 1, and then every `P(1..=32)`, +/// twice, to qubit 1 under qubit 0 as control, so a change of ±1 to either +/// component of any table entry changes its statevector hash. +fn vectors(gs: Gateset) -> Vec<(&'static str, Circuit, Option)> { + let mut bell = Circuit::with_gateset(2, gs); bell.h(0).cx(0, 1); - let mut ghz = Circuit::new(3); + let mut ghz = Circuit::with_gateset(3, gs); ghz.h(0).cx(0, 1).cx(1, 2); + let qft = |n: u8| { + let mut c = Circuit::qft(n); + c.gateset = gs; + c + }; + let x_then_qft = |n: u8, xs: &[u8]| { + let mut c = Circuit::with_gateset(n, gs); + for &q in xs { + c.x(q); + } + c.ops.extend(Circuit::qft(n).ops); + c + }; + + let mut ladder = Circuit::with_gateset(2, gs); + ladder.x(0).y(1).z(1).s(1); + let one = |base| Gate { base, controls: vec![], target: 1, param: 0 }; + ladder.ops.push(one(BaseGate::Sdg)); + ladder.t(1); + ladder.ops.push(one(BaseGate::Tdg)); + ladder.ops.push(one(BaseGate::I)); + for _ in 0..2 { + for k in 1..=32 { + ladder.cp(k, 0, 1); + } + } + vec![ ("bell", bell, Some(Measure { seed: 0xC0FFEE, shots: 100_000 })), ("ghz3", ghz, None), - ("qft3", Circuit::qft(3), None), - ("qft5", Circuit::qft(5), None), + ("qft3", qft(3), None), + ("qft5", qft(5), None), + ("qft5_x22", x_then_qft(5, &[1, 2, 4]), None), + ("dyadic_ladder", ladder, None), + ("qft8_ones", x_then_qft(8, &[0, 1, 2, 3, 4, 5, 6, 7]), None), ] } -fn generate(dir: &Path) { - let v = dir.join("vectors"); - std::fs::create_dir_all(&v).expect("mkdir"); +/// One vector tree: `vectors/.wqc` for each vector and its +/// `expected_hashes.json`, as (path under `prefix`, bytes). With +/// `name_capability`, each entry names its capability first. +fn tree_files(prefix: &str, gs: Gateset, name_capability: bool) -> Vec<(String, Vec)> { + let mut files = Vec::new(); let mut manifest = serde_json::Map::new(); - for (name, circuit, measure) in vectors() { - let bytes = to_wqc(&circuit, measure); - std::fs::write(v.join(format!("{name}.wqc")), &bytes).expect("write wqc"); + for (name, circuit, measure) in vectors(gs) { + let bytes = to_wqc(&circuit, measure).expect("a corpus circuit is valid"); + files.push((format!("{prefix}vectors/{name}.wqc"), bytes)); let sv = circuit.simulate().expect("simulate"); - let mut entry = serde_json::json!({ - "n_qubits": circuit.n_qubits, - "n_ops": circuit.ops.len(), - "circuit_hash": hex(&circuit_hash(&circuit)), - "statevector_hash": hex(&sv.statevector_hash()), - }); - if let Some(m) = measure { - entry["measure"] = serde_json::json!({ - "seed": m.seed, - "shots": m.shots, - "histogram_hash": hex(&sv.histogram_hash(m.seed, m.shots)), - }); + let mut entry = serde_json::Map::new(); + if name_capability { + entry.insert("capability".into(), gs.capability().into()); } - manifest.insert(format!("{name}.wqc"), entry); + entry.insert("n_qubits".into(), circuit.n_qubits.into()); + entry.insert("n_ops".into(), circuit.ops.len().into()); + entry.insert("circuit_hash".into(), hex(&circuit_hash(&circuit).expect("a corpus circuit is valid")).into()); + entry.insert("statevector_hash".into(), hex(&sv.statevector_hash()).into()); + if let Some(m) = measure { + entry.insert( + "measure".into(), + serde_json::json!({ + "seed": m.seed, + "shots": m.shots, + "histogram_hash": hex(&sv.histogram_hash(m.seed, m.shots)), + }), + ); + } + manifest.insert(format!("{name}.wqc"), entry.into()); + } + let text = serde_json::to_string_pretty(&serde_json::Value::Object(manifest)).unwrap() + "\n"; + files.push((format!("{prefix}expected_hashes.json"), text.into_bytes())); + files +} + +// ---- The reject corpus --------------------------------------------------------- + +const SECT_CONTRACT: u8 = 0x01; +const SECT_OPLOG: u8 = 0x02; +const SECT_MEASURE: u8 = 0x03; + +/// A container from raw section-table entries `(kind, offset, len)` and a blob. +fn wqc_table(magic: &[u8; 4], table: &[(u8, u32, u32)], blob: &[u8]) -> Vec { + let mut out = magic.to_vec(); + out.extend_from_slice(&u16::try_from(table.len()).expect("a short table").to_le_bytes()); + for &(kind, off, len) in table { + out.push(kind); + out.extend_from_slice(&off.to_le_bytes()); + out.extend_from_slice(&len.to_le_bytes()); + } + out.extend_from_slice(blob); + out +} + +/// A container holding `sections` in order, each table entry pointing at its +/// own bytes. +fn wqc_raw(magic: &[u8; 4], sections: &[(u8, &[u8])]) -> Vec { + let (mut table, mut blob) = (Vec::new(), Vec::new()); + for &(kind, data) in sections { + let at = |n: usize| u32::try_from(n).expect("a small section"); + table.push((kind, at(blob.len()), at(data.len()))); + blob.extend_from_slice(data); + } + wqc_table(magic, &table, &blob) +} + +/// One op-log entry, as the wire has it. +fn op(opcode: u8, controls: &[u8], target: u8, param: u16) -> Vec { + let mut out = vec![opcode, u8::try_from(controls.len()).expect("a few controls"), target]; + out.extend_from_slice(¶m.to_le_bytes()); + out.extend_from_slice(controls); + out +} + +/// An op-log holding `ops`. +fn oplog_of(ops: &[Vec]) -> Vec { + let mut out = u32::try_from(ops.len()).expect("a few ops").to_le_bytes().to_vec(); + for o in ops { + out.extend_from_slice(o); + } + out +} + +/// `bytes` with the first `from` replaced by `to`; `from` must be there. +fn swap(bytes: &[u8], from: &str, to: &str) -> Vec { + let s = std::str::from_utf8(bytes).expect("a contract is text"); + assert!(s.contains(from), "{from:?} is not in {s}"); + s.replacen(from, to, 1).into_bytes() +} + +/// The reject corpus: (name, file, the class a reader refuses it as). Each case +/// is a valid two-qubit Bell container with one thing broken, so that the +/// listed rule is the one that refuses it. +fn reject_cases() -> Vec<(&'static str, Vec, &'static str)> { + let bell = oplog_of(&[op(4, &[], 0, 0), op(1, &[0], 1, 0)]); + let canon1 = Gateset::V1.contract_bytes(2); + let canon2 = Gateset::V2.contract_bytes(2); + let v1 = |contract: &[u8]| wqc_raw(b"WQC1", &[(SECT_CONTRACT, contract), (SECT_OPLOG, &bell)]); + let v1_oplog = |oplog: &[u8]| wqc_raw(b"WQC1", &[(SECT_CONTRACT, &canon1), (SECT_OPLOG, oplog)]); + let v2 = |contract: &[u8], oplog: &[u8], measure: Option<&[u8]>| { + let mut sections = vec![(SECT_CONTRACT, contract), (SECT_OPLOG, oplog)]; + sections.extend(measure.map(|m| (SECT_MEASURE, m))); + wqc_raw(b"WQC2", §ions) + }; + let v2_contract = |contract: &[u8]| v2(contract, &bell, None); + let v2_oplog = |oplog: &[u8]| v2(&canon2, oplog, None); + let v2_measure = |measure: &[u8]| v2(&canon2, &bell, Some(measure)); + let mut magic_wqc3 = v2_contract(&canon2); + magic_wqc3[0..4].copy_from_slice(b"WQC3"); + // An op-log of 9 bytes that claims u32::MAX ops. + let n_ops_u32max = [u32::MAX.to_le_bytes().as_slice(), &op(4, &[], 0, 0)].concat(); + // An op-log section whose offset plus length wraps a u32. + let wrap = wqc_table( + b"WQC1", + &[(SECT_CONTRACT, 0, u32::try_from(canon1.len()).expect("a short contract")), (SECT_OPLOG, 0xFFFF_FFF0, 0x20)], + &[canon1.as_slice(), &bell].concat(), + ); + let reordered2 = format!( + r#"{{"n_qubits":2,"ext":"{}","numeric":"{}","frac":30,"gateset":"{}"}}"#, + Gateset::V2.ext(), + Gateset::V2.numeric(), + Gateset::V2.name() + ); + vec![ + ("magic_wqc3", magic_wqc3, "malformed"), + ("v1_ext_other", v1(&swap(&canon1, "circuit/1", "circuit/2")), "contract"), + ("v1_numeric_other", v1(&swap(&canon1, "wai.det.fixed64", "wai.det.amp64")), "contract"), + ("v1_frac_31", v1(&swap(&canon1, "\"frac\":30", "\"frac\":31")), "contract"), + ("v1_frac_float", v1(&swap(&canon1, "\"frac\":30", "\"frac\":30.0")), "contract"), + ("v1_gateset_rn", v1(&swap(&canon1, "dyadicP\"", "dyadicP.rn\"")), "contract"), + ("v1_contract2_in_wqc1", v1(&canon2), "contract"), + ("v1_fields_missing", v1(br#"{"n_qubits":3}"#), "contract"), + ("v1_contract_not_object", v1(&[b"[".as_slice(), &canon1, b"]"].concat()), "contract"), + ("v1_n_qubits_256", v1(&swap(&canon1, "\"n_qubits\":2", "\"n_qubits\":256")), "malformed"), + ("v1_n_qubits_258", v1(&swap(&canon1, "\"n_qubits\":2", "\"n_qubits\":258")), "malformed"), + ( + "v1_contract_twice", + wqc_raw(b"WQC1", &[(SECT_CONTRACT, &canon1), (SECT_OPLOG, &bell), (SECT_CONTRACT, &Gateset::V1.contract_bytes(3))]), + "malformed", + ), + ( + "v1_oplog_twice", + wqc_raw(b"WQC1", &[(SECT_CONTRACT, &canon1), (SECT_OPLOG, &bell), (SECT_OPLOG, &oplog_of(&[op(1, &[], 0, 0)]))]), + "malformed", + ), + ("v1_n_ops_u32max", v1_oplog(&n_ops_u32max), "malformed"), + ("v1_offset_wrap", wrap, "malformed"), + ("v2_contract_whitespace", v2_contract(&swap(&canon2, "\"n_qubits\":2", "\"n_qubits\": 2")), "contract"), + ("v2_contract_key_order", v2_contract(reordered2.as_bytes()), "contract"), + ("v2_contract_extra_member", v2_contract(&swap(&canon2, "}", ",\"note\":\"x\"}")), "contract"), + ("v2_contract1_in_wqc2", v2_contract(&canon1), "contract"), + ( + "v2_duplicate_controls", + v2(&Gateset::V2.contract_bytes(3), &oplog_of(&[op(1, &[0, 0], 2, 0)]), None), + "invalid", + ), + ("v2_param_on_h", v2_oplog(&oplog_of(&[op(4, &[], 0, 7), op(1, &[0], 1, 0)])), "invalid"), + ("v2_oplog_trailing_byte", v2_oplog(&[bell.as_slice(), &[0]].concat()), "malformed"), + ("v2_opcode_10", v2_oplog(&oplog_of(&[op(10, &[], 0, 0)])), "malformed"), + ("v2_measure_spaced", v2_measure(br#"{"seed": 1, "shots": 10, "basis": "computational"}"#), "malformed"), + ("v2_measure_no_shots", v2_measure(br#"{"seed":1,"basis":"computational"}"#), "malformed"), + ("v2_measure_basis_x", v2_measure(br#"{"seed":1,"shots":10,"basis":"x"}"#), "malformed"), + ] +} + +/// The reject tree: `.wqc` for each case and the listing, as (path under +/// `prefix`, bytes). Each case is checked here to fail with its class, so `gen` +/// never writes a reject file the reader accepts. +fn reject_files(prefix: &str) -> Vec<(String, Vec)> { + let mut files = Vec::new(); + let mut listing = serde_json::Map::new(); + for (name, bytes, kind) in reject_cases() { + match from_wqc(&bytes) { + Ok(_) => panic!("reject case {name} is accepted"), + Err(e) => assert_eq!(e.kind(), kind, "reject case {name}: {e}"), + } + listing.insert(format!("{name}.wqc"), kind.into()); + files.push((format!("{prefix}{name}.wqc"), bytes)); + } + let text = serde_json::to_string_pretty(&serde_json::Value::Object(listing)).unwrap() + "\n"; + files.push((format!("{prefix}{REJECT_LISTING}"), text.into_bytes())); + files +} + +/// Every file `gen` writes, as (path under the corpus directory, bytes). +fn corpus_files() -> Vec<(String, Vec)> { + let mut files = tree_files("", Gateset::V1, false); + files.extend(tree_files(&format!("{V2_DIR}/"), Gateset::V2, true)); + files.extend(reject_files(&format!("{REJECT_DIR}/"))); + files +} + +fn generate(dir: &Path) { + for (rel, bytes) in corpus_files() { + let path = dir.join(&rel); + std::fs::create_dir_all(path.parent().expect("a file has a directory")).expect("mkdir"); + std::fs::write(&path, &bytes).expect("write"); } - std::fs::write( - dir.join("expected_hashes.json"), - serde_json::to_string_pretty(&serde_json::Value::Object(manifest)).unwrap() + "\n", - ) - .expect("manifest"); println!("generated quantum vectors into {}", dir.display()); } -fn verify(dir: &Path) { - let m: serde_json::Value = - serde_json::from_slice(&std::fs::read(dir.join("expected_hashes.json")).expect("manifest")).expect("parse"); - let v = dir.join("vectors"); - let mut fails = 0; - for (file, spec) in m.as_object().expect("obj") { - let (circuit, measure) = from_wqc(&std::fs::read(v.join(file)).expect("wqc")).expect("parse"); - let sv = circuit.simulate().expect("simulate"); - let circ_ok = hex(&circuit_hash(&circuit)) == spec["circuit_hash"].as_str().unwrap(); - let sv_ok = hex(&sv.statevector_hash()) == spec["statevector_hash"].as_str().unwrap(); - let hist_ok = match (measure, spec.get("measure")) { - (Some(mm), Some(ms)) => { - hex(&sv.histogram_hash(mm.seed, mm.shots)) == ms["histogram_hash"].as_str().unwrap() - } - (None, None) => true, - _ => false, - }; - let ok = circ_ok && sv_ok && hist_ok; - println!("{} {file}", if ok { "PASS" } else { "FAIL" }); - if !ok { - fails += 1; +// ---- Verification ---------------------------------------------------------------- + +/// Verify all three trees under `dir`; the number of failures. +fn verify(dir: &Path) -> usize { + verify_tree(dir, "") + verify_tree(&dir.join(V2_DIR), &format!("{V2_DIR}/")) + verify_rejects(&dir.join(REJECT_DIR), &format!("{REJECT_DIR}/")) +} + +/// The `.wqc` file names in `dir`. +fn wqc_names(dir: &Path) -> Result, String> { + let entries = std::fs::read_dir(dir).map_err(|e| format!("{}: {e}", dir.display()))?; + let mut names = BTreeSet::new(); + for e in entries { + let name = e.map_err(|e| e.to_string())?.file_name().to_string_lossy().into_owned(); + if name.ends_with(".wqc") { + names.insert(name); } } - if fails > 0 { - eprintln!("{fails} failed — NOT conformant"); - std::process::exit(1); + Ok(names) +} + +/// A JSON object read from `path`. +fn read_object(path: &Path) -> Result, String> { + let bytes = std::fs::read(path).map_err(|e| format!("{}: {e}", path.display()))?; + match serde_json::from_slice(&bytes).map_err(|e| format!("{}: {e}", path.display()))? { + serde_json::Value::Object(o) => Ok(o), + _ => Err(format!("{}: not a JSON object", path.display())), } - println!("all vectors pass: statevector-equivalence holds"); +} + +/// Report one check: `PASS` or `FAIL` with the reason; 1 for a failure. +fn report(label: &str, r: Result) -> usize { + match r { + Ok(note) => { + println!("PASS {label}{note}"); + 0 + } + Err(why) => { + println!("FAIL {label}: {why}"); + 1 + } + } +} + +/// Each listed file and only those are present. +fn same_files(listed: BTreeSet, present: Result, String>) -> Result { + let present = present?; + let missing: Vec<_> = listed.difference(&present).collect(); + let unlisted: Vec<_> = present.difference(&listed).collect(); + if missing.is_empty() && unlisted.is_empty() { + Ok(String::new()) + } else { + Err(format!("listed but missing {missing:?}, present but unlisted {unlisted:?}")) + } +} + +/// A vector tree: `expected_hashes.json` and `vectors/`. +fn verify_tree(dir: &Path, label: &str) -> usize { + let m = match read_object(&dir.join("expected_hashes.json")) { + Ok(m) => m, + Err(why) => return report(&format!("{label}expected_hashes.json"), Err(why)), + }; + let v = dir.join("vectors"); + let mut fails = report(&format!("{label}vectors/ holds the listed files"), same_files(m.keys().cloned().collect(), wqc_names(&v))); + for (file, spec) in &m { + fails += report(&format!("{label}{file}"), check_vector(&v.join(file), spec)); + } + fails +} + +/// One vector against its manifest entry. +fn check_vector(path: &Path, spec: &serde_json::Value) -> Result { + let field = |k: &str| spec.get(k).ok_or(format!("the entry has no {k}")); + let text = |k: &str| field(k).and_then(|v| v.as_str().ok_or(format!("{k} is not a string"))); + let number = |v: &serde_json::Value, k: &str| v.get(k).and_then(|x| x.as_u64()).ok_or(format!("{k} is not an unsigned integer")); + // The original manifest names no capability: its entries are circuit/1. + let capability = match spec.get("capability") { + None => Gateset::V1.capability(), + Some(c) => c.as_str().ok_or("capability is not a string")?, + }; + let gs = Gateset::from_capability(capability).ok_or(format!("unknown capability {capability:?}"))?; + let bytes = std::fs::read(path).map_err(|e| e.to_string())?; + let (circuit, measure) = from_wqc(&bytes).map_err(|e| e.to_string())?; + if circuit.gateset != gs { + return Err(format!("the file is {}, the entry names {capability}", circuit.gateset.capability())); + } + if u64::from(circuit.n_qubits) != number(spec, "n_qubits")? || circuit.ops.len() as u64 != number(spec, "n_ops")? { + return Err("n_qubits or n_ops differ from the entry".into()); + } + let sv = circuit.simulate().map_err(|e| e.to_string())?; + let mut bad = Vec::new(); + if hex(&circuit_hash(&circuit).map_err(|e| e.to_string())?) != text("circuit_hash")? { + bad.push("circuit_hash"); + } + if hex(&sv.statevector_hash()) != text("statevector_hash")? { + bad.push("statevector_hash"); + } + match (measure, spec.get("measure")) { + (Some(mm), Some(ms)) => { + let hist = ms.get("histogram_hash").and_then(|h| h.as_str()).ok_or("histogram_hash is not a string")?; + if mm.seed != number(ms, "seed")? || mm.shots != number(ms, "shots")? { + bad.push("measure seed or shots"); + } + if hex(&sv.histogram_hash(mm.seed, mm.shots)) != hist { + bad.push("histogram_hash"); + } + } + (None, None) => {} + _ => bad.push("measure (present in only one of the file and the entry)"), + } + if bad.is_empty() { Ok(String::new()) } else { Err(format!("{} differ", bad.join(", "))) } +} + +/// The reject tree: every listed file fails with its listed class. +fn verify_rejects(dir: &Path, label: &str) -> usize { + let m = match read_object(&dir.join(REJECT_LISTING)) { + Ok(m) => m, + Err(why) => return report(&format!("{label}{REJECT_LISTING}"), Err(why)), + }; + let mut fails = report(&format!("{label} holds the listed files"), same_files(m.keys().cloned().collect(), wqc_names(dir))); + for (file, kind) in &m { + let r = (|| -> Result { + let kind = kind.as_str().ok_or("the class is not a string")?; + let bytes = std::fs::read(dir.join(file)).map_err(|e| e.to_string())?; + match from_wqc(&bytes) { + Ok(_) => Err(format!("accepted; it must be refused as {kind}")), + Err(e) if e.kind() == kind => Ok(format!(" (refused: {kind})")), + Err(e) => Err(format!("refused as {}, not {kind}: {e}", e.kind())), + } + })(); + fails += report(&format!("{label}{file}"), r); + } + fails +} + +#[cfg(test)] +mod tests { + use super::*; + use std::path::PathBuf; + + fn corpus() -> PathBuf { + Path::new(env!("CARGO_MANIFEST_DIR")).join("../quantum-conformance") + } + + /// A scratch copy of what `gen` writes, removed when dropped. + struct Scratch(PathBuf); + impl Scratch { + fn new(name: &str) -> Scratch { + let dir = std::env::temp_dir().join(format!("wai_quantum_vectors-{name}-{}", std::process::id())); + let _ = std::fs::remove_dir_all(&dir); + generate(&dir); + Scratch(dir) + } + } + impl Drop for Scratch { + fn drop(&mut self) { + let _ = std::fs::remove_dir_all(&self.0); + } + } + + /// The committed corpus is what `gen` writes today, file for file and byte + /// for byte, and it verifies. + #[test] + fn the_committed_corpus_is_what_gen_writes() { + let dir = corpus(); + let files = corpus_files(); + assert_eq!(files.len(), 2 * 8 + 27); + for (rel, want) in files { + let mut got = std::fs::read(dir.join(&rel)).unwrap_or_else(|e| panic!("{rel}: {e}")); + if rel.ends_with(".json") { + // A checkout may convert line endings of text files. + got = String::from_utf8(got).unwrap().replace("\r\n", "\n").into_bytes(); + } + assert!(got == want, "{rel} differs from what gen writes"); + } + assert_eq!(verify(&dir), 0); + } + + /// The reject corpus is the plan's 26 cases, named once each. + #[test] + fn every_reject_case_is_refused_as_listed() { + let cases = reject_cases(); + assert_eq!(cases.len(), 26); + let names: BTreeSet<_> = cases.iter().map(|c| c.0).collect(); + assert_eq!(names.len(), cases.len()); + for (name, bytes, kind) in &cases { + assert_eq!(from_wqc(bytes).map(|_| ()).unwrap_err().kind(), *kind, "{name}"); + } + } + + /// `verify` fails a corpus whose v2 entry names the other capability, whose + /// reject listing names an accepted file or the wrong class, or that holds a + /// file its listing does not. + #[test] + fn verify_fails_what_it_should() { + let s = Scratch::new("verify"); + assert_eq!(verify(&s.0), 0); + let edit = |rel: &str, from: &str, to: &str| { + let p = s.0.join(rel); + let text = std::fs::read_to_string(&p).unwrap(); + assert!(text.contains(from), "{from}"); + std::fs::write(&p, text.replacen(from, to, 1)).unwrap(); + }; + // A circuit2 file under a circuit/1 entry. + edit("v2/expected_hashes.json", "\"wai.quantum.circuit2\"", "\"wai.quantum.circuit\""); + assert_eq!(verify(&s.0), 1); + edit("v2/expected_hashes.json", "\"wai.quantum.circuit\"", "\"wai.quantum.circuit2\""); + // A refused file listed under another class. + edit("reject/reject.json", "\"v2_param_on_h.wqc\": \"invalid\"", "\"v2_param_on_h.wqc\": \"malformed\""); + assert_eq!(verify(&s.0), 1); + edit("reject/reject.json", "\"v2_param_on_h.wqc\": \"malformed\"", "\"v2_param_on_h.wqc\": \"invalid\""); + // An accepted file in the reject tree. + std::fs::copy(s.0.join("v2/vectors/bell.wqc"), s.0.join("reject/v2_param_on_h.wqc")).unwrap(); + assert_eq!(verify(&s.0), 1); + // A file no listing names, in the reject tree and in a vector tree. + std::fs::write(s.0.join("reject/stray.wqc"), b"WQC2").unwrap(); + std::fs::copy(s.0.join("vectors/bell.wqc"), s.0.join("v2/vectors/stray.wqc")).unwrap(); + assert_eq!(verify(&s.0), 3); + // A missing tree. + std::fs::remove_dir_all(s.0.join("v2")).unwrap(); + assert_eq!(verify(&s.0), 3); + } } diff --git a/wai/wai-rs/src/codecs/glossary_check.rs b/wai/wai-rs/src/codecs/glossary_check.rs index b630e2627..22bac592a 100644 --- a/wai/wai-rs/src/codecs/glossary_check.rs +++ b/wai/wai-rs/src/codecs/glossary_check.rs @@ -93,6 +93,7 @@ const GLOSSARY_NO_ENTRY: &[(&str, &str)] = &[ (CAP_NEURAL_INT_MLICPP, WAI_NATIVE), (CAP_NEURAL_INT_SYNTH, WAI_NATIVE), (CAP_QUANTUM_CIRCUIT, WAI_NATIVE), + (CAP_QUANTUM_CIRCUIT2, WAI_NATIVE), (CAP_SPLAT_4D, WAI_NATIVE), (CAP_SPLAT_INT_CODEC, WAI_NATIVE), (CAP_TERNARY_TT, WAI_NATIVE), diff --git a/wai/wai-rs/src/codecs/mod.rs b/wai/wai-rs/src/codecs/mod.rs index e1c0f0c8f..d6029fd01 100644 --- a/wai/wai-rs/src/codecs/mod.rs +++ b/wai/wai-rs/src/codecs/mod.rs @@ -289,8 +289,21 @@ pub const CAP_AVATAR_POSE: &str = "wai.avatar.pose"; // absent: NISQ noise + stochastic measurement break the determinism the standard // rests on. Engine behind the `quantum` feature (crate::quantum). NOT a media // codec — a way to make a quantum computation a transportable, receiptable object. +// Frozen (magic WQC1), so that existing content reproduces bit for bit: its phase +// table deviates from e^{2πi/2^k} for k ≥ 8 and applies no phase for k ≥ 19. New +// content uses wai.quantum.circuit2. pub const CAP_QUANTUM_CIRCUIT: &str = "wai.quantum.circuit"; +// The successor of wai.quantum.circuit (extensions/quantum-sim): the same WQC +// layout under magic WQC2, with a correctly rounded phase table (every P(k) within +// half a unit of 2^30·e^{2πi/2^k}, so the controlled-phase ladder gives the QFT), +// a byte-canonical contract and a strict op-log. A new capability rather than a +// change to the old one, because a registered capability's payload format is fixed +// (SPEC §8): a reader that predates it rejects a WQC2 payload at the magic, and an +// envelope under this string is inert to it. Same equivalence, tier and replicate +// class as wai.quantum.circuit. Engine behind the `quantum` feature. +pub const CAP_QUANTUM_CIRCUIT2: &str = "wai.quantum.circuit2"; + // Balanced-ternary matrix-product-state transform (extensions/ternary-tt): a // quantum-INSPIRED (tensor-network) LOSSY compressor whose whole decode is // integer (per-column integer scale, i64/i128), so the reconstruction is @@ -580,7 +593,7 @@ pub fn known_capabilities() -> &'static [&'static str] { CAP_MESH_GLB, CAP_SPLAT_SPZ, CAP_AUDIO_SCENE, CAP_AUDIO_SCORE, CAP_HAPTIC_SIGNAL, CAP_FEED_STATE, CAP_AVATAR_POSE, CAP_SPLAT_4D, CAP_FILM_LINEAR, CAP_FILM_REEL, // deterministic quantum-circuit simulation (byte-exact statevector) - CAP_QUANTUM_CIRCUIT, + CAP_QUANTUM_CIRCUIT, CAP_QUANTUM_CIRCUIT2, // quantum-inspired ternary-MPS transform (byte-exact recon, lossy) CAP_TERNARY_TT, // neural-network coding: the prior artifact form + the general-tensor candidate @@ -793,12 +806,13 @@ pub fn capability_tier(cap: &str) -> Option { // wai.generate.spectral is generation-equivalence: a deterministic integer // generator reproduces the GENERATED image byte-for-byte from its pin — the // generative analog of lossless (byte-exact, just of created not captured content). - // wai.quantum.circuit is statevector-equivalence: the reconstructed - // fixed-point statevector is byte-identical on every machine — the - // simulation analog of lossless, like world replay (it IS a fixed-point - // approximation of the ideal unitary evolution, but its reconstruction is - // exact and portable; the honest note lives in the extension's §Fidelity). -CAP_WORLD_SCENE | CAP_WORLD_REPLAY | CAP_WORLD_LIVE | CAP_WORLD_MODEL | CAP_MESH_GLB | CAP_AUDIO_SCENE | CAP_AUDIO_SCORE | CAP_HAPTIC_SIGNAL | CAP_FEED_STATE | CAP_AVATAR_POSE | CAP_FILM_LINEAR | CAP_FILM_REEL | CAP_GENERATE_SPECTRAL | CAP_QUANTUM_CIRCUIT => Lossless, + // wai.quantum.circuit and wai.quantum.circuit2 are statevector-equivalence: + // the reconstructed fixed-point statevector is byte-identical on every + // machine — the simulation analog of lossless, like world replay (it IS a + // fixed-point approximation of the ideal unitary evolution, but its + // reconstruction is exact and portable; the honest note lives in the + // extension's §Fidelity). +CAP_WORLD_SCENE | CAP_WORLD_REPLAY | CAP_WORLD_LIVE | CAP_WORLD_MODEL | CAP_MESH_GLB | CAP_AUDIO_SCENE | CAP_AUDIO_SCORE | CAP_HAPTIC_SIGNAL | CAP_FEED_STATE | CAP_AVATAR_POSE | CAP_FILM_LINEAR | CAP_FILM_REEL | CAP_GENERATE_SPECTRAL | CAP_QUANTUM_CIRCUIT | CAP_QUANTUM_CIRCUIT2 => Lossless, // Splat delivery is byte-exact and the temporal reconstruction is // frame-state-exact; the splat representation itself approximates a // scene, so the honest tier matches the captured medium, not the @@ -937,7 +951,7 @@ pub fn capability_license(cap: &str) -> Option { | CAP_WORLD_SCENE | CAP_WORLD_REPLAY | CAP_WORLD_LIVE | CAP_WORLD_MODEL | CAP_AUDIO_SCENE | CAP_AUDIO_SCORE | CAP_HAPTIC_SIGNAL | CAP_FEED_STATE | CAP_AVATAR_POSE | CAP_SPLAT_4D | CAP_FILM_LINEAR | CAP_FILM_REEL - | CAP_QUANTUM_CIRCUIT | CAP_TERNARY_TT | CAP_PRIOR_SET + | CAP_QUANTUM_CIRCUIT | CAP_QUANTUM_CIRCUIT2 | CAP_TERNARY_TT | CAP_PRIOR_SET | CAP_IMAGE_INT_REFINE | CAP_VIDEO_INT_REFINE | CAP_AUDIO_INT_REFINE | CAP_SPLAT_INT_REFINE // Open asset formats: glTF (Khronos), SPZ (open source). | CAP_MESH_GLB | CAP_SPLAT_SPZ @@ -1277,7 +1291,7 @@ pub fn capability_replicate_class(cap: &str) -> Option { | CAP_WORLD_SCENE | CAP_WORLD_REPLAY | CAP_WORLD_LIVE | CAP_WORLD_MODEL | CAP_AUDIO_SCENE | CAP_AUDIO_SCORE | CAP_HAPTIC_SIGNAL | CAP_FEED_STATE | CAP_AVATAR_POSE | CAP_SPLAT_4D | CAP_FILM_LINEAR | CAP_FILM_REEL - | CAP_QUANTUM_CIRCUIT | CAP_TERNARY_TT + | CAP_QUANTUM_CIRCUIT | CAP_QUANTUM_CIRCUIT2 | CAP_TERNARY_TT // A parameter-set listing: SPEC §3.1 fixes its bytes exactly, and they are // its own output. | CAP_PRIOR_SET @@ -1749,6 +1763,7 @@ mod tests { // Quantum-circuit simulation is statevector-equivalence — the simulation // analog of lossless (byte-exact reconstruction of the fixed-point state). assert_eq!(capability_tier(CAP_QUANTUM_CIRCUIT), Some(FidelityTier::Lossless)); + assert_eq!(capability_tier(CAP_QUANTUM_CIRCUIT2), Some(FidelityTier::Lossless)); // The semantic path is perceptual, not byte-exact. assert_eq!(capability_tier(CAP_SEMANTIC_CLIP), Some(FidelityTier::Perceptual)); assert_eq!(capability_tier(CAP_SEMANTIC_PERCO2), Some(FidelityTier::Perceptual)); @@ -1768,6 +1783,7 @@ mod tests { assert!(is_royalty_free(CAP_VIDEO_AV1)); assert!(is_royalty_free(CAP_FILM_REEL)); assert!(is_royalty_free(CAP_QUANTUM_CIRCUIT)); // WAI-native, integer floor + assert!(is_royalty_free(CAP_QUANTUM_CIRCUIT2)); // Verified June 2026 against primary sources: assert_eq!(capability_license(CAP_AUDIO_IAMF), Some(RoyaltyFree)); // AOMedia PL 1.0 assert_eq!(capability_license(CAP_IMAGE_JP2), Some(RoyaltyFree)); // ITU-T 2.1 Part 1 @@ -2092,7 +2108,8 @@ mod tests { // are standard-defined (reconstruction specified exactly, not by a model // WAI runs). for &c in &[CAP_IMAGE_PNG, CAP_AUDIO_FLAC, CAP_TEXT_ZSTD, - CAP_WORLD_REPLAY, CAP_AUDIO_SCORE, CAP_FILM_REEL, CAP_QUANTUM_CIRCUIT] { + CAP_WORLD_REPLAY, CAP_AUDIO_SCORE, CAP_FILM_REEL, CAP_QUANTUM_CIRCUIT, + CAP_QUANTUM_CIRCUIT2] { assert_eq!(capability_replicate_class(c), Some(StandardDefined), "{c}"); assert!(!is_neural_integer_exact(c), "{c} is not a neural decode"); } @@ -2330,6 +2347,7 @@ mod tests { (CAP_FILM_LINEAR, "extensions/linear-film.md: film-equivalence"), (CAP_FILM_REEL, "extensions/linear-film.md: reel-equivalence"), (CAP_QUANTUM_CIRCUIT, "extensions/quantum-sim.md: statevector-equivalence"), + (CAP_QUANTUM_CIRCUIT2, "extensions/quantum-sim.md: statevector-equivalence"), (CAP_TERNARY_TT, "extensions/ternary-tt.md: reconstruction-equivalence"), (CAP_PRIOR_SET, "SPEC.md §3.1: a listing's bytes are fixed exactly and are their own output"), (CAP_IMAGE_INT_REFINE, "extensions/staged-delivery.md §6.1: integer predictor, residual and clamp; staged-conformance"), diff --git a/wai/wai-rs/src/codecs/registry.rs b/wai/wai-rs/src/codecs/registry.rs index 781ae1b06..1bbe37d07 100644 --- a/wai/wai-rs/src/codecs/registry.rs +++ b/wai/wai-rs/src/codecs/registry.rs @@ -282,6 +282,7 @@ const ROWS: &[(&str, &str, Option<&str>, &str)] = &[ (CAP_FILM_REEL, S_INTERACTIVE, None, "2026-06-17"), (CAP_TERNARY_TT, S_INTERACTIVE, None, "2026-07-30"), (CAP_QUANTUM_CIRCUIT, S_INTERACTIVE, None, "2026-07-25"), + (CAP_QUANTUM_CIRCUIT2, S_INTERACTIVE, None, "2026-10-09"), (CAP_ASSET_PROVENANCE, S_INTERACTIVE, None, "2026-06-13"), (CAP_QUANTUM_CALIBRATION, S_INTERACTIVE, None, "2026-08-01"), (CAP_QUANTUM_JOB, S_INTERACTIVE, None, "2026-08-01"), @@ -685,7 +686,8 @@ pub fn capability_media(cap: &str) -> Option { | CAP_FILM_LINEAR | CAP_FILM_REEL => Video, CAP_WORLD_SCENE | CAP_WORLD_REPLAY | CAP_WORLD_LIVE | CAP_WORLD_MODEL | CAP_FEED_STATE | CAP_AVATAR_POSE => Interactive, - CAP_PRIOR_NNC | CAP_PRIOR_SET | CAP_TENSOR_NNC | CAP_QUANTUM_CIRCUIT | CAP_TERNARY_TT => Binary, + CAP_PRIOR_NNC | CAP_PRIOR_SET | CAP_TENSOR_NNC | CAP_QUANTUM_CIRCUIT | CAP_QUANTUM_CIRCUIT2 + | CAP_TERNARY_TT => Binary, c if c.starts_with("wai.image.") => Image, c if c.starts_with("wai.audio.") => Audio, c if c.starts_with("wai.video.") => Video, diff --git a/wai/wai-rs/src/lib.rs b/wai/wai-rs/src/lib.rs index 58d42b4ba..190b51941 100644 --- a/wai/wai-rs/src/lib.rs +++ b/wai/wai-rs/src/lib.rs @@ -426,12 +426,13 @@ pub mod world_model; pub mod film; // Deterministic quantum-circuit SIMULATION as transportable content -// (extensions/quantum-sim, `wai.quantum.circuit`): the wire carries a compact -// gate op-log (WQC container), the sink classically simulates it in a fixed-point -// complex floor (Amp = i64 re/im at 2^FRAC), and the reconstructed statevector is -// byte-identical on every machine (statevector-equivalence). Clifford+T + dyadic -// phase, so the controlled-phase ladder of the QFT ("quantum FFT") reconstructs -// byte-exact. Pure i64, no float/ONNX; compiles to wasm32. Quantum HARDWARE is +// (extensions/quantum-sim, `wai.quantum.circuit2` and the frozen +// `wai.quantum.circuit`): the wire carries a compact gate op-log (WQC container), +// the sink classically simulates it in a fixed-point complex floor (Amp = i64 +// re/im at 2^FRAC), and the reconstructed statevector is byte-identical on every +// machine (statevector-equivalence). Clifford+T + dyadic phase; circuit2's +// correctly rounded phase table gives the controlled-phase ladder of the QFT +// ("quantum FFT"). Pure i64, no float/ONNX; compiles to wasm32. Quantum HARDWARE is // deliberately absent — NISQ noise breaks the determinism the standard rests on. #[cfg(feature = "quantum")] pub use wai_quantum::quantum; diff --git a/wai/wai-rs/src/quantum_envelope.rs b/wai/wai-rs/src/quantum_envelope.rs index 5b09aae60..03b0c4a39 100644 --- a/wai/wai-rs/src/quantum_envelope.rs +++ b/wai/wai-rs/src/quantum_envelope.rs @@ -1,35 +1,115 @@ -//! Wrapping a quantum circuit in a WAI envelope. +//! Wrapping a quantum circuit in a WAI envelope, and opening one. //! //! The computation itself lives in the dependency-light [`wai_quantum`] crate; //! this is the container half — the part that belongs to WAI, not to quantum. +//! +//! The envelope's capability is the circuit's: `wai.quantum.circuit2` for a +//! [`Gateset::V2`] circuit, whose payload is a `WQC2` container, and +//! `wai.quantum.circuit` for a [`Gateset::V1`] one, whose payload is `WQC1`. -use wai_quantum::quantum::{to_wqc, Circuit, Measure, CAPABILITY}; +use wai_quantum::quantum::{from_wqc, to_wqc, Circuit, Gateset, Measure, QuantumError}; -/// Wrap a circuit in a v1.0 WAI envelope under `wai.quantum.circuit`. The payload -/// is the `WQC1` container; `intent=replicate` is sound because the reconstruction -/// is byte-exact (statevector-equivalence). -pub fn wrap_envelope(c: &Circuit, measure: Option) -> crate::container::Wai { +/// Wrap a circuit in a v1.0 WAI envelope under its gateset's capability +/// ([`Gateset::capability`]). The payload is the circuit's `WQC` container; +/// `intent=replicate` is sound because the reconstruction is byte-exact +/// (statevector-equivalence). +/// +/// Fails as [`to_wqc`] does, for a circuit its gateset does not allow, and with +/// [`QuantumError::TooManyQubits`] for a circuit of 64 qubits or more, whose +/// dimension the manifest's `target.dim` cannot hold. +pub fn wrap_envelope(c: &Circuit, measure: Option) -> Result { use crate::container::{Conditioning, Manifest, ModelRequirement}; - let payload = to_wqc(c, measure); - let manifest = Manifest::new("quantum", "replicate", ModelRequirement::new(CAPABILITY), Conditioning::new("wqc")) - .with_target(serde_json::json!({ "n_qubits": c.n_qubits, "dim": 1u64 << c.n_qubits })); - crate::container::Wai::new(manifest, payload) + let payload = to_wqc(c, measure)?; + let dim = 1u64.checked_shl(u32::from(c.n_qubits)).ok_or(QuantumError::TooManyQubits(c.n_qubits))?; + let manifest = Manifest::new("quantum", "replicate", ModelRequirement::new(c.gateset.capability()), Conditioning::new("wqc")) + .with_target(serde_json::json!({ "n_qubits": c.n_qubits, "dim": dim })); + Ok(crate::container::Wai::new(manifest, payload)) } +/// Open a quantum envelope: decode its `WQC` payload, then require the +/// envelope's capability to be the one the container's magic names. +/// +/// A payload that does not decode fails as [`from_wqc`] does. A decoded circuit +/// whose gateset is not the one the capability names — a `WQC2` payload under +/// `wai.quantum.circuit`, a `WQC1` payload under `wai.quantum.circuit2`, or any +/// payload under another capability — is [`QuantumError::Contract`]. +pub fn open_envelope(w: &crate::container::Wai) -> Result<(Circuit, Option), QuantumError> { + let (c, m) = from_wqc(&w.payload)?; + let cap = w.capability(); + if Gateset::from_capability(cap) != Some(c.gateset) { + // The capability is the file's; quote at most 64 characters of it. + let quoted: String = cap.chars().take(64).collect(); + return Err(QuantumError::Contract(format!( + "the envelope's capability {quoted:?} does not name the payload's container, {} ({})", + String::from_utf8_lossy(&c.gateset.magic()), + c.gateset.capability() + ))); + } + Ok((c, m)) +} #[cfg(test)] mod tests { use super::*; - use wai_quantum::quantum::from_wqc; - #[test] - fn envelope_wraps_under_capability() { - let mut c = Circuit::new(2); + use crate::container::Wai; + + fn bell(gs: Gateset) -> Circuit { + let mut c = Circuit::with_gateset(2, gs); c.h(0).cx(0, 1); - let wai = wrap_envelope(&c, None); - assert_eq!(wai.capability(), CAPABILITY); - let bytes = wai.to_bytes().unwrap(); - let back = crate::container::Wai::from_bytes(&bytes).unwrap(); - let (c2, _) = from_wqc(&back.payload).unwrap(); - assert_eq!(c, c2); + c + } + + /// The envelope's bytes, read back. + fn reread(w: &Wai) -> Wai { + Wai::from_bytes(&w.to_bytes().unwrap()).unwrap() + } + + /// The capability follows the circuit's gateset, and the circuit — gateset + /// included — and its measurement survive the envelope. + #[test] + fn envelope_wraps_under_the_gatesets_capability() { + for (gs, cap, magic) in [ + (Gateset::V2, crate::codecs::CAP_QUANTUM_CIRCUIT2, b"WQC2"), + (Gateset::V1, crate::codecs::CAP_QUANTUM_CIRCUIT, b"WQC1"), + ] { + // The registry's strings are copies of the crate's. + assert_eq!(gs.capability(), cap); + let c = bell(gs); + let m = Some(Measure { seed: 7, shots: 1024 }); + let wai = wrap_envelope(&c, m).unwrap(); + assert_eq!(wai.capability(), cap); + assert_eq!(&wai.payload[0..4], magic); + assert_eq!(wai.manifest.target, serde_json::json!({ "n_qubits": 2, "dim": 4 })); + let back = reread(&wai); + assert_eq!(open_envelope(&back).unwrap(), (c.clone(), m)); + let (c2, _) = from_wqc(&back.payload).unwrap(); + assert_eq!(c2, c); + } + assert_eq!(wrap_envelope(&Circuit::new(1), None).unwrap().capability(), "wai.quantum.circuit2"); + } + + /// An envelope whose capability is not the one its payload's magic names is + /// refused, whichever way round, and so is a quantum payload under any + /// other capability. A payload that is not a container fails as the + /// container reader does, and a circuit its gateset forbids is never wrapped. + #[test] + fn a_capability_that_does_not_name_the_payload_is_refused() { + for (gs, other) in [(Gateset::V2, "wai.quantum.circuit"), (Gateset::V1, "wai.quantum.circuit2"), (Gateset::V2, "wai.image.png")] { + let mut wai = wrap_envelope(&bell(gs), None).unwrap(); + wai.manifest.model_requirement.capability = other.to_string(); + let e = open_envelope(&reread(&wai)).unwrap_err(); + assert_eq!(e.kind(), "contract", "{gs:?} under {other}: {e}"); + } + let mut wai = wrap_envelope(&bell(Gateset::V2), None).unwrap(); + wai.payload = b"not a circuit".to_vec(); + assert_eq!(open_envelope(&wai).unwrap_err().kind(), "malformed"); + // A repeated control: allowed in circuit/1, refused by circuit2. + let mut c = Circuit::with_gateset(3, Gateset::V2); + c.ccx(0, 0, 2); + assert_eq!(wrap_envelope(&c, None).err().expect("refused").kind(), "invalid"); + c.gateset = Gateset::V1; + assert_eq!(open_envelope(&wrap_envelope(&c, None).unwrap()).unwrap().0, c); + // A circuit whose dimension the manifest cannot hold. + assert_eq!(wrap_envelope(&Circuit::new(64), None).err(), Some(QuantumError::TooManyQubits(64))); } } -- 2.54.0 (Apple Git-157) From 3f275c7cfd0849114402389b278753af99c36631 Mon Sep 17 00:00:00 2001 From: Transaction Science Date: Fri, 9 Oct 2026 13:05:18 -0400 Subject: [PATCH 07/13] wai-quantum circuit2: the browser sink, the IDE and the serve component follow the gateset QuantumSink reads WQC1 and WQC2 containers. It reports the capability the magic names and the circuit hash. A refused container throws a message that names its error class, the word reject.json lists. QuantumIde builds in circuit2 by default. set_gateset(1) moves the current circuit and every later build to circuit/1, and gateset() and capability() report it. The debugger's prefix circuit keeps the IDE's gateset. add() stores no parameter on a gate other than P, so an editor's leftover parameter no longer makes a circuit2 circuit invalid. circuit_hash_hex() is empty for an invalid circuit. A circuit/1 circuit with P(k>=8) gets the command-line tool's note on the browser console, once per circuit state, never in a returned value. wai-quantum-serve takes gateset=v1 or v2 (default v2) on /run, /qasm and /receipt. /run names the capability in its JSON, and /run and /receipt name it in a wai-capability header. The /receipt body stays the receipt JSON alone, so it is still the bytes a native seal gives. An unknown gateset or a circuit with no circuit hash is a 400. Native tests cover both corpora, the reject corpus, the IDE's gateset switching, the add() parameter rule, the empty hash, and the serve endpoints against the corpus GHZ3 hashes. The wai-web native test count in CI moves from 24 to 31, and that workflow now triggers on changes to the quantum corpora it reads. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/wai-energy-receipts.yml | 7 +- .github/workflows/wai-web-wasm.yml | 13 +- wai/wai-quantum-serve/src/lib.rs | 243 ++++++++++++++++--- wai/wai-web/src/lib.rs | 5 +- wai/wai-web/src/quantum.rs | 121 +++++++++- wai/wai-web/src/quantum_toolchain.rs | 275 ++++++++++++++++++++-- 6 files changed, 592 insertions(+), 72 deletions(-) diff --git a/.github/workflows/wai-energy-receipts.yml b/.github/workflows/wai-energy-receipts.yml index ed4e474ab..86c3751a0 100644 --- a/.github/workflows/wai-energy-receipts.yml +++ b/.github/workflows/wai-energy-receipts.yml @@ -29,7 +29,10 @@ name: WAI energy receipts # 3. wai-quantum-serve: `cargo test --locked --lib`, native: the verdict # logic behind POST /verify, for a receipt in either form, and that an # unmetered receipt's 0 is rendered as the unmetered marker (null), never -# as zero joules. +# as zero joules; and that /run and /receipt build in the gateset the +# request names (circuit2 by default), name its capability, give the +# corpus's GHZ3 hashes in each, and answer 400 for an unknown gateset or +# a circuit with no circuit hash. # # It does NOT: # - check that a newly sealed receipt in a family with an optional label @@ -170,7 +173,7 @@ jobs: # signer's level beside validity (jwp-receipts §7.1, §7.4). The verdict # logic is a plain function, tested natively; the wasm32-wasip2 build of # the component is not part of this workflow. - - name: wai-quantum-serve — POST /verify accepts either form and reports the signer's level + - name: wai-quantum-serve — POST /verify verdicts, and /run and /receipt in the requested gateset working-directory: wai/wai-quantum-serve shell: bash run: cargo test --locked --lib diff --git a/.github/workflows/wai-web-wasm.yml b/.github/workflows/wai-web-wasm.yml index 3f7c8ee7e..d9a3ae81d 100644 --- a/.github/workflows/wai-web-wasm.yml +++ b/.github/workflows/wai-web-wasm.yml @@ -55,6 +55,8 @@ on: - 'wai/wai-web/**' - 'wai/wai-rs/**' - 'wai/wai-quantum/**' + # quantum::tests and quantum_toolchain::tests read the quantum corpora. + - 'wai/quantum-conformance/**' - 'wai/neural-conformance/**' - 'wai/staged-conformance/**' - 'wai/staged-measure-conformance/**' @@ -67,6 +69,8 @@ on: - 'wai/wai-web/**' - 'wai/wai-rs/**' - 'wai/wai-quantum/**' + # quantum::tests and quantum_toolchain::tests read the quantum corpora. + - 'wai/quantum-conformance/**' - 'wai/neural-conformance/**' - 'wai/staged-conformance/**' - 'wai/staged-measure-conformance/**' @@ -131,7 +135,12 @@ jobs: # staged-delivery binding's pure half replays the staged corpus and reads a layer's # header and cost (stage::tests); the measurement binding's pure half evaluates the # staged-measurement corpus to its goldens and seals a page-recorded session equal - # to the native tool's (measure::tests). The count check turns a test that + # to the native tool's (measure::tests). The quantum sink opens every file of + # both quantum corpora to their manifests' hashes and capability and refuses + # every reject file with its listed class (quantum::tests), and the quantum IDE + # builds its catalog circuits in its gateset to the same manifests, stores no + # parameter on a gate other than P, and shows no hash for an invalid circuit + # (quantum_toolchain::tests). The count check turns a test that # stopped being compiled into a failure instead of a silent "0 passed". - name: wai-web native unit tests shell: bash @@ -139,7 +148,7 @@ jobs: run: | set -euo pipefail cargo test --locked --lib 2>&1 | tee "$RUNNER_TEMP/wai-web-native.log" - grep -q "test result: ok. 24 passed; 0 failed" "$RUNNER_TEMP/wai-web-native.log" + grep -q "test result: ok. 31 passed; 0 failed" "$RUNNER_TEMP/wai-web-native.log" - name: wasm32 int_synth decode equals the frozen native corpus shell: bash working-directory: wai/wai-web diff --git a/wai/wai-quantum-serve/src/lib.rs b/wai/wai-quantum-serve/src/lib.rs index e9b463b28..b32824155 100644 --- a/wai/wai-quantum-serve/src/lib.rs +++ b/wai/wai-quantum-serve/src/lib.rs @@ -18,6 +18,15 @@ //! POST /verify body = receipt JSON → verdict //! ``` //! +//! `/run` (both forms), `/qasm` and `/receipt` take `gateset=v2` (the default, +//! `wai.quantum.circuit2`) or `gateset=v1` (the frozen `wai.quantum.circuit`), +//! and build the circuit in it; any other value is a 400. The circuit hash, the +//! statevector and the receipt follow the gateset. `/run` names the capability +//! in its JSON as `"capability"`, and `/run` and `/receipt` name it in a +//! `wai-capability` response header. The `/receipt` body stays the receipt JSON +//! alone, so it is the same bytes a native run seals. A circuit whose hash +//! cannot be computed is a 400. +//! //! `POST /verify` accepts a receipt in either form: as sealed, or with its //! joule figure's acquisition class inside the signature //! (`quantum_energy::Labelled`), and restates the class in its verdict. It @@ -27,7 +36,7 @@ //! `unconfirmed`, with the reason it stopped at. use ed25519_dalek::SigningKey; -use wai::quantum::{Circuit, Measure}; +use wai::quantum::{Circuit, Gateset, Measure}; use wai::quantum_energy::MaybeLabelled; use wai::quantum_receipt::QuantumReceipt; use wai::quantum_toolchain as qt; @@ -116,9 +125,15 @@ fn esc(s: &str) -> String { o } -fn send(out: ResponseOutparam, status: u16, ctype: &str, body: &[u8]) { +/// The response header that names the capability a circuit was built in. +const CAPABILITY_HEADER: &str = "wai-capability"; + +fn send(out: ResponseOutparam, status: u16, ctype: &str, capability: Option<&str>, body: &[u8]) { let hdrs = Fields::new(); let _ = hdrs.append("content-type", &ctype.as_bytes().to_vec()); + if let Some(c) = capability { + let _ = hdrs.append(CAPABILITY_HEADER, &c.as_bytes().to_vec()); + } let resp = OutgoingResponse::new(hdrs); let _ = resp.set_status_code(status); let Ok(rbody) = resp.body() else { return }; @@ -136,11 +151,34 @@ fn send(out: ResponseOutparam, status: u16, ctype: &str, body: &[u8]) { } fn json(out: ResponseOutparam, status: u16, body: String) { - send(out, status, "application/json", body.as_bytes()); + send(out, status, "application/json", None, body.as_bytes()); +} + +/// The JSON body of an error response. +fn err_body(msg: &str) -> String { + format!("{{\"error\":\"{}\"}}", esc(msg)) } fn err(out: ResponseOutparam, status: u16, msg: &str) { - json(out, status, format!("{{\"error\":\"{}\"}}", esc(msg))); + json(out, status, err_body(msg)); +} + +/// An answer computed apart from the WASI types, so the native tests can check +/// it: the status, the JSON body and, for a circuit that was built, the +/// capability it was built in. +struct Reply { + status: u16, + body: String, + capability: Option<&'static str>, +} + +impl Reply { + fn error(status: u16, msg: &str) -> Reply { + Reply { status, body: err_body(msg), capability: None } + } + fn send(self, out: ResponseOutparam) { + send(out, self.status, "application/json", self.capability, self.body.as_bytes()); + } } fn read_body(body: IncomingBody) -> Vec { @@ -211,16 +249,36 @@ fn qnum(pq: &str, key: &str, dflt: T) -> T { query(pq, key).and_then(|v| v.parse().ok()).unwrap_or(dflt) } +/// The gateset `?gateset=` names: `v2` (`wai.quantum.circuit2`, the default) +/// or `v1` (the frozen `wai.quantum.circuit`). Any other value is an error. +fn gateset(pq: &str) -> Result { + match query(pq, "gateset").as_deref() { + None | Some("v2") => Ok(Gateset::V2), + Some("v1") => Ok(Gateset::V1), + Some(other) => Err(format!("unknown gateset '{other}' (v1, v2)")), + } +} + +/// Move a built circuit into the gateset the request names. The builders make +/// circuit2 circuits with distinct controls and no parameter on a gate other +/// than `P`, which are valid circuit/1 circuits too. +fn in_gateset(mut c: Circuit, pq: &str) -> Result { + c.gateset = gateset(pq)?; + Ok(c) +} + fn build(pq: &str) -> Result { let algo = query(pq, "algo").ok_or("missing ?algo= (see /algorithms)")?; let n: u8 = qnum(pq, "n", 3); let p: u32 = qnum(pq, "p", 0); - qt::build_algorithm(&algo, n, p).ok_or(format!("unknown algorithm '{algo}'")) + let c = qt::build_algorithm(&algo, n, p).ok_or(format!("unknown algorithm '{algo}'"))?; + in_gateset(c, pq) } /// The simulation result as JSON — the same fields the CLI prints. fn run_json(c: &Circuit, top: usize) -> Result { c.validate().map_err(|e| format!("invalid circuit: {e:?}"))?; + let id = qt::circuit_id(c).map_err(|e| format!("circuit id: {e}"))?; let sv = c.simulate().map_err(|e| format!("simulate: {e:?}"))?; let (backend, why) = qt::recommend_backend(c); let n = c.n_qubits as usize; @@ -229,9 +287,10 @@ fn run_json(c: &Circuit, top: usize) -> Result { .map(|(k, p)| format!("{{\"state\":\"{k:0n$b}\",\"p\":{p}}}", n = n)) .collect(); Ok(format!( - "{{\"circuit_id\":\"{}\",\"n_qubits\":{},\"n_gates\":{},\"backend\":\"{backend:?}\",\ + "{{\"circuit_id\":\"{}\",\"capability\":\"{}\",\"n_qubits\":{},\"n_gates\":{},\"backend\":\"{backend:?}\",\ \"backend_reason\":\"{}\",\"work_amp_updates\":{},\"statevector_hash\":\"{}\",\"top\":[{}]}}", - hx(&qt::circuit_id(c)), + hx(&id), + c.gateset.capability(), c.n_qubits, c.ops.len(), esc(why), @@ -241,6 +300,38 @@ fn run_json(c: &Circuit, top: usize) -> Result { )) } +/// `/run`: 200 and the run JSON for the circuit the request built, or 400. +fn run_reply(built: Result, top: usize) -> Reply { + match built.and_then(|c| run_json(&c, top).map(|body| (c.gateset, body))) { + Ok((gs, body)) => Reply { status: 200, body, capability: Some(gs.capability()) }, + Err(e) => Reply::error(400, &e), + } +} + +/// `/receipt`: 200 and the sealed receipt JSON, or 400 for a request that +/// names no circuit or an unknown gateset, or a circuit the seal refuses. +fn receipt_reply(pq: &str) -> Reply { + let c = match build(pq) { + Ok(c) => c, + Err(e) => return Reply::error(400, &e), + }; + let shots: u64 = qnum(pq, "shots", 0); + let seed: u64 = qnum(pq, "seed", 0); + seal_reply(&c, (shots > 0).then_some(Measure { seed, shots })) +} + +/// Seal `c`: 200 and the receipt JSON, or 400 when the seal refuses the +/// circuit. Every refusal is the circuit's own: one its gateset does not allow, +/// so it has no circuit hash, or one too large to simulate. +fn seal_reply(c: &Circuit, measure: Option) -> Reply { + // Deterministic demo key so the sealed bytes match a native run. + let key = SigningKey::from_bytes(&[42u8; 32]); + match QuantumReceipt::seal(&key, "did:key:wai-quantum-cli", c, measure, 0, None) { + Ok(r) => Reply { status: 200, body: r.to_json(), capability: Some(c.gateset.capability()) }, + Err(e) => Reply::error(400, &format!("seal: {e}")), + } +} + impl wasi::exports::http::incoming_handler::Guest for Component { fn handle(req: IncomingRequest, out: ResponseOutparam) { let method = req.method(); @@ -254,6 +345,7 @@ impl wasi::exports::http::incoming_handler::Guest for Component { 200, "{\"service\":\"wai-quantum\",\"kind\":\"wasi:http/proxy component\",\ \"deterministic\":true,\"note\":\"statevector hashes and receipt bytes are identical to a native run\",\ + \"gateset\":{\"default\":\"v2\",\"v2\":\"wai.quantum.circuit2\",\"v1\":\"wai.quantum.circuit\"},\ \"endpoints\":[\"/health\",\"/algorithms\",\"/run?algo=ghz&n=5\",\"/qasm?algo=bell\",\"/receipt?algo=ghz&n=3\",\"POST /run (OpenQASM)\",\"POST /verify (receipt JSON)\"]}" .to_string(), ), @@ -275,13 +367,7 @@ impl wasi::exports::http::incoming_handler::Guest for Component { json(out, 200, format!("{{\"algorithms\":[{}]}}", items.join(","))); } - (false, "/run") => match build(&pq) { - Err(e) => err(out, 400, &e), - Ok(c) => match run_json(&c, qnum(&pq, "top", 8usize)) { - Ok(body) => json(out, 200, body), - Err(e) => err(out, 400, &e), - }, - }, + (false, "/run") => run_reply(build(&pq), qnum(&pq, "top", 8usize)).send(out), (true, "/run") => { let top = qnum(&pq, "top", 8usize); @@ -289,35 +375,16 @@ impl wasi::exports::http::incoming_handler::Guest for Component { return err(out, 400, "no body"); }; let src = String::from_utf8_lossy(&read_body(body)).into_owned(); - match qt::from_qasm(&src) { - Err(e) => err(out, 400, &format!("QASM parse: {e}")), - Ok(c) => match run_json(&c, top) { - Ok(b) => json(out, 200, b), - Err(e) => err(out, 400, &e), - }, - } + let built = qt::from_qasm(&src).map_err(|e| format!("QASM parse: {e}")); + run_reply(built.and_then(|c| in_gateset(c, &pq)), top).send(out) } (false, "/qasm") => match build(&pq) { Err(e) => err(out, 400, &e), - Ok(c) => send(out, 200, "text/plain; charset=utf-8", qt::to_qasm(&c).as_bytes()), + Ok(c) => send(out, 200, "text/plain; charset=utf-8", None, qt::to_qasm(&c).as_bytes()), }, - (false, "/receipt") => match build(&pq) { - Err(e) => err(out, 400, &e), - Ok(c) => { - let shots: u64 = qnum(&pq, "shots", 0); - let seed: u64 = qnum(&pq, "seed", 0); - let measure = (shots > 0).then_some(Measure { seed, shots }); - // Deterministic demo key so the sealed bytes match a native run. - let key = SigningKey::from_bytes(&[42u8; 32]); - match QuantumReceipt::seal(&key, "did:key:wai-quantum-cli", &c, measure, 0, None) - { - Ok(r) => json(out, 200, r.to_json()), - Err(e) => err(out, 500, &format!("seal: {e:?}")), - } - } - }, + (false, "/receipt") => receipt_reply(&pq).send(out), (true, "/verify") => { let Ok(body) = req.consume() else { @@ -336,6 +403,7 @@ impl wasi::exports::http::incoming_handler::Guest for Component { #[cfg(test)] mod tests { use super::*; + use wai::quantum::{circuit_hash, BaseGate, Gate}; use wai::quantum_energy::{CounterFiltering, EnergyClass, Labelled, Uncertainty}; fn ghz3() -> Circuit { @@ -344,6 +412,107 @@ mod tests { c } + /// GHZ3's circuit and statevector hashes in each capability, as the + /// conformance corpora state them (`v2/expected_hashes.json` and + /// `expected_hashes.json`). + const GHZ3_V2: (&str, &str) = ( + "c5911b898a899c999ce01730bb825c84678569493ac513760e15dcbccd84227f", + "51850f28817200e9e03980b60f34879c7bd0147a07b0c1746528b801cae2e2f8", + ); + const GHZ3_V1: (&str, &str) = ( + "34e44f7d68037cbb3aee677f56b894dc48df6d408aa068a16d3b18076e5e7c5c", + "08c5e6fb50b91a00d28795506cc5d99ce1e9a3a8b4c41e4bfd05f5d829af9542", + ); + + #[test] + fn the_gateset_parameter_defaults_to_circuit2_and_names_no_other() { + assert_eq!(gateset("/run?algo=bell"), Ok(Gateset::V2)); + assert_eq!(gateset("/run?algo=bell&gateset=v2"), Ok(Gateset::V2)); + assert_eq!(gateset("/run?gateset=v1&algo=bell"), Ok(Gateset::V1)); + for bad in ["v3", "V1", "", "circuit2", "wai.quantum.circuit"] { + let e = gateset(&format!("/run?algo=bell&gateset={bad}")).unwrap_err(); + assert!(e.contains("unknown gateset"), "{bad}: {e}"); + } + // The OpenQASM body of `POST /run` takes the query's gateset too. + let c = qt::from_qasm("OPENQASM 3.0;\nqubit[3] q;\nh q[0];\ncx q[0], q[1];\ncx q[1], q[2];\n").unwrap(); + assert_eq!(in_gateset(c.clone(), "/run?gateset=v1").unwrap().gateset, Gateset::V1); + assert_eq!(in_gateset(c.clone(), "/run").unwrap().gateset, Gateset::V2); + assert!(in_gateset(c, "/run?gateset=v0").is_err()); + } + + /// `/run` builds in the gateset the request names and says which, in its + /// JSON and its header; its hashes are the corpus's for that capability. + #[test] + fn run_names_its_capability_and_reproduces_the_corpus() { + for (q, capability, (circuit, statevector)) in [ + ("/run?algo=ghz&n=3", "wai.quantum.circuit2", GHZ3_V2), + ("/run?algo=ghz&n=3&gateset=v2", "wai.quantum.circuit2", GHZ3_V2), + ("/run?algo=ghz&n=3&gateset=v1", "wai.quantum.circuit", GHZ3_V1), + ] { + let r = run_reply(build(q), 8); + assert_eq!((r.status, r.capability), (200, Some(capability)), "{q}: {}", r.body); + assert!( + r.body.starts_with(&format!("{{\"circuit_id\":\"{circuit}\",\"capability\":\"{capability}\",\"n_qubits\":3,")), + "{q}: {}", + r.body + ); + assert!(r.body.contains(&format!("\"statevector_hash\":\"{statevector}\"")), "{q}: {}", r.body); + } + let r = run_reply(build("/run?algo=ghz&n=3&gateset=v9"), 8); + assert_eq!((r.status, r.capability), (400, None)); + assert_eq!(r.body, "{\"error\":\"unknown gateset 'v9' (v1, v2)\"}"); + } + + /// A circuit with no circuit hash is a 400 from `/run` and from the seal, + /// never a 500 or a panic. A repeated control is such a circuit in + /// circuit2, and an ordinary one in circuit/1. + #[test] + fn a_circuit_with_no_hash_is_a_bad_request() { + let mut c = Circuit::new(3); + c.h(0); + c.ops.push(Gate { base: BaseGate::X, controls: vec![0, 0], target: 1, param: 0 }); + assert!(circuit_hash(&c).is_err()); + let r = run_reply(Ok(c.clone()), 8); + assert_eq!((r.status, r.capability), (400, None), "{}", r.body); + assert!(r.body.contains("invalid circuit"), "{}", r.body); + let r = seal_reply(&c, None); + assert_eq!((r.status, r.capability), (400, None), "{}", r.body); + assert!(r.body.starts_with("{\"error\":\"seal: invalid circuit: "), "{}", r.body); + + c.gateset = Gateset::V1; + assert!(circuit_hash(&c).is_ok()); + assert_eq!(run_reply(Ok(c.clone()), 8).status, 200); + assert_eq!(seal_reply(&c, None).status, 200); + } + + /// `/receipt` seals in the gateset the request names. Its body is the + /// receipt JSON alone, the bytes a native seal of the same circuit gives; + /// the capability rides in the header. + #[test] + fn receipt_seals_in_the_requested_gateset() { + for (q, gs, circuit) in [ + ("/receipt?algo=ghz&n=3&shots=64&seed=7", Gateset::V2, GHZ3_V2.0), + ("/receipt?algo=ghz&n=3&shots=64&seed=7&gateset=v1", Gateset::V1, GHZ3_V1.0), + ] { + let r = receipt_reply(q); + assert_eq!((r.status, r.capability), (200, Some(gs.capability())), "{q}: {}", r.body); + let mut c = ghz3(); + c.gateset = gs; + let key = SigningKey::from_bytes(&[42u8; 32]); + let native = QuantumReceipt::seal(&key, "did:key:wai-quantum-cli", &c, Some(Measure { seed: 7, shots: 64 }), 0, None).unwrap(); + assert_eq!(r.body, native.to_json(), "{q}"); + assert!(!r.body.contains("capability"), "{q}"); + let parsed = QuantumReceipt::from_json(&r.body).unwrap(); + assert!(parsed.verify()); + assert_eq!(hx(&parsed.circuit_hash), circuit, "{q}"); + assert!(parsed.verify_reconstruction(&c)); + } + let r = receipt_reply("/receipt?algo=ghz&n=3&gateset=2"); + assert_eq!((r.status, r.capability), (400, None)); + assert!(r.body.contains("unknown gateset"), "{}", r.body); + assert_eq!(receipt_reply("/receipt?algo=nope").status, 400); + } + #[test] fn verify_accepts_both_forms_and_restates_the_class() { let key = SigningKey::from_bytes(&[0x71u8; 32]); diff --git a/wai/wai-web/src/lib.rs b/wai/wai-web/src/lib.rs index 48711a466..af884ba2f 100644 --- a/wai/wai-web/src/lib.rs +++ b/wai/wai-web/src/lib.rs @@ -65,8 +65,9 @@ pub mod int_world; // sink-supplied rasterizer presents the cloud (splat-equivalence). pub mod int_splat; -// Deterministic quantum-circuit SIMULATION (wai.quantum.circuit) — the SAME -// `wai::quantum` engine native sinks run, compiled to wasm32. A WQC gate op-log +// Deterministic quantum-circuit SIMULATION (wai.quantum.circuit2, and the frozen +// wai.quantum.circuit) — the SAME `wai::quantum` engine native sinks run, compiled +// to wasm32. A WQC gate op-log, in the phase table its magic names, // is classically simulated in the fixed-point complex floor; the reconstructed // statevector hashes byte-identically to the native reference // (statevector-equivalence). Pure i64, no float, no ML runtime, no quantum diff --git a/wai/wai-web/src/quantum.rs b/wai/wai-web/src/quantum.rs index 30258c1b2..beef65ed6 100644 --- a/wai/wai-web/src/quantum.rs +++ b/wai/wai-web/src/quantum.rs @@ -1,4 +1,5 @@ -//! Deterministic quantum-circuit SIMULATION sink (`wai.quantum.circuit`). +//! Deterministic quantum-circuit SIMULATION sink (`wai.quantum.circuit2`, and +//! the frozen `wai.quantum.circuit`). //! //! The SAME `wai::quantum` engine native sinks run, compiled to wasm32 — no //! second implementation. A `WQC` gate op-log is parsed and classically @@ -9,8 +10,13 @@ //! equivalence). Pure i64, no float, no ML runtime, and no quantum hardware — //! the point is a quantum computation made a transportable, receiptable object, //! with no speedup claimed. +//! +//! The container's magic names its capability: a `WQC2` file is simulated with +//! the circuit2 phase table and a `WQC1` file with the frozen circuit/1 table, +//! and [`QuantumSink::capability`] says which one the file was. A file the +//! reference reader refuses is refused here with the same error class. -use wai::quantum::{from_wqc, StateVector}; +use wai::quantum::{circuit_hash, from_wqc, Gateset, QuantumError, StateVector}; use wasm_bindgen::prelude::*; fn hex(b: &[u8]) -> String { @@ -19,38 +25,73 @@ fn hex(b: &[u8]) -> String { #[wasm_bindgen] pub struct QuantumSink { + gateset: Gateset, n_qubits: u8, n_ops: u32, + circuit_hash: [u8; 32], sv: StateVector, seed: u64, shots: u64, has_measure: bool, } -#[wasm_bindgen] +/// The text of an error the sink throws: ` (): `, where +/// the class is [`QuantumError::kind`] (`malformed`, `invalid`, `contract` or +/// `too-many-qubits`), the word the reject corpus's `reject.json` lists. +fn error_text(stage: &str, e: &QuantumError) -> String { + format!("{stage} ({}): {e}", e.kind()) +} + impl QuantumSink { - /// Parse a `WQC` container and classically simulate it — pure i64, no float. - /// Throws on a malformed container or an over-large circuit. - #[wasm_bindgen(constructor)] - pub fn new(wqc: &[u8]) -> Result { - let (circuit, measure) = - from_wqc(wqc).map_err(|e| JsError::new(&format!("WQC parse: {e:?}")))?; - let sv = circuit - .simulate() - .map_err(|e| JsError::new(&format!("simulate: {e:?}")))?; + /// Parse and simulate, with the reference reader's own error. The + /// `#[wasm_bindgen]` constructor only converts that error for JS. + fn open(wqc: &[u8]) -> Result { + let (circuit, measure) = from_wqc(wqc).map_err(|e| ("WQC parse", e))?; + // `from_wqc` validated the circuit, so its hash cannot fail. + let circuit_hash = circuit_hash(&circuit).map_err(|e| ("circuit hash", e))?; + let sv = circuit.simulate().map_err(|e| ("simulate", e))?; let (seed, shots, has_measure) = match measure { Some(m) => (m.seed, m.shots, true), None => (0, 0, false), }; Ok(QuantumSink { + gateset: circuit.gateset, n_qubits: circuit.n_qubits, + // A valid circuit has at most `u32::MAX` ops. n_ops: circuit.ops.len() as u32, + circuit_hash, sv, seed, shots, has_measure, }) } +} + +#[wasm_bindgen] +impl QuantumSink { + /// Parse a `WQC1` or `WQC2` container and classically simulate it — pure + /// i64, no float. Throws on a container the reference reader refuses or an + /// over-large circuit; the message is ` (): `, with + /// the class `reject.json` names (`malformed`, `invalid`, `contract`, + /// `too-many-qubits`). + #[wasm_bindgen(constructor)] + pub fn new(wqc: &[u8]) -> Result { + QuantumSink::open(wqc).map_err(|(stage, e)| JsError::new(&error_text(stage, &e))) + } + + /// The capability the container's magic names: `wai.quantum.circuit2` + /// (`WQC2`) or `wai.quantum.circuit` (`WQC1`). + pub fn capability(&self) -> String { + self.gateset.capability().to_string() + } + + /// The circuit hash: BLAKE3 over the capability's canonical contract and the + /// op-log, so it binds the capability and its phase table. It is the value + /// a corpus manifest and a run receipt carry. + pub fn circuit_hash_hex(&self) -> String { + hex(&self.circuit_hash) + } pub fn n_qubits(&self) -> u8 { self.n_qubits @@ -110,3 +151,59 @@ impl QuantumSink { .collect() } } + +#[cfg(test)] +mod tests { + use super::QuantumSink; + use std::path::{Path, PathBuf}; + + fn corpus() -> PathBuf { + Path::new(env!("CARGO_MANIFEST_DIR")).join("../quantum-conformance") + } + + fn manifest(dir: &Path) -> serde_json::Map { + let text = std::fs::read_to_string(dir.join("expected_hashes.json")).unwrap(); + serde_json::from_str::(&text).unwrap().as_object().unwrap().clone() + } + + /// Every file of both corpora opens in the sink with the manifest's + /// capability, circuit hash, statevector hash and, where it pins a + /// measurement, histogram hash. A circuit/1 manifest entry names no + /// capability. + #[test] + fn the_sink_reproduces_both_corpora() { + for (dir, default_capability) in [(corpus(), "wai.quantum.circuit"), (corpus().join("v2"), "")] { + let entries = manifest(&dir); + assert!(!entries.is_empty(), "{}", dir.display()); + for (name, e) in &entries { + let bytes = std::fs::read(dir.join("vectors").join(name)).unwrap(); + let Ok(sink) = QuantumSink::open(&bytes) else { panic!("{name} is refused") }; + let capability = e.get("capability").and_then(|c| c.as_str()).unwrap_or(default_capability); + assert_eq!(sink.capability(), capability, "{name}"); + assert_eq!(sink.n_qubits() as u64, e["n_qubits"].as_u64().unwrap(), "{name}"); + assert_eq!(sink.n_ops() as u64, e["n_ops"].as_u64().unwrap(), "{name}"); + assert_eq!(sink.circuit_hash_hex(), e["circuit_hash"].as_str().unwrap(), "{name}"); + assert_eq!(sink.statevector_hash_hex(), e["statevector_hash"].as_str().unwrap(), "{name}"); + let histogram = e.get("measure").map(|m| m["histogram_hash"].as_str().unwrap().to_string()); + assert_eq!(sink.histogram_hash_hex(), histogram, "{name}"); + } + } + } + + /// Every file of the reject corpus is refused with the class `reject.json` + /// lists, and the thrown text names that class in parentheses. + #[test] + fn the_sink_refuses_the_reject_corpus_by_class() { + let dir = corpus().join("reject"); + let text = std::fs::read_to_string(dir.join("reject.json")).unwrap(); + let cases: serde_json::Map = serde_json::from_str(&text).unwrap(); + assert!(!cases.is_empty()); + for (name, kind) in &cases { + let bytes = std::fs::read(dir.join(name)).unwrap(); + let Err((stage, e)) = QuantumSink::open(&bytes) else { panic!("{name} is accepted") }; + assert_eq!(e.kind(), kind.as_str().unwrap(), "{name}: {e}"); + let shown = super::error_text(stage, &e); + assert!(shown.starts_with(&format!("WQC parse ({}): ", e.kind())), "{name}: {shown}"); + } + } +} diff --git a/wai/wai-web/src/quantum_toolchain.rs b/wai/wai-web/src/quantum_toolchain.rs index c39dacb35..8756a19b5 100644 --- a/wai/wai-web/src/quantum_toolchain.rs +++ b/wai/wai-web/src/quantum_toolchain.rs @@ -3,9 +3,14 @@ //! native sink runs, compiled to wasm32 — a full quantum development environment //! that runs entirely in the page, and whose every run is reproducible to the bit //! and sealed into a signed receipt. +//! +//! The IDE builds its circuits in one gateset at a time: `wai.quantum.circuit2` +//! by default, or the frozen `wai.quantum.circuit` after `set_gateset(1)`, to +//! reproduce circuit/1 content. The circuit hash, the statevector and the +//! receipt all follow that gateset. use ed25519_dalek::SigningKey; -use wai::quantum::{circuit_hash, BaseGate, Circuit, Gate, Measure, StateVector, FRAC}; +use wai::quantum::{circuit_hash, BaseGate, Circuit, Gate, Gateset, Measure, StateVector, FRAC}; use wai::quantum_mps; use wai::quantum_pauli; use wai::quantum_receipt::QuantumReceipt; @@ -51,17 +56,86 @@ fn name_of(b: BaseGate) -> &'static str { } } +/// What the IDE prints on the browser console, never in a returned value, when +/// a circuit/1 circuit holds a phase that circuit/1 does not reproduce. It is the +/// command-line tool's note, with the IDE's own switch as the remedy. +const V1_NOTE: &str = + "circuit/1 table: P(k≥8) deviates by up to 12868 units of 2^-30, P(k≥19) applies no phase; use set_gateset(2)"; + +/// The note for `c`, if it is a circuit/1 circuit with a `P(k ≥ 8)`: the same +/// condition the command-line tool notes. +fn v1_note(c: &Circuit) -> Option<&'static str> { + (c.gateset == Gateset::V1 && c.ops.iter().any(|g| g.base == BaseGate::P && g.param >= 8)).then_some(V1_NOTE) +} + +#[cfg(target_arch = "wasm32")] +#[wasm_bindgen] +extern "C" { + #[wasm_bindgen(js_namespace = console, js_name = warn)] + fn console_warn(s: &str); +} + +/// Off wasm32 (the native unit tests) the console is standard error. +#[cfg(not(target_arch = "wasm32"))] +fn console_warn(s: &str) { + eprintln!("{s}"); +} + +/// The gateset `set_gateset` names: 1 is circuit/1, 2 is circuit2. +fn gateset_numbered(g: u8) -> Option { + match g { + 1 => Some(Gateset::V1), + 2 => Some(Gateset::V2), + _ => None, + } +} + #[wasm_bindgen] pub struct QuantumIde { + /// The circuit being edited. Its gateset is the IDE's: every circuit the IDE + /// builds is built in it. circuit: Circuit, sv: Option, + /// Whether the console note has been printed for the circuit as it stands. + noted: bool, } #[wasm_bindgen] impl QuantumIde { + /// An empty circuit on `n_qubits` (1 to 12), in circuit2. #[wasm_bindgen(constructor)] pub fn new(n_qubits: u8) -> QuantumIde { - QuantumIde { circuit: Circuit::new(n_qubits.clamp(1, 12)), sv: None } + QuantumIde { circuit: Circuit::with_gateset(n_qubits.clamp(1, 12), Gateset::V2), sv: None, noted: false } + } + + // ---- gateset ---- + /// Switch the IDE to gateset `g`: 2 for `wai.quantum.circuit2` (the + /// default), 1 for the frozen `wai.quantum.circuit`. The current circuit + /// keeps its gates and moves to that gateset, and every later build uses it. + /// Any other `g` changes nothing and returns `false`. + pub fn set_gateset(&mut self, g: u8) -> bool { + let Some(gs) = gateset_numbered(g) else { return false }; + self.circuit.gateset = gs; + self.changed(); + true + } + /// The IDE's gateset: 2 for circuit2, 1 for circuit/1. + pub fn gateset(&self) -> u8 { + match self.circuit.gateset { + Gateset::V1 => 1, + Gateset::V2 => 2, + // `set_gateset` names no other gateset. + _ => 0, + } + } + /// The IDE's capability string: `wai.quantum.circuit2` or `wai.quantum.circuit`. + pub fn capability(&self) -> String { + self.circuit.gateset.capability().to_string() + } + /// The console note for the current circuit, or `""`: the text `run` and + /// `run_to` print on the console for a circuit/1 circuit with a `P(k ≥ 8)`. + pub fn note(&self) -> String { + v1_note(&self.circuit).unwrap_or_default().to_string() } // ---- editing ---- @@ -71,45 +145,49 @@ impl QuantumIde { pub fn n_gates(&self) -> u32 { self.circuit.ops.len() as u32 } + /// Replace the circuit with an empty one on `n` qubits (1 to 12), in the + /// IDE's gateset. pub fn set_qubits(&mut self, n: u8) { - self.circuit = Circuit::new(n.clamp(1, 12)); - self.sv = None; + self.circuit = Circuit::with_gateset(n.clamp(1, 12), self.circuit.gateset); + self.changed(); } /// Append a gate. `controls` are the control qubits (empty for a 1-qubit gate); - /// `param` is the `P(k)` dyadic index (ignored otherwise). + /// `param` is the `P(k)` dyadic index, and is stored as 0 for every other + /// gate, which carries no parameter. Returns whether the circuit is still + /// valid in its gateset; the gate is appended either way. pub fn add(&mut self, name: &str, controls: Vec, target: u8, param: u16) -> bool { let Some(base) = base_of(name) else { return false }; + let param = if base == BaseGate::P { param } else { 0 }; self.circuit.ops.push(Gate { base, controls, target, param }); - self.sv = None; + self.changed(); self.circuit.validate().is_ok() } pub fn pop(&mut self) { self.circuit.ops.pop(); - self.sv = None; + self.changed(); } pub fn clear(&mut self) { self.circuit.ops.clear(); - self.sv = None; + self.changed(); } - /// Replace the circuit with a library algorithm. + /// Replace the circuit with a library algorithm, built in the IDE's gateset. pub fn load_algorithm(&mut self, id: &str, n: u8, param: u32) -> bool { if let Some(c) = tc::build_algorithm(id, n.clamp(1, 12), param) { - self.circuit = c; - self.sv = None; + self.replace(c); true } else { false } } - /// Replace the circuit from an OpenQASM source. Returns an error string on - /// failure (unsupported gate, non-dyadic phase, bad register). + /// Replace the circuit from an OpenQASM source, built in the IDE's gateset. + /// Returns an error string on failure (unsupported gate, non-dyadic phase, + /// bad register, a gate that names one qubit twice). pub fn load_qasm(&mut self, src: &str) -> Result<(), String> { let c = tc::from_qasm(src)?; if c.n_qubits > 12 { return Err(format!("{} qubits exceeds the in-browser limit (12)", c.n_qubits)); } - self.circuit = c; - self.sv = None; + self.replace(c); Ok(()) } /// The op-log as JSON, for the editor to render. @@ -136,6 +214,7 @@ impl QuantumIde { pub fn run(&mut self) -> Result<(), String> { let sv = self.circuit.simulate().map_err(|e| e.to_string())?; self.sv = Some(sv); + self.print_note(); Ok(()) } /// Debugger step: simulate only the first `k` gates and set the inspected state @@ -144,10 +223,11 @@ impl QuantumIde { /// entanglement build up. pub fn run_to(&mut self, k: u32) -> Result<(), String> { let k = (k as usize).min(self.circuit.ops.len()); - let mut sub = Circuit::new(self.circuit.n_qubits); + let mut sub = self.circuit.empty_like(); sub.ops = self.circuit.ops[..k].to_vec(); let sv = sub.simulate().map_err(|e| e.to_string())?; self.sv = Some(sv); + self.print_note(); Ok(()) } fn state(&self) -> Option<&StateVector> { @@ -217,8 +297,10 @@ impl QuantumIde { pub fn work(&self) -> f64 { tc::statevector_work(&self.circuit) as f64 } + /// The circuit hash, which binds the IDE's capability and its phase table, + /// or `""` for a circuit its gateset does not allow. pub fn circuit_hash_hex(&self) -> String { - hex(&circuit_hash(&self.circuit)) + circuit_hash(&self.circuit).map(|h| hex(&h)).unwrap_or_default() } pub fn statevector_hash_hex(&self) -> String { self.state().map(|sv| hex(&sv.statevector_hash())).unwrap_or_default() @@ -338,6 +420,31 @@ impl QuantumIde { } impl QuantumIde { + /// The circuit changed: its state and its note are stale. + fn changed(&mut self) { + self.sv = None; + self.noted = false; + } + + /// Install a circuit built elsewhere, moved into the IDE's gateset. The + /// builders make circuit2 circuits with distinct controls and no parameter + /// on a gate other than `P`, which are valid circuit/1 circuits too. + fn replace(&mut self, mut c: Circuit) { + c.gateset = self.circuit.gateset; + self.circuit = c; + self.changed(); + } + + /// Print the console note once for the circuit as it stands. + fn print_note(&mut self) { + if !self.noted { + if let Some(note) = v1_note(&self.circuit) { + console_warn(note); + } + self.noted = true; + } + } + fn seal(&self, seed: u64, shots: u64) -> Option { QuantumReceipt::seal( &signer(), @@ -360,3 +467,137 @@ pub fn ide_catalog() -> String { .collect(); format!("[{}]", items.join(",")) } + +#[cfg(test)] +mod tests { + use super::QuantumIde; + use std::path::Path; + + fn manifest(rel: &str) -> serde_json::Map { + let p = Path::new(env!("CARGO_MANIFEST_DIR")).join("../quantum-conformance").join(rel); + let text = std::fs::read_to_string(p).unwrap(); + serde_json::from_str::(&text).unwrap().as_object().unwrap().clone() + } + + /// The catalog circuits the browser check drives, by corpus file name. + const CASES: [(&str, &str, u8); 4] = [("bell.wqc", "bell", 2), ("ghz3.wqc", "ghz", 3), ("qft3.wqc", "qft", 3), ("qft5.wqc", "qft", 5)]; + + /// The IDE builds in circuit2 by default and reproduces the v2 manifest; + /// after `set_gateset(1)` it reproduces the circuit/1 manifest. A gateset + /// it does not know changes nothing. + #[test] + fn catalog_circuits_follow_the_ide_gateset() { + for (g, rel, capability) in [(2u8, "v2/expected_hashes.json", "wai.quantum.circuit2"), (1, "expected_hashes.json", "wai.quantum.circuit")] { + let m = manifest(rel); + for (name, id, n) in CASES { + let mut ide = QuantumIde::new(n); + if g == 1 { + assert!(ide.set_gateset(1)); + } + assert!(ide.load_algorithm(id, n, 0), "{name}"); + assert_eq!((ide.gateset(), ide.capability().as_str()), (g, capability), "{name}"); + ide.run().unwrap(); + assert_eq!(ide.circuit_hash_hex(), m[name]["circuit_hash"].as_str().unwrap(), "{name} in {g}"); + assert_eq!(ide.statevector_hash_hex(), m[name]["statevector_hash"].as_str().unwrap(), "{name} in {g}"); + } + } + let mut ide = QuantumIde::new(2); + assert_eq!(ide.gateset(), 2); + for g in [0, 3, 255] { + assert!(!ide.set_gateset(g)); + assert_eq!(ide.gateset(), 2); + } + } + + /// Switching gateset moves the current circuit, and every later build + /// (`set_qubits`, `load_algorithm`, `load_qasm`) and the debugger's prefix + /// circuit stay in it. + #[test] + fn builds_and_prefixes_keep_the_ide_gateset() { + let mut ide = QuantumIde::new(5); + assert!(ide.load_algorithm("qft", 5, 0)); + let v2 = ide.circuit_hash_hex(); + assert!(ide.set_gateset(1)); + let v1 = ide.circuit_hash_hex(); + assert_ne!(v1, v2); + ide.set_qubits(3); + assert_eq!(ide.gateset(), 1); + ide.load_qasm("OPENQASM 3.0;\nqubit[2] q;\nh q[0];\ncx q[0], q[1];\n").unwrap(); + assert_eq!(ide.capability(), "wai.quantum.circuit"); + assert!(ide.load_algorithm("qft", 5, 0)); + assert_eq!(ide.circuit_hash_hex(), v1); + + // The debugger's state after the last gate is the run's state, in circuit/1. + ide.run().unwrap(); + let full = ide.statevector_hash_hex(); + ide.run_to(ide.n_gates()).unwrap(); + assert_eq!(ide.statevector_hash_hex(), full); + // A prefix differs from the same prefix in circuit2. + ide.run_to(3).unwrap(); + let prefix_v1 = ide.statevector_hash_hex(); + assert!(ide.set_gateset(2)); + ide.run_to(3).unwrap(); + assert_ne!(ide.statevector_hash_hex(), prefix_v1); + } + + /// A parameter given to a gate other than `P` is stored as 0, so the gate + /// is valid in circuit2; a `P(k)` keeps its index and is checked. + #[test] + fn add_keeps_a_parameter_only_on_p() { + let mut ide = QuantumIde::new(2); + assert!(ide.add("h", vec![], 0, 5)); + assert!(ide.add("x", vec![0], 1, 9)); + assert!(ide.gates_json().contains("{\"name\":\"h\",\"controls\":[],\"target\":0,\"param\":0}"), "{}", ide.gates_json()); + assert!(ide.add("p", vec![0], 1, 32)); + assert!(ide.gates_json().ends_with("\"param\":32}]"), "{}", ide.gates_json()); + assert!(!ide.add("p", vec![], 0, 0)); + ide.pop(); + assert!(!ide.add("p", vec![], 0, 33)); + ide.pop(); + assert!(ide.run().is_ok()); + } + + /// An invalid circuit has no circuit hash: the IDE shows `""` instead of + /// failing, and shows the hash again once the circuit is valid. + #[test] + fn an_invalid_circuit_has_an_empty_hash() { + let mut ide = QuantumIde::new(3); + ide.add("h", vec![], 0, 0); + let valid = ide.circuit_hash_hex(); + assert_eq!(valid.len(), 64); + // A repeated control is invalid in circuit2 and allowed in circuit/1. + assert!(!ide.add("x", vec![0, 0], 1, 0)); + assert_eq!(ide.circuit_hash_hex(), ""); + assert!(ide.run().is_err()); + assert!(ide.set_gateset(1)); + assert_eq!(ide.circuit_hash_hex().len(), 64); + assert!(ide.set_gateset(2)); + // A target out of range is invalid in both. + ide.pop(); + assert!(!ide.add("x", vec![], 7, 0)); + for g in [1, 2] { + assert!(ide.set_gateset(g)); + assert_eq!(ide.circuit_hash_hex(), ""); + } + ide.pop(); + assert_eq!(ide.circuit_hash_hex(), valid); + } + + /// The note is the command-line tool's condition: a circuit/1 circuit with + /// a `P(k ≥ 8)`. A circuit2 circuit, or a circuit/1 circuit whose phases + /// stop at `P(7)`, gets none. + #[test] + fn the_note_names_the_circuit1_phases() { + let mut ide = QuantumIde::new(2); + ide.add("p", vec![0], 1, 7); + assert_eq!(ide.note(), ""); + assert!(ide.set_gateset(1)); + assert_eq!(ide.note(), ""); + ide.add("p", vec![0], 1, 8); + assert_eq!(ide.note(), super::V1_NOTE); + assert!(super::V1_NOTE.starts_with("circuit/1 table: P(k≥8) deviates by up to 12868 units of 2^-30, P(k≥19) applies no phase; use ")); + ide.run().unwrap(); + assert!(ide.set_gateset(2)); + assert_eq!(ide.note(), ""); + } +} -- 2.54.0 (Apple Git-157) From e92109f08aa544befcc7bab84e61dd8f6bf0f0fb Mon Sep 17 00:00:00 2001 From: Transaction Science Date: Fri, 9 Oct 2026 13:30:41 -0400 Subject: [PATCH 08/13] wai-quantum circuit2: an integer-only referee, a wasm32-wasip2 leg and the browser check for both capabilities wai/quantum-conformance/tools/referee.py is a second implementation of the quantum-sim extension in Python, standard library only and integers only. It shares no code with wai-rs or wai-quantum. It checks its own BLAKE3 against the published test vectors. It derives the circuit/1 table by its recurrence and the circuit2 table by the interval-certified u128 procedure, and requires both literal tables and their digests. It rebuilds every file of both vector trees byte for byte, reads each back, and recomputes every manifest entry with its own simulator and sampler; the four original circuit/1 vectors must also keep their frozen hashes. It requires reject.json to list exactly the 26 reject cases and refuses each file with its listed class and by the rule its name states. It requires dyadic_ladder to change its statevector hash under every change of 1 to either component of every P(1..32), in both tables. wai-conformance.yml runs the referee with python3 -I in the independent-verifiers job on Linux. A new quantum-wasip2 job on Linux runs tests/bit_identity.rs and the unit tests of the quantum, receipt_pins, quantum_fabric, quantum_source, quantum_channel, quantum_qudit and quantum_receipt modules under wasmtime, as debug builds so that overflow panics, and fails unless every one of those modules ran a test. It then runs wai_quantum_vectors verify over the corpus under wasmtime. wasmtime is the 49.0.2 release binary, pinned by its SHA-256. browser_verify.mjs now opens every file of both vector trees in QuantumSink against its manifest entry (capability, n_qubits, n_ops, circuit, statevector and histogram hashes), requires every reject file to be refused with its listed class, and checks QuantumIde's bell, ghz and qft against the circuit2 manifest by default and against the circuit/1 manifest after set_gateset(1). It remains a manual tool. The corpus README describes the referee, the CI leg and the browser check. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/wai-conformance.yml | 100 ++- wai/quantum-conformance/README.md | 30 +- wai/quantum-conformance/browser_verify.mjs | 206 +++++- wai/quantum-conformance/tools/referee.py | 798 +++++++++++++++++++++ 4 files changed, 1091 insertions(+), 43 deletions(-) create mode 100644 wai/quantum-conformance/tools/referee.py diff --git a/.github/workflows/wai-conformance.yml b/.github/workflows/wai-conformance.yml index 2cc9cfe29..12b80f603 100644 --- a/.github/workflows/wai-conformance.yml +++ b/.github/workflows/wai-conformance.yml @@ -457,6 +457,92 @@ jobs: v staged-measure wai_stage_measure ../staged-measure-conformance v telemetry wai_telemetry ../telemetry-conformance + # wai-quantum and the quantum corpus under wasm32-wasip2, run by wasmtime. On wasm32 + # usize is 32 bits, so arithmetic that cannot overflow on a 64-bit host can there: a + # section's offset plus its length, a capacity reserved from a count a file claims, a + # shift by a qubit count. The builds are debug builds, so overflow checks are on and + # such an overflow panics instead of wrapping. Three steps: + # 1. tests/bit_identity.rs, which pins one result of each f64 layer, so the wasm32 + # build must give the bits every native leg gives; + # 2. the library's unit tests in the modules that hold the circuit core, its + # container and its pins (quantum, receipt_pins, quantum_fabric, quantum_source, + # quantum_channel, quantum_qudit, quantum_receipt), each required to have run; + # 3. wai_quantum_vectors verify over all three trees of quantum-conformance/ (both + # capabilities and the reject corpus), the same check the matrix above runs natively. + # One leg: wasm32 is the same machine on every host. Linux, so it does not queue on the + # single macOS runner. + quantum-wasip2: + name: quantum core and corpus under wasm32-wasip2 (wasmtime) + runs-on: linux + timeout-minutes: 60 + steps: + - uses: actions/checkout@v5 + + - name: Install Rust 1.98.0 with the wasm32-wasip2 target + shell: bash + run: | + export PATH="$HOME/.cargo/bin:$PATH" + command -v rustup >/dev/null 2>&1 || \ + curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --profile minimal --default-toolchain 1.98.0 + rustup toolchain install 1.98.0 --profile minimal + rustup default 1.98.0 + rustup target add --toolchain 1.98.0 wasm32-wasip2 + echo "$HOME/.cargo/bin" >> "$GITHUB_PATH" + + # The release binary, pinned by version and SHA-256, rather than a build from source. + - name: Install wasmtime 49.0.2 + shell: bash + run: | + set -euo pipefail + [ "$(uname -m)" = x86_64 ] || { echo "::error::the pinned wasmtime archive is x86_64; this host is $(uname -m)"; exit 1; } + if ! command -v xz >/dev/null 2>&1; then + SUDO=""; [ "$(id -u)" -ne 0 ] && SUDO="sudo" + $SUDO apt-get update -qq + $SUDO apt-get install -y -qq xz-utils + fi + v=49.0.2 + f="wasmtime-v$v-x86_64-linux.tar.xz" + curl --proto '=https' --tlsv1.2 -sSfL -o "$RUNNER_TEMP/$f" \ + "https://github.com/bytecodealliance/wasmtime/releases/download/v$v/$f" + echo "a4d6e9e3a5a60f527cf7793d674c48930c80c2e8977995b8a275cad3254b9322 $RUNNER_TEMP/$f" | sha256sum -c - + tar -xJf "$RUNNER_TEMP/$f" -C "$RUNNER_TEMP" + echo "$RUNNER_TEMP/wasmtime-v$v-x86_64-linux" >> "$GITHUB_PATH" + + - name: tests/bit_identity.rs under wasmtime + working-directory: wai/wai-quantum + env: + CARGO_TARGET_WASM32_WASIP2_RUNNER: wasmtime + run: cargo test --locked --target wasm32-wasip2 --features full --test bit_identity + + # The filters are module paths, so a module that is renamed or loses its tests would + # leave its filter matching nothing and the run passing; the loop fails the step + # unless every module had a test that passed. + - name: The circuit core's unit tests under wasmtime + shell: bash + working-directory: wai/wai-quantum + env: + CARGO_TARGET_WASM32_WASIP2_RUNNER: wasmtime + run: | + set -euo pipefail + mods="quantum receipt_pins quantum_fabric quantum_source quantum_channel quantum_qudit quantum_receipt" + filters=() + for m in $mods; do filters+=("$m::"); done + cargo test --locked --target wasm32-wasip2 --features full --lib -- "${filters[@]}" 2>&1 \ + | tee "$RUNNER_TEMP/wasip2-lib.log" + for m in $mods; do + grep -q "^test $m::.* \.\.\. ok$" "$RUNNER_TEMP/wasip2-lib.log" \ + || { echo "::error::no test of $m ran"; exit 1; } + done + + - name: wai_quantum_vectors verify under wasmtime + shell: bash + working-directory: wai/wai-rs + run: | + set -euo pipefail + cargo build --locked --target wasm32-wasip2 --no-default-features --features quantum --bin wai_quantum_vectors + wasmtime run --dir ../quantum-conformance::/corpus \ + target/wasm32-wasip2/debug/wai_quantum_vectors.wasm verify /corpus + # The generated v2 vocabulary (wai/vocab/v2.*) is checked as a consumer uses it: # sample manifests, typed and untyped, WAI1 and WAI2, are expanded through the # JSON-LD context exactly as written on the wire and validated against the SHACL @@ -488,7 +574,7 @@ jobs: # verdicts required to equal wai_signer_vectors' line for line), and # int_payload_verify.py (the integer payloads and their refusal fixtures), and # staged_verify.py (the staged-delivery corpus), and http_verify.py (the HTTP delivery - # corpus). + # corpus), and quantum-conformance/tools/referee.py (both quantum-sim capabilities). # Each exits non-zero on any failure or any difference, and the step fails with it. # One platform: what they check is the reading of the text, not the architecture. independent-verifiers: @@ -587,6 +673,18 @@ jobs: working-directory: wai shell: bash run: python3 staged-measure-conformance/sim_verify.py staged-measure-conformance + # The quantum corpus's second implementation (quantum-sim extension), integers only: + # it derives the circuit/1 phase table by its recurrence and the circuit2 table by an + # interval-certified procedure, and requires both literal tables and their digests; + # rebuilds every file of both vector trees byte for byte and recomputes every manifest + # entry with its own reader, simulator, sampler and BLAKE3; refuses every reject file + # with its listed class and by the rule its name states; and requires dyadic_ladder + # to see every ±1 change of every P(k) under both tables. Standard library only, run + # isolated (-I), so no module from the working directory, the environment or the + # user's site-packages is imported. + - name: quantum referee.py + shell: bash + run: python3 -I wai/quantum-conformance/tools/referee.py wai/quantum-conformance # chain_differential.py signs receipt chains around jwp-receipts §3 step 3 and §6.1 # (two keys, two tracks, both payloads, replays, forks, gaps, the guard and the # freshness bound), and chain_gap_differential.py genuine chains up to 90 groups diff --git a/wai/quantum-conformance/README.md b/wai/quantum-conformance/README.md index 715cd7308..5aba94b8f 100644 --- a/wai/quantum-conformance/README.md +++ b/wai/quantum-conformance/README.md @@ -28,15 +28,37 @@ wasmtime run --dir wai/quantum-conformance::/corpus \ target/wasm32-wasip2/release/wai_quantum_vectors.wasm verify /corpus ``` +CI runs this leg as a debug build, so integer overflow panics instead of wrapping +(`wai-conformance.yml`, job `quantum-wasip2`, which also runs the crate's +`bit_identity` tests and the circuit core's unit tests under `wasmtime`). + ## Browser ```sh -node wai/quantum-conformance/browser_verify.mjs [path/to/quantum-ml/pkg] +node wai/quantum-conformance/browser_verify.mjs [path/to/quantum-ml/pkg] [module file] ``` -Drives the deployed wasm-bindgen bundle in headless Chromium. Note that -`statevector_hash_hex()` is empty until `run()` is called — the circuit is built -eagerly, the state is not. +Drives the deployed wasm-bindgen bundle in headless Chromium: the sink opens every +file of both vector trees to its manifest entry and refuses every reject file with +its listed class, and the IDE builds its catalog circuits in each gateset to the +matching manifest. It is run by hand, before the website deploys a bundle. Note that +the IDE's `statevector_hash_hex()` is empty until `run()` is called — the circuit is +built eagerly, the state is not. + +## Referee + +```sh +python3 -I wai/quantum-conformance/tools/referee.py wai/quantum-conformance +``` + +A second implementation of the extension in Python, integers only, sharing no code +with the Rust reference. It derives both phase tables (circuit/1 by its recurrence, +circuit2 by an interval-certified procedure) and requires their literal values and +digests; rebuilds every vector file byte for byte and recomputes every manifest +entry; refuses every reject file with its listed class and by the rule its name +states; and requires `dyadic_ladder` to change its statevector hash under every ±1 +change of every phase-table entry, in both tables. CI runs it on Linux +(`wai-conformance.yml`, job `independent-verifiers`). ## Scope diff --git a/wai/quantum-conformance/browser_verify.mjs b/wai/quantum-conformance/browser_verify.mjs index e8db3973c..11fee714d 100644 --- a/wai/quantum-conformance/browser_verify.mjs +++ b/wai/quantum-conformance/browser_verify.mjs @@ -1,14 +1,26 @@ -// Browser leg of statevector-equivalence. +// Browser leg of statevector-equivalence, for both capabilities. // // The native and `wasm32-wasip2` legs are checked by `wai_quantum_vectors verify`. -// This checks the third surface the extension claims — a browser — against the -// same pinned corpus, so all three are held to one reference rather than to each -// other. +// This checks the third surface the extension claims, a browser, against the same +// pinned corpus, so all three are held to one reference rather than to each other. +// It is a manual tool: run it on the bundle before the website deploys it. // -// node browser_verify.mjs [path/to/quantum-ml/pkg] +// node browser_verify.mjs [path/to/quantum-ml/pkg] [module file] // -// Needs Playwright with its bundled Chromium. `statevector_hash_hex()` is empty -// until `run()` is called: the circuit is built eagerly, the state is not. +// In headless Chromium, with the wasm-bindgen bundle (module file +// `wai_quantum_ml.js` by default): +// +// - QuantumSink opens every file of `vectors/` (wai.quantum.circuit) and +// `v2/vectors/` (wai.quantum.circuit2) to its manifest entry: the capability (an +// entry of the circuit/1 manifest names none), n_qubits, n_ops, the circuit and +// statevector hashes, and the histogram hash where the entry pins a measurement. +// - QuantumSink refuses every file `reject/reject.json` lists, and its error names +// the listed class in parentheses: ` (): `. +// - QuantumIde.load_algorithm builds bell, ghz and qft in its default gateset to +// `v2/expected_hashes.json`, and after `set_gateset(1)` to the circuit/1 manifest. +// +// Needs Playwright with its bundled Chromium. `statevector_hash_hex()` of the IDE is +// empty until `run()` is called: the circuit is built eagerly, the state is not. import http from "node:http"; import fs from "node:fs"; import path from "node:path"; @@ -18,19 +30,68 @@ import { createRequire } from "node:module"; const HERE = path.dirname(fileURLToPath(import.meta.url)); const PKG = process.argv[2] || "/Users/dcharlot/data-share/vibe-coding/wai-transaction-science-web/public/quantum-ml/pkg"; +const MODULE = process.argv[3] || "wai_quantum_ml.js"; const PLAYWRIGHT_HOST = process.env.PLAYWRIGHT_HOST || "/Users/dcharlot/data-share/vibe-coding/joulesperbit/"; const { chromium } = createRequire(PLAYWRIGHT_HOST)("playwright"); -const EXPECTED = JSON.parse(fs.readFileSync(path.join(HERE, "expected_hashes.json"), "utf8")); -// vector name -> how the catalog builds that circuit -const CASES = [ - ["bell.wqc", "bell", 2, 0], - ["ghz3.wqc", "ghz", 3, 0], - ["qft3.wqc", "qft", 3, 0], - ["qft5.wqc", "qft", 5, 0], +const readJson = (p) => JSON.parse(fs.readFileSync(p, "utf8")); +const bytesOf = (p) => Array.from(fs.readFileSync(p)); + +// The two vector trees. An entry of the circuit/1 manifest names no capability. +const TREES = [ + { prefix: "", dir: HERE, capability: "wai.quantum.circuit" }, + { prefix: "v2/", dir: path.join(HERE, "v2"), capability: null }, ]; +const MANIFEST = Object.fromEntries(TREES.map((t) => [t.prefix, readJson(path.join(t.dir, "expected_hashes.json"))])); + +// Every vector file, with what the sink must report for it. +const SINK = []; +for (const t of TREES) { + for (const [file, e] of Object.entries(MANIFEST[t.prefix])) { + SINK.push({ + label: t.prefix + file, + bytes: bytesOf(path.join(t.dir, "vectors", file)), + want: { + capability: e.capability || t.capability, + n_qubits: e.n_qubits, + n_ops: e.n_ops, + circuit: e.circuit_hash, + statevector: e.statevector_hash, + histogram: e.measure ? e.measure.histogram_hash : null, + }, + }); + } +} + +// Every reject file, with the class it must be refused as. +const REJECT_DIR = path.join(HERE, "reject"); +const REJECT = Object.entries(readJson(path.join(REJECT_DIR, "reject.json"))).map(([file, kind]) => ({ + label: "reject/" + file, + bytes: bytesOf(path.join(REJECT_DIR, file)), + kind, +})); + +// The IDE's catalog: vector file -> (algorithm id, qubits), in each gateset against +// its manifest. Gateset 2 (circuit2) is the IDE's default; 1 is set explicitly. +const CATALOG = [ + ["bell.wqc", "bell", 2], + ["ghz3.wqc", "ghz", 3], + ["qft3.wqc", "qft", 3], + ["qft5.wqc", "qft", 5], +]; +const IDE = []; +for (const [gateset, prefix, capability] of [[2, "v2/", "wai.quantum.circuit2"], [1, "", "wai.quantum.circuit"]]) { + for (const [file, id, n] of CATALOG) { + const e = MANIFEST[prefix][file]; + IDE.push({ + label: `QuantumIde ${id}(${n}) in gateset ${gateset} = ${prefix}${file}`, + id, n, gateset, + want: { gateset, capability, circuit: e.circuit_hash, statevector: e.statevector_hash }, + }); + } +} const MIME = { ".js": "text/javascript", ".wasm": "application/wasm", ".html": "text/html" }; const PORT = Number(process.env.PORT || 8794); @@ -46,12 +107,17 @@ const server = http.createServer((req, res) => { fs.createReadStream(f).pipe(res); }); +// Compare the fields of `want` with `got`; the names of those that differ. +function differ(want, got) { + return Object.keys(want).filter((k) => got[k] !== want[k]); +} + let browser; process.on("unhandledRejection", (e) => { console.error("UNHANDLED:", e); process.exitCode = 1; }); try { - if (!fs.existsSync(path.join(PKG, "wai_quantum_ml.js"))) { - console.error(`no bundle at ${PKG} — pass the pkg directory as argv[1]`); + if (!fs.existsSync(path.join(PKG, MODULE))) { + console.error(`no ${MODULE} at ${PKG}; pass the pkg directory (and the module file) as arguments`); process.exit(2); } await new Promise((r) => server.listen(PORT, "127.0.0.1", r)); @@ -64,37 +130,101 @@ try { // but networkidle still stalls behind them on some hosts. await page.goto(`http://127.0.0.1:${PORT}/`, { waitUntil: "domcontentloaded", timeout: 30000 }); - const got = await page.evaluate(async ({ cases, port }) => { - const mod = await import(`http://127.0.0.1:${port}/wai_quantum_ml.js`); + const got = await page.evaluate(async ({ sink, reject, ide, port, module }) => { + const mod = await import(`http://127.0.0.1:${port}/${module}`); await mod.default(); - const out = {}; - for (const [name, id, n, param] of cases) { - const ide = new mod.QuantumIde(n); - const ok = ide.load_algorithm(id, n, param); - if (ok) ide.run(); // the state is not computed until this - out[name] = ok - ? { circuit: ide.circuit_hash_hex(), statevector: ide.statevector_hash_hex() } - : { error: "load_algorithm returned false" }; - ide.free(); + const message = (e) => String((e && e.message) || e); + const out = { sink: {}, reject: {}, ide: {} }; + for (const c of sink) { + try { + const s = new mod.QuantumSink(new Uint8Array(c.bytes)); + out.sink[c.label] = { + capability: s.capability(), + n_qubits: s.n_qubits(), + n_ops: s.n_ops(), + circuit: s.circuit_hash_hex(), + statevector: s.statevector_hash_hex(), + histogram: s.histogram_hash_hex() ?? null, + }; + s.free(); + } catch (e) { + out.sink[c.label] = { error: message(e) }; + } + } + for (const c of reject) { + try { + new mod.QuantumSink(new Uint8Array(c.bytes)).free(); + out.reject[c.label] = { accepted: true }; + } catch (e) { + out.reject[c.label] = { error: message(e) }; + } + } + for (const c of ide) { + const q = new mod.QuantumIde(c.n); + try { + if (c.gateset !== 2 && !q.set_gateset(c.gateset)) { + out.ide[c.label] = { error: `set_gateset(${c.gateset}) returned false` }; + continue; + } + if (!q.load_algorithm(c.id, c.n, 0)) { + out.ide[c.label] = { error: "load_algorithm returned false" }; + continue; + } + q.run(); // the state is not computed until this + out.ide[c.label] = { + gateset: q.gateset(), + capability: q.capability(), + circuit: q.circuit_hash_hex(), + statevector: q.statevector_hash_hex(), + }; + } catch (e) { + out.ide[c.label] = { error: message(e) }; + } finally { + q.free(); + } } return out; - }, { cases: CASES, port: PORT }); + }, { sink: SINK, reject: REJECT, ide: IDE, port: PORT, module: MODULE }); let fail = 0; - for (const [name] of CASES) { - const e = EXPECTED[name], g = got[name]; - if (g.circuit === e.circuit_hash && g.statevector === e.statevector_hash) { - console.log(`PASS ${name}`); - } else { + const report = (label, problem) => { + if (problem) { fail++; - console.log(`FAIL ${name}`); - console.log(` circuit browser=${g.circuit}\n corpus =${e.circuit_hash}`); - console.log(` statevector browser=${g.statevector}\n corpus =${e.statevector_hash}`); + console.log(`FAIL ${label}: ${problem}`); + } else { + console.log(`PASS ${label}`); } + }; + for (const c of SINK) { + const g = got.sink[c.label]; + if (g.error) { + report(c.label, `refused: ${g.error}`); + continue; + } + const bad = differ(c.want, g); + report(c.label, bad.length ? bad.map((k) => `${k} browser=${g[k]} corpus=${c.want[k]}`).join("; ") : null); } - console.log(errs.length ? `console errors: ${errs.length}` : "console errors: 0"); + for (const c of REJECT) { + const g = got.reject[c.label]; + if (g.accepted) { + report(c.label, `accepted; it must be refused as ${c.kind}`); + continue; + } + const m = /^[^()]+ \(([a-z-]+)\): /.exec(g.error); + report(`${c.label} refused as ${c.kind}`, m && m[1] === c.kind ? null : `refused as: ${g.error}`); + } + for (const c of IDE) { + const g = got.ide[c.label]; + if (g.error) { + report(c.label, g.error); + continue; + } + const bad = differ(c.want, g); + report(c.label, bad.length ? bad.map((k) => `${k} browser=${g[k]} corpus=${c.want[k]}`).join("; ") : null); + } + console.log(errs.length ? `console errors: ${errs.length}\n ${errs.join("\n ")}` : "console errors: 0"); if (fail === 0 && errs.length === 0) { - console.log("all vectors pass: statevector-equivalence holds in the browser"); + console.log("all checks pass: statevector-equivalence holds in the browser for both capabilities, and every reject file is refused as listed"); } process.exitCode = fail || errs.length ? 1 : 0; } catch (e) { diff --git a/wai/quantum-conformance/tools/referee.py b/wai/quantum-conformance/tools/referee.py new file mode 100644 index 000000000..ede11b41a --- /dev/null +++ b/wai/quantum-conformance/tools/referee.py @@ -0,0 +1,798 @@ +#!/usr/bin/env python3 +"""Referee for quantum-conformance/: both capabilities of the quantum-sim extension +(extensions/quantum-sim.md), `wai.quantum.circuit` (WQC1) and `wai.quantum.circuit2` +(WQC2), recomputed with integers only. + +Written from the extension's rules. It shares no code with wai-rs or wai-quantum, +which generate the corpus: the phase tables, the container writer and reader, the +fixed-point simulator, the shot sampler and BLAKE3 are all written here. Standard +library only, and no floating point anywhere. + +It checks, in this order, and reports every failure rather than stopping at one: + +1. BLAKE3 against the published test vectors. +2. The phase tables. circuit/1's is derived by its recurrence (§3.1) and circuit2's + by an interval-certified procedure in exact integers (§3.2): each step keeps a + true enclosure of 2^62·cos(2π/2^k) and 2^62·sin(2π/2^k), and both ends of every + enclosure round to the same integer far from a tie, so that integer is the + nearest one. Each derived table must equal its literal table (Appendix B, restated + below) and its BLAKE3 digest. +3. The vector trees: `./` (circuit/1) and `v2/` (circuit2). Every file is rebuilt byte + for byte from the circuit definitions below and read back by the reader here, and + every manifest entry must equal the entry computed here: n_qubits, n_ops, the + circuit, statevector and histogram hashes, the measurement, and in `v2/` the + capability. The four original circuit/1 vectors must also keep their frozen hashes. +4. The reject tree: `reject.json` must list exactly the cases below, the directory + must hold exactly the listed files, and the reader here must refuse each one with + its listed class and by the rule its name states. +5. Mutation completeness: under each table, a change of ±1 to either component of any + P(1..32), with H following re P(3), must change dyadic_ladder's statevector hash. + +It was written from the same reading of the text as the Rust reference, so a misreading +the two share would pass both. + + python3 -I referee.py [quantum-conformance dir] + +Exit status 0 only when every check passes. +""" +import json +import os +import struct +import sys +from math import isqrt + +# ---------------------------------------------------------------- BLAKE3 (hash mode) + +_IV = [0x6A09E667, 0xBB67AE85, 0x3C6EF372, 0xA54FF53A, 0x510E527F, 0x9B05688C, 0x1F83D9AB, 0x5BE0CD19] +_PERM = [2, 6, 3, 10, 7, 0, 4, 13, 1, 11, 12, 5, 9, 14, 15, 8] +_CHUNK_START, _CHUNK_END, _PARENT, _ROOT = 1, 2, 4, 8 +_M32 = 0xFFFFFFFF +_CHUNK = 1024 +_BLOCK = 64 + + +def _rotr(x, n): + return ((x >> n) | (x << (32 - n))) & _M32 + + +def _g(s, a, b, c, d, x, y): + s[a] = (s[a] + s[b] + x) & _M32 + s[d] = _rotr(s[d] ^ s[a], 16) + s[c] = (s[c] + s[d]) & _M32 + s[b] = _rotr(s[b] ^ s[c], 12) + s[a] = (s[a] + s[b] + y) & _M32 + s[d] = _rotr(s[d] ^ s[a], 8) + s[c] = (s[c] + s[d]) & _M32 + s[b] = _rotr(s[b] ^ s[c], 7) + + +def _compress(cv, block, counter, block_len, flags): + m = list(struct.unpack("<16I", block)) + s = list(cv) + _IV[:4] + [counter & _M32, (counter >> 32) & _M32, block_len, flags] + for r in range(7): + _g(s, 0, 4, 8, 12, m[0], m[1]) + _g(s, 1, 5, 9, 13, m[2], m[3]) + _g(s, 2, 6, 10, 14, m[4], m[5]) + _g(s, 3, 7, 11, 15, m[6], m[7]) + _g(s, 0, 5, 10, 15, m[8], m[9]) + _g(s, 1, 6, 11, 12, m[10], m[11]) + _g(s, 2, 7, 8, 13, m[12], m[13]) + _g(s, 3, 4, 9, 14, m[14], m[15]) + if r < 6: + m = [m[i] for i in _PERM] + return [s[i] ^ s[i + 8] for i in range(8)] + [s[i + 8] ^ cv[i] for i in range(8)] + + +def _chunk_output(chunk, counter): + """The last block of a chunk, ready to compress: (cv, block, counter, len, flags).""" + cv = _IV + n_blocks = max(1, (len(chunk) + _BLOCK - 1) // _BLOCK) + for b in range(n_blocks): + block = chunk[b * _BLOCK:(b + 1) * _BLOCK] + flags = _CHUNK_START if b == 0 else 0 + if b == n_blocks - 1: + return (cv, block.ljust(_BLOCK, b"\0"), counter, len(block), flags | _CHUNK_END) + cv = _compress(cv, block, counter, _BLOCK, flags)[:8] + + +def _cv(output): + cv, block, counter, block_len, flags = output + return _compress(cv, block, counter, block_len, flags)[:8] + + +def _parent(left_cv, right_cv): + return (_IV, struct.pack("<8I", *left_cv) + struct.pack("<8I", *right_cv), 0, _BLOCK, _PARENT) + + +def blake3(data): + """The 32-byte BLAKE3 hash of `data`.""" + chunks = [data[i:i + _CHUNK] for i in range(0, len(data), _CHUNK)] or [b""] + stack = [] + for i, chunk in enumerate(chunks[:-1]): + cv = _cv(_chunk_output(chunk, i)) + total = i + 1 + while total & 1 == 0: + cv = _cv(_parent(stack.pop(), cv)) + total >>= 1 + stack.append(cv) + node = _chunk_output(chunks[-1], len(chunks) - 1) + while stack: + node = _parent(stack.pop(), _cv(node)) + cv, block, counter, block_len, flags = node + return struct.pack("<8I", *_compress(cv, block, counter, block_len, flags | _ROOT)[:8]) + + +# The published BLAKE3 test vectors (input byte i is i mod 251): input length, hash. +BLAKE3_VECTORS = [ + (0, "af1349b9f5f9a1a6a0404dea36dcc9499bcb25c9adc112b7cc9a93cae41f3262"), + (1, "2d3adedff11b61f14c886e35afa036736dcd87a74d27b5c1510225d0f592e213"), + (63, "e9bc37a594daad83be9470df7f7b3798297c3d834ce80ba85d6e207627b7db7b"), + (64, "4eed7141ea4a5cd4b788606bd23f46e212af9cacebacdc7d1f4c6dc7f2511b98"), + (65, "de1e5fa0be70df6d2be8fffd0e99ceaa8eb6e8c93a63f2d8d1c30ecb6b263dee"), + (1023, "10108970eeda3eb932baac1428c7a2163b0e924c9a9e25b35bba72b28f70bd11"), + (1024, "42214739f095a406f3fc83deb889744ac00df831c10daa55189b5d121c855af7"), + (1025, "d00278ae47eb27b34faecf67b4fe263f82d5412916c1ffd97c8cb7fb814b8444"), + (2048, "e776b6028c7cd22a4d0ba182a8bf62205d2ef576467e838ed6f2529b85fba24a"), + (2049, "5f4d72f40d7a5f82b15ca2b2e44b1de3c2ef86c426c95c1af0b6879522563030"), + (3072, "b98cb0ff3623be03326b373de6b9095218513e64f1ee2edd2525c7ad1e5cffd2"), + (3073, "7124b49501012f81cc7f11ca069ec9226cecb8a2c850cfe644e327d22d3e1cd3"), + (4096, "015094013f57a5277b59d8475c0501042c0b642e531b0a1c8f58d2163229e969"), + (4097, "9b4052b38f1c5fc8b1f9ff7ac7b27cd242487b3d890d15c96a1c25b8aa0fb995"), + (8193, "bab6c09cb8ce8cf459261398d2e7aef35700bf488116ceb94a36d0f5f1b7bc3b"), + (16384, "f875d6646de28985646f34ee13be9a576fd515f76b5b0a26bb324735041ddde4"), + (31744, "62b6960e1a44bcc1eb1a611a8d6235b6b4b78f32e7abc4fb4c6cdcce94895c47"), +] + +# ---------------------------------------------------------------- the phase tables + +FRAC = 30 +ONE = 1 << FRAC +HALF = 1 << (FRAC - 1) +DYADIC_MAX = 32 + +# Appendix B.1: circuit/1, (re, im) for k = 1..32. P(19..32) apply no phase at all. +PHASE_V1 = [None] + [ + (-1073741824, 0), (0, 1073741824), (759250124, 759250124), (992008094, 410903207), + (1053110175, 209476638), (1068571463, 105245102), (1072448454, 52686012), + (1073418432, 26350953), (1073660972, 13176485), (1073721610, 6588405), + (1073736770, 3294284), (1073740560, 1647223), (1073741507, 823774), + (1073741744, 411887), (1073741803, 207243), (1073741818, 103621), + (1073741822, 56755), (1073741823, 32768), +] + [(1073741823, 0)] * 14 +PHASE_V1_DIGEST = "3fa9805ff96feb819a1cf371fedcd3b5228f93eddf933d0edcf401cbf8c28466" + +# Appendix B.2: circuit2, the integers nearest 2^30·cos(2π/2^k) and 2^30·sin(2π/2^k). +PHASE_V2 = [None] + [ + (-1073741824, 0), (0, 1073741824), (759250125, 759250125), (992008094, 410903207), + (1053110176, 209476638), (1068571464, 105245103), (1072448455, 52686014), + (1073418433, 26350943), (1073660973, 13176464), (1073721611, 6588356), + (1073736771, 3294193), (1073740561, 1647099), (1073741508, 823550), + (1073741745, 411775), (1073741804, 205887), (1073741819, 102944), + (1073741823, 51472), +] + [(ONE, im) for im in (25736, 12868, 6434, 3217, 1608, 804, 402, 201, 101, 50, 25, 13, 6, 3, 2)] +PHASE_V2_DIGEST = "d34cb793715348e7c92db027c0bc3dddb6008f5730470bafaed978ea7fb23510" + + +def derive_v1(): + """circuit/1's table by its recurrence: with c = re P(k-1), + re P(k) = isqrt(floor((2^30 + c) / 2) · 2^30), im P(k) = isqrt(floor((2^30 - c) / 2) · 2^30).""" + t = [None, (-ONE, 0), (0, ONE)] + for k in range(3, DYADIC_MAX + 1): + c = t[k - 1][0] + t.append((isqrt(((ONE + c) // 2) << FRAC), isqrt(((ONE - c) // 2) << FRAC))) + return t + + +def derive_v2(): + """circuit2's table by enclosures at scale 2^62, and the reasons it is not certified. + + cos(θ/2) = √((1 + cos θ)/2) and sin(θ/2) = sin θ / (2 cos(θ/2)), each bound rounded + outward, starting from (cos, sin)(π/2) = (0, 1), which is exact.""" + s = 1 << 62 + + def ceil_isqrt(x): + r = isqrt(x) + return r if r * r == x else r + 1 + + def ceil_div(a, b): + return -((-a) // b) + + def nearest(x): + # From 2^-62 to 2^-30, half up. + return (x + (1 << 31)) >> 32 + + problems = [] + c_lo, c_hi, s_lo, s_hi = 0, 0, s, s + t = [None, (-ONE, 0), (0, ONE)] + for k in range(3, DYADIC_MAX + 1): + c_lo2 = isqrt(((s + c_lo) * s) >> 1) + c_hi2 = ceil_isqrt(ceil_div((s + c_hi) * s, 2)) + s_lo2 = (s_lo * s) // (2 * c_hi2) + s_hi2 = ceil_div(s_hi * s, 2 * c_lo2) + if nearest(c_lo2) != nearest(c_hi2) or nearest(s_lo2) != nearest(s_hi2): + problems.append("k=%d: the enclosure's ends round apart" % k) + for x in (c_lo2, c_hi2, s_lo2, s_hi2): + if abs((x % (1 << 32)) - (1 << 31)) <= (1 << 20): + problems.append("k=%d: an end lies within 2^20 of a tie" % k) + t.append((nearest(c_lo2), nearest(s_lo2))) + c_lo, c_hi, s_lo, s_hi = c_lo2, c_hi2, s_lo2, s_hi2 + return t, problems + + +def table_digest(t): + """BLAKE3 over i64le(re) ‖ i64le(im) for k = 1..32, no domain prefix.""" + return blake3(b"".join(struct.pack(" P: + raise Refused("malformed", "opcode") + param = struct.unpack_from("= n_qubits: + raise Refused("invalid", "target") + if len(controls) > 255: + raise Refused("invalid", "control count") + for c in controls: + if c >= n_qubits: + raise Refused("invalid", "control") + if c == target: + raise Refused("invalid", "control is target") + if gs is V2 and len(set(controls)) != len(controls): + raise Refused("invalid", "repeated control") + if op == P: + if not 1 <= param <= DYADIC_MAX: + raise Refused("invalid", "phase index") + elif gs is V2 and param != 0: + raise Refused("invalid", "param") + + +def read_measure(gs, raw): + """circuit/1: JSON, a missing or non-u64 seed or shots reads as 0. circuit2: u64 seed + and shots, and exactly the canonical bytes.""" + mv = strict_json(raw) + if mv is _NOT_JSON: + raise Refused("malformed", "measure JSON") + if gs is V1: + def field(key): + v = mv.get(key) if isinstance(mv, dict) else None + return v if unsigned(v, 1 << 64) else 0 + return (field("seed"), field("shots")) + for key in ("seed", "shots"): + if not (isinstance(mv, dict) and unsigned(mv.get(key), 1 << 64)): + raise Refused("malformed", "measure " + key) + m = (mv["seed"], mv["shots"]) + if raw != measure_bytes(*m): + raise Refused("malformed", "canonical measure") + return m + + +def read_wqc(data): + """(gateset, n_qubits, ops, measure or None), or Refused.""" + if len(data) < 6 or data[:4] not in BY_MAGIC: + raise Refused("malformed", "magic") + gs = BY_MAGIC[data[:4]] + n_sections = struct.unpack_from(" len(data): + raise Refused("malformed", "section table") + sections = {} + for i in range(n_sections): + p = 6 + 9 * i + kind = data[p] + offset, length = struct.unpack_from(" len(data): + raise Refused("malformed", "section bounds") + if kind not in (SECT_CONTRACT, SECT_OPLOG, SECT_MEASURE): + continue # unknown kinds are ignored + if kind in sections: + raise Refused("malformed", "duplicate section") + sections[kind] = data[start:end] + if SECT_CONTRACT not in sections: + raise Refused("malformed", "missing contract") + if SECT_OPLOG not in sections: + raise Refused("malformed", "missing op-log") + n_qubits = read_contract(gs, sections[SECT_CONTRACT]) + ops, used = read_oplog(sections[SECT_OPLOG]) + if gs is V2 and used != len(sections[SECT_OPLOG]): + raise Refused("malformed", "trailing bytes") + check_valid(gs, n_qubits, ops) + measure = read_measure(gs, sections[SECT_MEASURE]) if SECT_MEASURE in sections else None + return gs, n_qubits, ops, measure + + +# ---------------------------------------------------------------- simulator (§3.0) + + +def fx(a, b): + """(a·b + 2^29) >> 30, exact, with an arithmetic shift.""" + return (a * b + HALF) >> FRAC + + +def cmul(a, b): + return (fx(a[0], b[0]) - fx(a[1], b[1]), fx(a[0], b[1]) + fx(a[1], b[0])) + + +def cadd(a, b): + return (a[0] + b[0], a[1] + b[1]) + + +def matrix(op, param, table, h): + z, one, i, minus_i = (0, 0), (ONE, 0), (0, ONE), (0, -ONE) + p3 = table[3] + return { + I: ((one, z), (z, one)), + X: ((z, one), (one, z)), + Y: ((z, minus_i), (i, z)), + Z: ((one, z), (z, (-ONE, 0))), + H: (((h, 0), (h, 0)), ((h, 0), (-h, 0))), + S: ((one, z), (z, i)), + SDG: ((one, z), (z, minus_i)), + T: ((one, z), (z, p3)), + TDG: ((one, z), (z, (p3[0], -p3[1]))), + P: ((one, z), (z, table[param] if op == P else z)), + }[op] + + +def simulate(n_qubits, ops, table): + """The statevector from |0…0⟩. H is re P(3) of the same table.""" + h = table[3][0] + dim = 1 << n_qubits + amps = [(0, 0)] * dim + amps[0] = (ONE, 0) + for op, controls, target, param in ops: + (m00, m01), (m10, m11) = matrix(op, param, table, h) + tbit = 1 << target + cmask = 0 + for c in controls: + cmask |= 1 << c + for i in range(dim): + if i & tbit == 0 and i & cmask == cmask: + j = i | tbit + a, b = amps[i], amps[j] + amps[i] = cadd(cmul(m00, a), cmul(m01, b)) + amps[j] = cadd(cmul(m10, a), cmul(m11, b)) + return amps + + +# ---------------------------------------------------------------- the hashes (§4) + + +def circuit_hash(gs, n_qubits, ops): + return blake3(b"wai:quantum-circuit\x01" + gs.contract(n_qubits) + oplog_bytes(ops)).hex() + + +def statevector_hash(n_qubits, amps): + out = bytearray(b"wai:quantum-statevector\x01") + bytes([n_qubits]) + struct.pack("> 30)) * 0xBF58476D1CE4E5B9) & 0xFFFFFFFFFFFFFFFF + z = ((z ^ (z >> 27)) * 0x94D049BB133111EB) & 0xFFFFFFFFFFFFFFFF + return state, z ^ (z >> 31) + + +def histogram_hash(n_qubits, amps, seed, shots): + """Each shot draws r = splitmix64 mod W, W the sum of the weights re² + im², and + counts the first basis state whose cumulative weight exceeds r.""" + cumulative, total = [], 0 + for re, im in amps: + total += re * re + im * im + cumulative.append(total) + counts = [0] * len(amps) + state = seed + for _ in range(shots): + state, r = splitmix64(state) + r %= total + lo, hi = 0, len(cumulative) + while lo < hi: + mid = (lo + hi) // 2 + if cumulative[mid] > r: + hi = mid + else: + lo = mid + 1 + counts[min(lo, len(counts) - 1)] += 1 + out = bytearray(b"wai:quantum-histogram\x01") + bytes([n_qubits]) + struct.pack(" j; then the + reversal swaps, each as CX(a→b), CX(b→a), CX(a→b).""" + ops = [] + for j in range(n): + ops.append(gate(H, j)) + for l in range(j + 1, n): + ops.append(gate(P, j, (l,), l - j + 1)) + for j in range(n // 2): + a, b = j, n - 1 - j + ops += [gate(X, b, (a,)), gate(X, a, (b,)), gate(X, b, (a,))] + return ops + + +def dyadic_ladder_ops(): + ops = [gate(X, 0)] + [gate(op, 1) for op in (Y, Z, S, SDG, T, TDG, I)] + chain = [gate(P, 1, (0,), k) for k in range(1, DYADIC_MAX + 1)] + return ops + chain + chain + + +# name: (n_qubits, ops, measure (seed, shots) or None). Both trees hold all of them. +VECTORS = { + "bell": (2, [gate(H, 0), gate(X, 1, (0,))], (12648430, 100000)), + "ghz3": (3, [gate(H, 0), gate(X, 1, (0,)), gate(X, 2, (1,))], None), + "qft3": (3, qft_ops(3), None), + "qft5": (5, qft_ops(5), None), + "qft5_x22": (5, [gate(X, 1), gate(X, 2), gate(X, 4)] + qft_ops(5), None), + "dyadic_ladder": (2, dyadic_ladder_ops(), None), + "qft8_ones": (8, [gate(X, q) for q in range(8)] + qft_ops(8), None), +} + +# The four original circuit/1 vectors are frozen: circuit, statevector, histogram hash. +FROZEN_V1 = { + "bell": ("894d9a9582b19f27b8d08d06938357237fc00f7f30bf4bfc2be0c04fa5e47a4e", + "bd5df69662f4e61878d17baee1f3cf5705c3726d6740d1a4bf7be5133c099efb", + "0893f48ef32ca9f2865f6617537f0a4a5334f67e3c944bba845beb2f377acdb3"), + "ghz3": ("34e44f7d68037cbb3aee677f56b894dc48df6d408aa068a16d3b18076e5e7c5c", + "08c5e6fb50b91a00d28795506cc5d99ce1e9a3a8b4c41e4bfd05f5d829af9542", None), + "qft3": ("7e8414be4410557a05b52bcc1084e1c5f93b6caa0f28fa45f4d2ab661efd2a60", + "0fe1e5b8e2ac8e9afbfa926b3877967ebfead92efb7ed8e1ff6b117668926d6e", None), + "qft5": ("f36a9af99a251cae6f42144b2be3c57523f15e1e2ab3492cf15cac709fc048be", + "c268d77f2ddf84fecb028e80ce989123912870f04e5b9a1e3d885c9ce235c27d", None), +} + +# The reject corpus: file stem → (class, the rule that refuses it here). +REJECTS = { + "magic_wqc3": ("malformed", "magic"), + "v1_ext_other": ("contract", "ext"), + "v1_numeric_other": ("contract", "numeric"), + "v1_frac_31": ("contract", "frac"), + "v1_frac_float": ("contract", "frac"), + "v1_gateset_rn": ("contract", "gateset"), + "v1_contract2_in_wqc1": ("contract", "ext"), + "v1_fields_missing": ("contract", "ext"), + "v1_contract_not_object": ("contract", "not an object"), + "v1_n_qubits_256": ("malformed", "n_qubits"), + "v1_n_qubits_258": ("malformed", "n_qubits"), + "v1_contract_twice": ("malformed", "duplicate section"), + "v1_oplog_twice": ("malformed", "duplicate section"), + "v1_n_ops_u32max": ("malformed", "op header"), + "v1_offset_wrap": ("malformed", "section bounds"), + "v2_contract_whitespace": ("contract", "canonical contract"), + "v2_contract_key_order": ("contract", "canonical contract"), + "v2_contract_extra_member": ("contract", "canonical contract"), + "v2_contract1_in_wqc2": ("contract", "canonical contract"), + "v2_duplicate_controls": ("invalid", "repeated control"), + "v2_param_on_h": ("invalid", "param"), + "v2_oplog_trailing_byte": ("malformed", "trailing bytes"), + "v2_opcode_10": ("malformed", "opcode"), + "v2_measure_spaced": ("malformed", "canonical measure"), + "v2_measure_no_shots": ("malformed", "measure shots"), + "v2_measure_basis_x": ("malformed", "canonical measure"), +} + +# ---------------------------------------------------------------- the checks + + +class Report: + def __init__(self): + self.failures = 0 + + def check(self, label, problem): + """`problem` is None for a pass, else why it failed.""" + if problem is None: + print("PASS", label) + else: + self.failures += 1 + print("FAIL %s: %s" % (label, problem)) + + +def check_blake3(rep): + bad = [n for n, want in BLAKE3_VECTORS if blake3(bytes(i % 251 for i in range(n))).hex() != want] + rep.check("BLAKE3 against the published test vectors", "lengths %s differ" % bad if bad else None) + + +def check_tables(rep): + v1 = derive_v1() + rep.check("circuit/1 table: the recurrence gives Appendix B.1", + None if v1 == PHASE_V1 else "k = %s differ" % [k for k in range(1, 33) if v1[k] != PHASE_V1[k]]) + d1 = table_digest(PHASE_V1) + rep.check("circuit/1 table digest", None if d1 == PHASE_V1_DIGEST else "got " + d1) + v2, problems = derive_v2() + rep.check("circuit2 table: every enclosure certifies its nearest integer", "; ".join(problems) or None) + rep.check("circuit2 table: the certified integers are Appendix B.2", + None if v2 == PHASE_V2 else "k = %s differ" % [k for k in range(1, 33) if v2[k] != PHASE_V2[k]]) + d2 = table_digest(PHASE_V2) + rep.check("circuit2 table digest", None if d2 == PHASE_V2_DIGEST else "got " + d2) + + +def read_object(path): + """A JSON object from `path`, or the reason it is not one.""" + try: + with open(path, "rb") as f: + v = strict_json(f.read()) + except OSError as e: + return None, str(e) + if not isinstance(v, dict): + return None, "not a JSON object" + return v, None + + +def listed_and_present(listed, directory): + """None when `directory` holds exactly the `.wqc` files `listed` names.""" + try: + present = {f for f in os.listdir(directory) if f.endswith(".wqc")} + except OSError as e: + return str(e) + listed = set(listed) + if listed == present: + return None + return "listed but missing %s, present but unlisted %s" % (sorted(listed - present), sorted(present - listed)) + + +def expected_entry(gs, name, label_capability): + """The manifest entry for vector `name` in `gs`, computed here, and the hashes.""" + n, ops, measure = VECTORS[name] + amps = simulate(n, ops, gs.table) + entry = {"n_qubits": n, "n_ops": len(ops), "circuit_hash": circuit_hash(gs, n, ops), + "statevector_hash": statevector_hash(n, amps)} + if label_capability: + entry["capability"] = gs.capability + if measure is not None: + entry["measure"] = {"seed": measure[0], "shots": measure[1], + "histogram_hash": histogram_hash(n, amps, *measure)} + return entry + + +def check_tree(rep, root, sub, gs, label_capability): + tree = os.path.join(root, sub) if sub else root + prefix = sub + "/" if sub else "" + manifest, why = read_object(os.path.join(tree, "expected_hashes.json")) + if manifest is None: + rep.check(prefix + "expected_hashes.json", why) + return + names = ["%s.wqc" % v for v in VECTORS] + rep.check(prefix + "expected_hashes.json lists every vector and no other", + None if set(manifest) == set(names) else "lists %s" % sorted(manifest)) + rep.check(prefix + "vectors/ holds exactly the listed files", listed_and_present(names, os.path.join(tree, "vectors"))) + for name, (n, ops, measure) in VECTORS.items(): + label = "%s%s.wqc (%s)" % (prefix, name, gs.label) + try: + with open(os.path.join(tree, "vectors", name + ".wqc"), "rb") as f: + data = f.read() + except OSError as e: + rep.check(label, str(e)) + continue + problems = [] + if data != write_wqc(gs, n, ops, measure): + problems.append("the file is not the bytes rebuilt here") + try: + if read_wqc(data) != (gs, n, ops, measure): + problems.append("the file reads back as another circuit") + except Refused as r: + problems.append("refused (%s)" % r) + want = expected_entry(gs, name, label_capability) + got = manifest.get(name + ".wqc") + if got != want: + differ = sorted(k for k in set(want) | set(got or {}) if (got or {}).get(k) != want.get(k)) + problems.append("manifest members %s differ from the values computed here" % differ) + if gs is V1 and name in FROZEN_V1: + frozen = FROZEN_V1[name] + computed = (want["circuit_hash"], want["statevector_hash"], + want["measure"]["histogram_hash"] if "measure" in want else None) + if computed != frozen: + problems.append("a frozen circuit/1 hash changed") + rep.check(label, "; ".join(problems) or None) + + +def check_rejects(rep, root): + tree = os.path.join(root, "reject") + listing, why = read_object(os.path.join(tree, "reject.json")) + if listing is None: + rep.check("reject/reject.json", why) + return + want = {"%s.wqc" % stem: kind for stem, (kind, _) in REJECTS.items()} + rep.check("reject/reject.json lists every reject case with its class", + None if listing == want else "differs in %s" % sorted( + f for f in set(listing) | set(want) if listing.get(f) != want.get(f))) + rep.check("reject/ holds exactly the listed files", listed_and_present(listing, tree)) + for stem, (kind, rule) in REJECTS.items(): + label = "reject/%s.wqc" % stem + try: + with open(os.path.join(tree, stem + ".wqc"), "rb") as f: + data = f.read() + except OSError as e: + rep.check(label, str(e)) + continue + try: + read_wqc(data) + rep.check(label, "accepted; it must be refused as %s (%s)" % (kind, rule)) + except Refused as r: + rep.check(label + " refused as %s (%s)" % (kind, rule), + None if (r.kind, r.rule) == (kind, rule) else "refused as %s (%s)" % (r.kind, r.rule)) + + +def check_mutations(rep): + n, ops, _ = VECTORS["dyadic_ladder"] + for gs in (V1, V2): + base = statevector_hash(n, simulate(n, ops, gs.table)) + missed = [] + for k in range(1, DYADIC_MAX + 1): + for component in (0, 1): + for delta in (-1, 1): + table = list(gs.table) + entry = list(table[k]) + entry[component] += delta + table[k] = tuple(entry) + if statevector_hash(n, simulate(n, ops, table)) == base: + missed.append("P(%d).%s%+d" % (k, "re" if component == 0 else "im", delta)) + rep.check("dyadic_ladder sees every ±1 change of P(1..32) in the %s table" % gs.label, + "missed %s" % missed if missed else None) + + +def main(): + root = sys.argv[1] if len(sys.argv) > 1 else os.path.join(os.path.dirname(os.path.abspath(__file__)), "..") + rep = Report() + check_blake3(rep) + check_tables(rep) + check_tree(rep, root, "", V1, False) + check_tree(rep, root, "v2", V2, True) + check_rejects(rep, root) + check_mutations(rep) + if rep.failures: + print("%d failed — NOT conformant" % rep.failures) + sys.exit(1) + print("referee: every check passes") + + +if __name__ == "__main__": + main() -- 2.54.0 (Apple Git-157) From 95849040e33f8fb74d2c621d9af863c114d0d3a2 Mon Sep 17 00:00:00 2001 From: Transaction Science Date: Fri, 9 Oct 2026 13:55:36 -0400 Subject: [PATCH 09/13] wai-quantum circuit2: the specification, registry and documentation for both capabilities The quantum-sim extension now specifies both capabilities to the bit. Its header and section 1 name wai.quantum.circuit2 for new content and wai.quantum.circuit as frozen. Section 2 states the little-endian layout, overflow-free section bounds, the refusal of a repeated known section, and the three refusal classes in the order a reader checks them. Section 2.1 gives both canonical contracts, the ext convention, the circuit/1 reader rules and circuit2's byte-canonical rule, and defines wai.det.amp64 and the meaning of wai.det.fixed64 inside a circuit/1 contract. Sections 2.2 and 2.3 state the op-log and measurement rules per capability. Section 3.0 states the shared arithmetic, the index convention and the gate matrices. Section 3.1 gives the exact circuit/1 recurrence and the deviation of every entry and no longer claims that table gives the QFT. Section 3.2 defines the circuit2 table as the nearest integers, makes the literal table normative with its digest, and gives the interval-certified derivation as informative. Sections 4 to 6 state the hash and sampler layouts, the error per capability and how receipts name their capability, and a new section 8 says why circuit2 is a capability of its own, what older readers do, and which circuit/1 payloads become non-conforming. Appendix B holds both phase tables and digests, rendered from PHASE_V1 and PHASE_V2 by a new wai_quantum_vectors appendix subcommand. A bin test fails when the text differs from the constants, and the referee requires Appendix B to state its literal tables; wai-conformance now triggers on the extension. SPEC.md names wai.det.amp64 and lists the newly non-conforming circuit/1 payloads. The registry adds wai.det.amp64 as an identifier and annotates wai.det.fixed64, and the export is regenerated. The crate rustdoc, the crate, wai and quantum-conformance READMEs, and the changelog describe both capabilities, the corpus coverage and its limits, and the 0.4.0 migration. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/wai-conformance.yml | 7 +- wai/CHANGELOG.md | 78 +++ wai/README.md | 21 +- wai/REGISTRY.md | 8 +- wai/SPEC.md | 22 +- wai/extensions/quantum-sim.md | 576 +++++++++++++++++++--- wai/quantum-conformance/README.md | 46 +- wai/quantum-conformance/tools/referee.py | 72 ++- wai/registry/capabilities.json | 7 +- wai/wai-quantum/README.md | 31 +- wai/wai-quantum/src/quantum.rs | 39 +- wai/wai-rs/src/bin/wai_quantum_vectors.rs | 86 +++- wai/wai-rs/src/codecs/registry.rs | 7 +- 13 files changed, 879 insertions(+), 121 deletions(-) diff --git a/.github/workflows/wai-conformance.yml b/.github/workflows/wai-conformance.yml index 12b80f603..266221a29 100644 --- a/.github/workflows/wai-conformance.yml +++ b/.github/workflows/wai-conformance.yml @@ -62,6 +62,8 @@ on: - 'wai/extensions/http-delivery.md' - 'wai/extensions/moq-streaming-format.md' - 'wai/extensions/jwp-receipts.md' + # The quantum referee requires this extension's Appendix B to state its phase tables. + - 'wai/extensions/quantum-sim.md' - 'wai/wai-web/**' # The audio provenance check below reads the corpora above with this tool. - 'wai/tools/wai_audio_provenance_check.py' @@ -95,6 +97,8 @@ on: - 'wai/extensions/http-delivery.md' - 'wai/extensions/moq-streaming-format.md' - 'wai/extensions/jwp-receipts.md' + # The quantum referee requires this extension's Appendix B to state its phase tables. + - 'wai/extensions/quantum-sim.md' - 'wai/wai-web/**' # The audio provenance check below reads the corpora above with this tool. - 'wai/tools/wai_audio_provenance_check.py' @@ -675,7 +679,8 @@ jobs: run: python3 staged-measure-conformance/sim_verify.py staged-measure-conformance # The quantum corpus's second implementation (quantum-sim extension), integers only: # it derives the circuit/1 phase table by its recurrence and the circuit2 table by an - # interval-certified procedure, and requires both literal tables and their digests; + # interval-certified procedure, and requires both literal tables and their digests, + # and the extension's Appendix B to state the same; # rebuilds every file of both vector trees byte for byte and recomputes every manifest # entry with its own reader, simulator, sampler and BLAKE3; refuses every reject file # with its listed class and by the rule its name states; and requires dyadic_ladder diff --git a/wai/CHANGELOG.md b/wai/CHANGELOG.md index 112c348c5..19905ced0 100644 --- a/wai/CHANGELOG.md +++ b/wai/CHANGELOG.md @@ -6,6 +6,84 @@ wire format may change and such changes are recorded here. ## v0.4.0 — current +### Quantum circuits: `wai.quantum.circuit2`, and `wai.quantum.circuit` stated exactly + +- **A new capability, `wai.quantum.circuit2`** ([quantum-sim](extensions/quantum-sim.md), + SPEC §5). Magic `WQC2`, contract `ext` `wai.quantum.circuit2/1`, numeric + `wai.det.amp64`, gateset `cliffordT+dyadicP.rn`. The container layout, the gate IR and + the arithmetic are those of `wai.quantum.circuit`; the phase table is not. Each + `P(k)` holds the integers nearest to `2^30·cos(2π/2^k)` and `2^30·sin(2π/2^k)`, so its + controlled-phase ladder gives the QFT. The literal table is normative (§3.2, Appendix + B.2, digest `d34cb793…3510`); an exact-integer interval procedure certifies every entry, + informatively. The contract and the measurement are byte-canonical, and the op-log ends + exactly after its last op. Registered in the machine-readable registry and the + vocabularies, with `wai.det.amp64` as an identifier. +- **`wai.quantum.circuit` is frozen, and its table stated for the first time** (§3.1, + Appendix B.1, digest `3fa9805f…8466`). The text gives the exact recurrence — the sine + formula, the truncating halving, the floor square root — and the deviation of every + entry from the ideal phase: from `P(8)` on the imaginary part is too large, by up to + 7032 units of `2^−30`, and `P(19..32)` apply no phase. The claim that this table gives the + QFT is withdrawn. Writers SHOULD NOT emit new circuit/1 content. +- **Both capabilities are specified to the bit** (§2–§4): little-endian integers, section + bounds computed without overflow, a repeated known section refused, the three refusal + classes (malformed, contract, invalid) in the order a reader checks them, the + complex-product composition, the index convention, the gate matrices, the hash layouts + and the shot sampler. `circuit_hash` covers the canonical contract, so it names the + capability and its table; `statevector_hash` does not. §5 states the error per + capability, and a new §8 why circuit2 is a capability of its own and what an older + reader does with it (it refuses `WQC2` at the magic). +- **Newly non-conforming circuit/1 payloads** (quantum-sim §8, SPEC §5): a contract whose + `ext`, `numeric`, `frac` or `gateset` is missing or carries another value; an + `n_qubits` that is not an unsigned integer of at most 255; a known section kind more + than once; section bounds that overflow. No file a reference writer wrote is among + them. +- **Corpus.** `quantum-conformance/` keeps its four original vectors and manifest entries + byte for byte and adds qft5_x22, dyadic_ladder and qft8_ones, which see the phase table: + under either table, `dyadic_ladder`'s statevector hash changes with every ±1 change of + any `P(1..32)` component. `v2/` holds the same seven circuits as circuit2, and `reject/` + a payload for each refusal with its class. `tools/referee.py`, integers only, derives + both tables, requires Appendix B to state them, and rebuilds and checks every file. The + Appendix B tables are rendered from the reference constants by + `wai_quantum_vectors appendix`, and a test fails when the text differs. + +### wai-quantum 0.4.0: circuits carry their gateset + +- **`Gateset { V1, V2 }`** names the capability a circuit is reconstructed with, and + `Circuit` carries one in a new field. `Circuit::new` and `Circuit::qft` build V2 + (`wai.quantum.circuit2`); a circuit read from a container takes the container's, and one + derived from another keeps it. `PHASE_V1` and `PHASE_V2` are literal constants: no + recurrence runs at load. `simulate`, `to_wqc`, `circuit_hash`, receipts, the fabric's + lanes and the toolchain follow the circuit's gateset. `from_wqc` reads both magics, + checks a circuit/1 contract's values and a circuit2 contract's bytes, refuses repeated + sections and wrapping bounds, and no longer sizes an allocation from a claimed op count. +- **Every circuit built in code changes its `circuit_hash`**, since the contract names the + capability, and its statevector wherever `H`, `T`, `T†` or a phase acts on a nonzero + amplitude. Every circuit/1 artefact reproduces: the four original vectors, the legacy + receipt pins, the lanes32 certificate corpus and the other pins are unchanged, built in + `Gateset::V1`, and new V2 pins sit beside them. +- **wai-rs** registers `wai.quantum.circuit2`; `quantum_envelope` tags an envelope with the + circuit's capability and refuses one whose capability and magic disagree. The + `wai-quantum` CLI takes `--gateset v1|v2` (default v2) and names the capability in its + JSON; with `--gateset v1` and a phase of `k ≥ 8` it prints the circuit/1 deviation on + standard error. `wai_quantum_meter` seals the capability the file names. The browser + sink reads both magics; the IDE has `set_gateset`, and its `add()` sets the parameter of + every gate but `P` to 0. The serve component's `/run`, `/qasm` and `/receipt` take + `gateset=v1|v2`. +- **Migration from 0.3** (a breaking release): + - `Circuit::new` is V2. To reproduce or verify a 0.3 artefact, read its `WQC1` file, or + set `c.gateset = Gateset::V1` on a circuit built in code; a receipt's `gateset_of` + says which gateset a circuit's ops match it under. + - `quantum::phase`, `quantum::inv_sqrt2` and `quantum::CAPABILITY` are removed in favour + of `Gateset::phase`, `Gateset::inv_sqrt2` and `Gateset::capability`. + - `to_wqc`, `circuit_hash` and `quantum_toolchain::circuit_id` return a `Result`; they + fail only on a circuit its gateset does not allow, never on one that has simulated. + - `Circuit` and `QuantumError` are `#[non_exhaustive]`; `QuantumError` gains `Contract` + and `kind()`. + - `quantum_fabric::certify_lanes` and `lanes::random_circuit` take a gateset, with a + kernel and generator id per gateset; the circuit/1 ids are unchanged. + - `validate` refuses a gate with more than 255 controls, and in V2 repeated controls and + a parameter on any gate but `P`. `from_wqc` refuses the payloads quantum-sim §8 lists. + ### Staged measurement and the session claim in the browser - **The measurement core, bound for JS** ([staged-measurement](extensions/staged-measurement.md)). diff --git a/wai/README.md b/wai/README.md index aa0d59a36..c3e0e037d 100644 --- a/wai/README.md +++ b/wai/README.md @@ -150,14 +150,19 @@ pure Rust with no host bindings, **one source builds for native and `wasm32-wasip2` and produces byte-identical results** — the same circuit reconstructs to the same statevector hash on a laptop or at the edge. -Only `wai.quantum.circuit` is a registered payload capability, and -`wai.quantum.calibration` and `wai.quantum.job` are registered records; the other -names below are module labels ([`REGISTRY.md`](REGISTRY.md) lists the receipt -labels no revision registers). +`wai.quantum.circuit2` and `wai.quantum.circuit` are the registered payload +capabilities, and `wai.quantum.calibration` and `wai.quantum.job` are registered +records; the other names below are module labels ([`REGISTRY.md`](REGISTRY.md) +lists the receipt labels no revision registers). The two circuit capabilities +share the container and the gates and differ in the phase table: circuit2's holds +the integers nearest to each phase and is the default for new content, and +`wai.quantum.circuit` is frozen so that every file, hash and receipt made with it +reproduces ([quantum-sim](extensions/quantum-sim.md) §8). | Module | What it does | Conformance | |---|---|---| -| `wai.quantum.circuit` | byte-exact statevector simulation (dyadic Clifford+T+P(k)) | statevector-equivalence | +| `wai.quantum.circuit2` | byte-exact statevector simulation (dyadic Clifford+T+P(k)), with the correctly rounded phase table | statevector-equivalence | +| `wai.quantum.circuit` | the same, with the frozen phase table existing content was made with | statevector-equivalence | | `wai.quantum.toolchain` | four backends (statevector / stabilizer / tensor-network MPS / sparse-Pauli), algorithm library, OpenQASM 3.0 in-and-out, **QIR export + ingest**, Bloch + entanglement analysis | statevector-equivalence | | `wai.quantum.compile` | Clifford routing + stabilizer-tableau equivalence proof | signed equivalence certificate | | `wai.quantum.cal.*` | calibration, filter-function robust control, DD noise spectroscopy | reproducible + receipt | @@ -185,8 +190,13 @@ cargo build --release --no-default-features \ ./target/release/wai-quantum qir qft -n 4 | \\ ./target/release/wai-quantum run --qir # and read it back ./target/release/wai-quantum receipt ghz -n 3 | ./target/release/wai-quantum verify +./target/release/wai-quantum run qft -n 5 --gateset v1 # the frozen wai.quantum.circuit ``` +Circuits are `wai.quantum.circuit2` unless `--gateset v1` asks for +`wai.quantum.circuit`, which reproduces the circuit and statevector hashes +earlier releases gave. + ### Same source, at the edge ```bash @@ -218,6 +228,7 @@ wasmtime serve -S cli=y --addr 127.0.0.1:8231 \ GET /health GET /algorithms GET /run?algo=ghz&n=5&top=8 POST /run (body = OpenQASM 3.0) GET /qasm?algo=bell GET /receipt?algo=ghz&n=3[&shots=&seed=] + (/run, /qasm and /receipt also take &gateset=v1|v2) POST /verify (body = receipt JSON; 200 valid / 409 invalid / 400 unparseable) ``` diff --git a/wai/REGISTRY.md b/wai/REGISTRY.md index bfe36bfe7..13509daef 100644 --- a/wai/REGISTRY.md +++ b/wai/REGISTRY.md @@ -49,8 +49,9 @@ either; staged-delivery §4 processes it. Two further tables hold strings that look like capabilities and are not: - **identifiers** name a part of a capability's definition (`wai.det.fixed64`, the - fixed-point floor; `wai.world.sim.*`, the simulation steps of the feed and avatar - capabilities); + fixed-point floor; `wai.det.amp64`, the complex amplitude arithmetic of + `wai.quantum.circuit2`; `wai.world.sim.*`, the simulation steps of the feed and + avatar capabilities); - **unregistered labels** are strings the reference implementation uses as labels that no revision registers, each with the reason. @@ -251,7 +252,8 @@ and a role are fixed once registered. | identifier (not a capability) | meaning | in | |---|---|---| -| `wai.det.fixed64` | the fixed-point arithmetic floor the deterministic *-equivalence criteria are computed in | §5 Interactive, temporal & computed content | +| `wai.det.amp64` | the complex fixed-point arithmetic of wai.quantum.circuit2: re and im each an i64 at scale 2^frac, products exact then rounded half up, sums exact (quantum-sim §2.1, §3.0) | §5 Interactive, temporal & computed content | +| `wai.det.fixed64` | the fixed-point arithmetic floor the deterministic *-equivalence criteria are computed in (Q32.32, interactive-worlds §6.2); inside a wai.quantum.circuit contract it names the quantum-sim §3.0 arithmetic at frac 30, products rounded half up | §5 Interactive, temporal & computed content | | `wai.world.sim.feed0` | the identity simulation step of wai.feed.state | §5 Interactive, temporal & computed content | | `wai.world.sim.skeletal0` | the skeletal-pose simulation step of wai.avatar.pose | §5 Interactive, temporal & computed content | diff --git a/wai/SPEC.md b/wai/SPEC.md index 9acd1ee22..839544ec1 100644 --- a/wai/SPEC.md +++ b/wai/SPEC.md @@ -522,9 +522,9 @@ fidelity tier, a replicate class and a role. A derivation or record string is no capability and is not dispatchable (§4): step 3 does not take an envelope's `capability` that names one, and step 5 may still take a usable fallback. A row of the tables below that registers a record says what the record attests, not a payload format. Named -sub-identifiers this section uses — `wai.det.fixed64`, `wai.world.sim.feed0`, -`wai.world.sim.skeletal0` — name parts of a capability's definition and are -not capabilities. +sub-identifiers this section uses — `wai.det.fixed64`, `wai.det.amp64`, +`wai.world.sim.feed0`, `wai.world.sim.skeletal0` — name parts of a capability's +definition and are not capabilities. ### Mandatory floor @@ -732,6 +732,22 @@ standard. See the linked extension for each. | `wai.quantum.circuit` | WQC container (gate op-log — Clifford+T + dyadic phase) | the wire carries a **circuit** (kilobytes, linear in gate count), the sink CLASSICALLY simulates it in a fixed-point **complex** floor (`Amp` = i64 re/im), and the reconstructed `2^n`-amplitude statevector is byte-identical on every machine — **statevector-equivalence** (a portable statevector hash); measurement is sampled by the pinned `splitmix64` so a shot histogram at a pinned `(seed, shots)` converges too (**shot-histogram-equivalence**). The dyadic phase ladder transports the **QFT** ("quantum FFT") as a circuit, reconstructed byte-exact; the phase table is frozen as existing content was made with it, so `P(k)` deviates from the ideal phase for k ≥ 8 and applies none for k ≥ 19, and new content uses `wai.quantum.circuit2`. No quantum hardware — NISQ noise breaks the determinism the standard rests on; this makes a quantum *computation* transportable + receiptable, with **no** speedup claimed · [quantum-sim](extensions/quantum-sim.md) | | `wai.quantum.circuit2` | WQC container under magic `WQC2` (the same gate op-log) | the successor of `wai.quantum.circuit`, with the same statevector- and shot-histogram-equivalence: a correctly rounded phase table (every `P(k)` within half a unit of 2^30·e^{2πi/2^k}), so the dyadic phase ladder gives the **QFT**; a byte-canonical contract; an op-log that ends exactly after its last op. A new capability, not a change to the old one, because a registered capability's payload format is fixed (§8): a reader that predates it rejects a `WQC2` payload at the magic · [quantum-sim](extensions/quantum-sim.md) | +The contract of a `wai.quantum.circuit2` payload names its arithmetic +`wai.det.amp64`: complex amplitudes as `i64` pairs at scale `2^frac`, each +product exact and then rounded half up, each sum exact. Inside a +`wai.quantum.circuit` contract, `wai.det.fixed64` names that same arithmetic at +frac 30. [quantum-sim](extensions/quantum-sim.md) §2.1 defines both. + +*Payloads the quantum-sim reader rules make non-conforming* (their envelopes still +parse). [quantum-sim](extensions/quantum-sim.md) §2 and §2.1 state reader rules that +enforce the registration of `wai.quantum.circuit` as it was made (§8). A sink refuses, +when it decodes it, a `wai.quantum.circuit` payload whose contract does not carry +the registered `ext`, `numeric`, `frac` and `gateset`, a `wai.quantum.circuit2` +contract among them; whose `n_qubits` is not an unsigned integer of at most 255; +that carries a known section kind more than once; or that has a section whose +bounds overflow. No payload a reference writer wrote is among them; +[quantum-sim](extensions/quantum-sim.md) §8 lists them with the rules. + And the verify half — a signed, energy-accounted lineage for opaque captured content WAI does **not** reconstruct (content-addressing + receipts + measured joules): [`extensions/provenance.md`](extensions/provenance.md) diff --git a/wai/extensions/quantum-sim.md b/wai/extensions/quantum-sim.md index 0cab4fbe2..9bdc3ed76 100644 --- a/wai/extensions/quantum-sim.md +++ b/wai/extensions/quantum-sim.md @@ -5,20 +5,39 @@ > the sink **classically simulates** it in a fixed-point complex floor, > and the reconstructed statevector hashes identically on every machine. > Conformance is **statevector-equivalence** (and, for measured circuits, -> **shot-histogram-equivalence**). Reference impl: the `quantum` feature of -> `wai-rs` (`crate::quantum`); corpus: `quantum-conformance/`; capability -> `wai.quantum.circuit`. +> **shot-histogram-equivalence**). Two capabilities share the container +> layout, the gate IR and the arithmetic: `wai.quantum.circuit2` (magic +> `WQC2`), for new content, and `wai.quantum.circuit` (magic `WQC1`), frozen +> so that existing content reproduces bit for bit. Reference impl: the +> `quantum` module of the `wai-quantum` crate, which `wai-rs` re-exports as +> `wai::quantum` under its `quantum` feature; corpus: `quantum-conformance/`. Keywords **MUST**, **MUST NOT**, **SHOULD**, **MAY** are RFC 2119/8174. ## 1. Scope and model -A `wai.quantum.circuit` object is a **quantum circuit**: a qubit count and -an ordered list of gates. The sink reconstructs the resulting quantum state -by simulating the circuit from `|0…0⟩` in the `wai.det.fixed64` floor — -every amplitude is a complex fixed-point number (`re, im` as `i64` at scale -`2^FRAC`), every gate an integer matrix–vector update with a pinned rounding -rule. It is lever 3 (instructions-at-the-sink) applied to computation: the -wire carries the *circuit*, the sink reconstructs the *state*. +A quantum-circuit object is a **quantum circuit**: a qubit count and an ordered +list of gates. The sink reconstructs the resulting quantum state by simulating +the circuit from `|0…0⟩` in a fixed-point complex floor — every amplitude is a +complex fixed-point number (`re, im` as `i64` at scale `2^FRAC`), every gate an +integer matrix–vector update with a pinned rounding rule. It is lever 3 +(instructions-at-the-sink) applied to computation: the wire carries the +*circuit*, the sink reconstructs the *state*. + +Two capabilities carry such an object. They differ only in the magic, the +contract (§2.1), the phase table the gates read (§3.1, §3.2) and how strictly a +reader holds the container (§2): + +| capability | magic | contract `ext` | phase table | for | +|---|---|---|---|---| +| `wai.quantum.circuit2` | `WQC2` | `wai.quantum.circuit2/1` | correctly rounded (§3.2) | new content | +| `wai.quantum.circuit` | `WQC1` | `wai.quantum.circuit/1` | frozen (§3.1) | existing content, which reproduces bit for bit | + +Below, **circuit2** and **circuit/1** name the two. Writers SHOULD NOT emit new +circuit/1 content: its phase table deviates from the ideal phases for `k ≥ 8` and +applies none for `k ≥ 19` (§3.1). A sink that reconstructs circuit/1 content +reads it exactly as this text states, so that the content keeps its hashes and +its receipts keep verifying. §8 says why the correction is a capability of its +own. The design rule that governs the whole extension: **WAI's value is determinism** — byte-identical reconstruction, "conformance is a hash." Real @@ -41,38 +60,101 @@ do not belong in this exact-reconstruction extension (§7). Relationship to the core spec: mirrors worlds / score / film. The reconstruct runs in the deterministic floor; `media` takes the informational -value `"quantum"`; `wai.quantum.circuit` is authoritative. Its replicate -class is `StandardDefined` (WAI's own deterministic floor), its fidelity tier -`Lossless` (the simulation analog — the reconstruction is byte-exact). +value `"quantum"`; the envelope's capability names which of the two applies. +Both capabilities have the replicate class `StandardDefined` (WAI's own +deterministic floor) and the fidelity tier `Lossless` (the simulation analog — +the reconstruction is byte-exact). ## 2. Container (`WQC`) ``` -"WQC1" | u16 n_sections | section table (u8 kind, u32 off, u32 len) | sections +magic | u16 n_sections | n_sections × (u8 kind, u32 off, u32 len) | sections ``` -`off` is relative to the start of the sections blob (immediately after the -table). Unknown section kinds MUST be ignored (forward-compatible). +The magic names the capability: `WQC1` is circuit/1 and `WQC2` is circuit2. The +layout is otherwise identical. All integers in the container are little-endian. + +The sections blob starts immediately after the table, at byte +`6 + 9·n_sections`, and `off` is relative to it: a section occupies the bytes +from `blob + off` to `blob + off + len`. A reader computes these bounds without +overflow (in arithmetic wider than 32 bits, or with checked operations) and +refuses a table or a section, of any kind, that runs past the end of the payload. | kind | section | required | |---|---|---| -| `0x01` | contract (canonical JSON) | REQUIRED | -| `0x02` | gate op-log | REQUIRED | -| `0x03` | measurement (canonical JSON) | OPTIONAL | +| `0x01` | contract (canonical JSON, §2.1) | REQUIRED | +| `0x02` | gate op-log (§2.2) | REQUIRED | +| `0x03` | measurement (canonical JSON, §2.3) | OPTIONAL | + +Unknown section kinds MUST be ignored (forward-compatible). A known kind that +appears more than once MUST be refused: two readers could otherwise read +different sections of one payload. The reference writer emits the contract, the +op-log and the measurement, if any, in that order and contiguous from offset 0, +and no other section. + +A reader refuses a payload that breaks a rule of this section or of §2.1–§2.3. +It checks in this order and refuses with the class of the first rule broken: + +1. The payload is at least 6 bytes and starts with `WQC1` or `WQC2`; else + **malformed**. +2. The section table and every section, of a known kind or not, lie within the + payload; no known kind repeats; the contract and the op-log are present. + Else **malformed**. +3. The contract (§2.1); else **contract**, except that an `n_qubits` that is + not an unsigned integer of at most 255 is **malformed**. +4. The op-log parses (§2.2); else **malformed**. +5. The circuit keeps the rules of its capability (§2.2); else **invalid**. +6. The measurement, if present (§2.3); else **malformed**. + +The corpus's `reject/` tree holds one payload for each of these refusals, and +`reject.json` names each one's class. ### 2.1 Contract -```json -{ "ext": "wai.quantum.circuit/1", "n_qubits": 3, - "numeric": "wai.det.fixed64", "frac": 30, "gateset": "cliffordT+dyadicP" } -``` +The canonical contract bytes, used for writing and for `circuit_hash` (§4.1), +have no whitespace and these members in this order. `N` is `n_qubits` in +decimal, `0` to `255`, with no sign, leading zero, fraction or exponent: -`frac` is the amplitude fixed-point scale (`FRAC`, 30 in v1). `gateset` -names the reconstruction's gate algebra (§3). +- circuit/1: + `{"ext":"wai.quantum.circuit/1","n_qubits":N,"numeric":"wai.det.fixed64","frac":30,"gateset":"cliffordT+dyadicP"}` +- circuit2: + `{"ext":"wai.quantum.circuit2/1","n_qubits":N,"numeric":"wai.det.amp64","frac":30,"gateset":"cliffordT+dyadicP.rn"}` + +A writer writes the canonical contract of its capability. + +`ext` is `/`: `wai.quantum.circuit2/1` is revision +1 of the circuit2 capability, not a version "2.1". `frac` is the amplitude +fixed-point scale (`FRAC`, 30 in both). `gateset` names the reconstruction's gate +algebra (§3); circuit2's string differs from circuit/1's because its constants +differ. `numeric` names the arithmetic: + +- `wai.det.amp64`: complex amplitudes, `re` and `im` each an `i64` at scale + `2^frac`. A product of two components is computed exactly and rounded as + `(a·b + 2^(frac−1)) >> frac`, with an arithmetic shift (round half up); sums + are exact (§3.0). +- `wai.det.fixed64`, inside a circuit/1 contract, means the same: the §3.0 + arithmetic at frac 30, with products rounded half up. Those bytes are inside + every circuit/1 `circuit_hash`, so they stay. It does not mean the Q32.32 + floor, with products rounded toward −∞, that + [interactive-worlds](interactive-worlds.md) §6.2 names `wai.det.fixed64`. + +Reading a circuit/1 contract. The section MUST be a JSON object, else contract. +Its `ext`, `numeric` and `gateset` MUST be the strings of the template above and +its `frac` the JSON integer `30` (`30.0` is not); a member that is missing or +carries another value is a contract refusal, so a circuit2 contract in a `WQC1` +container is refused. Then `n_qubits` MUST be an unsigned JSON integer of at most +255, else malformed. Member order, whitespace and further members are free; +`circuit_hash` covers the canonical bytes, not the received ones (§4.1). + +Reading a circuit2 contract. The section MUST be a JSON object, else contract, +whose `n_qubits` is an unsigned JSON integer of at most 255, else malformed. Its +bytes MUST then equal the canonical circuit2 contract for that `n_qubits`, else +contract: no whitespace, no other member order and no further member, and a +circuit/1 contract in a `WQC2` container is refused. ### 2.2 Gate op-log -`n_ops u32`, then per op: +`u32 n_ops`, then per op: ``` u8 opcode | u8 n_ctrl | u8 target | u16 param | n_ctrl × u8 control @@ -80,7 +162,7 @@ u8 opcode | u8 n_ctrl | u8 target | u16 param | n_ctrl × u8 control The op is a **controlled-1-qubit gate**: the base gate `opcode` is applied to `target` iff every qubit in `control` is set. `param` carries the dyadic index -`k` for `P(k)` (0 otherwise). Opcodes: +`k` for `P(k)`. Opcodes: | opcode | gate | | opcode | gate | |---|---|---|---|---| @@ -91,71 +173,246 @@ The op is a **controlled-1-qubit gate**: the base gate `opcode` is applied to | 4 | `H` | | 9 | `P(k) = diag(1, e^{2πi/2^k})` | `CX`/`CZ`/`CP(k)` are these with `n_ctrl = 1`; `CCX` (Toffoli) with -`n_ctrl = 2`; `SWAP` is three `CX`. A target MUST be `< n_qubits`; a control -MUST be `< n_qubits` and `≠ target`; `P(k)` MUST have `1 ≤ k ≤ 32`. +`n_ctrl = 2`; `SWAP` is three `CX`. + +Parsing, in both capabilities: every op lies within the section and its opcode is +at most 9, else malformed. In circuit2 the section MUST end exactly after the +`n_ops`-th op, else malformed; in circuit/1 bytes after it are ignored and are +not part of the circuit. `n_ops` is the payload's claim: every op takes at least +5 bytes, so a section of `len` bytes holds at most `⌊(len − 4)/5⌋` ops, and a +reader reserves no more than that. + +The rules of the circuit, refused as invalid: + +- in both capabilities, a target is `< n_qubits`; a control is `< n_qubits` and + `≠ target`; a gate has at most 255 controls (the wire counts them in a `u8`, + so this binds a writer); `P(k)` has `1 ≤ k ≤ 32`; a circuit has at most + `2^32 − 1` ops; +- in circuit2 also, a gate's controls are pairwise distinct, so there are at + most 254, and `param` is 0 for opcodes 0–8. + +circuit/1 keeps the rules it had: a repeated control is the same condition +twice, and opcodes 0–8 ignore `param`. ### 2.3 Measurement (optional) ```json -{ "seed": 13034510, "shots": 100000, "basis": "computational" } +{"seed":12648430,"shots":100000,"basis":"computational"} ``` -Pins a computational-basis sampling for shot-histogram-equivalence (§4). +Pins a computational-basis sampling for shot-histogram-equivalence (§4.2); the +example is the corpus's bell measurement. +`seed` and `shots` are unsigned 64-bit integers. + +- circuit2: the section MUST be exactly + `{"seed":S,"shots":N,"basis":"computational"}`, with `S` and `N` in decimal + with no sign, leading zero, fraction, exponent or whitespace; else malformed. +- circuit/1: the section MUST be JSON, else malformed. A `seed` or `shots` that + is missing or is not an unsigned integer in the `u64` range reads as 0, and + `basis` is not read. The reference writer writes the circuit2 form. ## 3. Reconstruct contract -The state is `2^n` amplitudes, `amps[|0…0⟩] = 1`, all others `0` at start. Each -op applies its base gate's 2×2 fixed-point matrix `M` to the amplitude pair -`(i, i∨2^target)` for every `i` with the target bit clear **and** all control -bits set: +### 3.0 Arithmetic, for both capabilities -``` -a0' = M00·a0 + M01·a1 a1' = M10·a0 + M11·a1 -``` +An implementer needs all of this, with the phase table of §3.1 or §3.2, to +reproduce a statevector. -Complex products use a pinned round-half-up fixed-point multiply -(`(a·b + 2^(FRAC−1)) >> FRAC`, `i128` intermediate). The one irrational the -Clifford+T set needs — `1/√2` — and the dyadic phases `e^{2πi/2^k}` are built -by an **integer half-angle recurrence** seeded at `(cos π/2, sin π/2) = (0, 1)` -(`cos(θ/2)=√((1+cosθ)/2)`, integer `isqrt`), so **no float ever runs**. -`gateset = "cliffordT+dyadicP"` is universal (Clifford+T is dense in -`SU(2^n)`); the dyadic phase ladder gives the **QFT** ("quantum FFT"), which is -transported as a circuit (`H` + controlled-`P(k)` + swaps) and reconstructs -byte-exact. +- **Amplitude.** `(re, im)`, each an `i64` at scale `2^30`; `2^30` is 1. A unit + below means `2^−30`. +- **Product.** `fx(a, b) = (a·b + 2^29) >> 30`, with `a·b` exact (signed 128-bit + arithmetic holds it) and `>>` an arithmetic shift, that is floor division by + `2^30`. +- **Complex product.** `(a·b).re = fx(a.re, b.re) − fx(a.im, b.im)` and + `(a·b).im = fx(a.re, b.im) + fx(a.im, b.re)`. Each product is rounded + separately; the sums are exact. +- **Initial state.** `2^n` amplitudes, `amps[0] = (2^30, 0)` and every other + `(0, 0)`. Qubit `q` is bit `q` of the basis index, qubit 0 the least + significant. +- **A gate** (base, controls `C`, target `t`, param), for every index `i` with + bit `t` clear and every bit of `C` set, with `j = i | 2^t`, sets + + ``` + a_i' = m00·a_i + m01·a_j a_j' = m10·a_i + m11·a_j + ``` + + from the values before the gate, each product a complex product and each sum + exact. Gates apply in op-log order. +- **Matrices.** With `P(k)` the capability's phase table (§3.1 or §3.2), + `h = re P(3)`, `1 = (2^30, 0)` and `i = (0, 2^30)`: + + | opcode | gate | `m00` | `m01` | `m10` | `m11` | + |---|---|---|---|---|---| + | 0 | `I` | `1` | `0` | `0` | `1` | + | 1 | `X` | `0` | `1` | `1` | `0` | + | 2 | `Y` | `0` | `−i` | `i` | `0` | + | 3 | `Z` | `1` | `0` | `0` | `−1` | + | 4 | `H` | `(h, 0)` | `(h, 0)` | `(h, 0)` | `(−h, 0)` | + | 5 | `S` | `1` | `0` | `0` | `i` | + | 6 | `S†` | `1` | `0` | `0` | `−i` | + | 7 | `T` | `1` | `0` | `0` | `P(3)` | + | 8 | `T†` | `1` | `0` | `0` | `conj P(3)` | + | 9 | `P(k)` | `1` | `0` | `0` | `P(k)` | + +- The capability selects the table and nothing else, and only `H`, `T`, `T†` + and `P(k)` read it. +- `fx(±2^30, x) = ±x` and `fx(0, x) = 0` exactly, so a product with a matrix + entry of `0`, `±1` or `±i` is exact. An implementation may skip such products + (a diagonal gate need touch only `a_j`) without changing a bit. The canonical reconstructed quantity is the amplitude vector serialized as `re_le(i64) ‖ im_le(i64)` per amplitude, in index order. +`cliffordT+dyadicP` and `cliffordT+dyadicP.rn` are universal (Clifford+T is +dense in `SU(2^n)`), and `P(k)` gives the controlled-phase ladder of the QFT +("quantum FFT"), transported as a circuit (`H` + controlled-`P(k)` + swaps) and +reconstructed byte-exact. No float runs on the decode: the phase tables are +integer constants. + +### 3.1 The circuit/1 phase table (frozen) + +`P1(1) = (−2^30, 0)` and `P1(2) = (0, 2^30)`. For `k = 3..32`, with +`c = re P1(k−1)`: + +``` +re P1(k) = isqrt(⌊(2^30 + c) / 2⌋ · 2^30) +im P1(k) = isqrt(⌊(2^30 − c) / 2⌋ · 2^30) +``` + +`isqrt` is the floor square root. Both halves are non-negative, so truncating +division and floor division agree. `H` uses `h = re P1(3) = 759250124`, `T` is +`P1(3)` and `T†` is `conj P1(3)`. + +The table is the one every circuit/1 file, hash and receipt was made with, and it +is kept bit for bit. Its literal values (Appendix B.1) are normative by equality, +and so is its digest, BLAKE3 over `i64le(re) ‖ i64le(im)` for `k = 1..32` +(512 bytes, no domain prefix): +`3fa9805ff96feb819a1cf371fedcd3b5228f93eddf933d0edcf401cbf8c28466`. + +Its floors compound. The stored values deviate from `2^30·e^{2πi/2^k}`, in units: + +- `re P1(k)` is low by up to 1.3 units for every `k ≥ 3`; +- `im P1(k)` is within 2.2 units for `k ≤ 7`, then too large: +9.52 (`k = 8`), + +21.1 (9), +49.0 (10), +90.5 (11), +124.3 (12), +224.4 (13), +112.2 (14), + +1355.6 (15), +677.3 (16), +5283.2 (17, about 10% of the angle) and +7032.1 + (18, about 27%); +- `im P1(19..32) = 0`: these entries apply no phase at all, a deviation of the + whole angle, from −12868 units at `k = 19` to −1.57 at `k = 32`. + +A QFT on `n` qubits uses `P(2..n)`, so in circuit/1 a QFT on 8 or more qubits +applies phases that are off by more than 9 units, and one on 19 or more applies +none for its smallest rotations. + +### 3.2 The circuit2 phase table + +`P2(1) = (−2^30, 0)` and `P2(2) = (0, 2^30)`, both exact. For `k = 3..32`: + +``` +P2(k) = ( ⌊2^30·cos(2π/2^k) + ½⌋ , ⌊2^30·sin(2π/2^k) + ½⌋ ) +``` + +the integers nearest to the ideal values. For `k ≥ 3` every ideal value is +irrational, so none is a half-integer and the nearest integer is unique. The +closest any ideal value comes to a half-integer is 0.00456 unit, at `im P2(22)`, +so any computation of the ideal values accurate to better than that rounds to the +same table. `H` uses `h = re P2(3) = 759250125`, `T` is `P2(3)` and `T†` is +`conj P2(3)`; `re P2(3) = im P2(3)`. + +The literal values (Appendix B.2) are normative by equality, and an +implementation carries them as constants; it need not compute them. Their digest, +computed as for circuit/1, is +`d34cb793715348e7c92db027c0bc3dddb6008f5730470bafaed978ea7fb23510`. + +**The circuit2 table gives the QFT**: every rotation `P(2..32)` of the +controlled-phase ladder is within half a unit per component of its ideal value. +§5 states what that leaves of the error. + +*Derivation (informative).* An exact-integer procedure certifies the table with no +floating point. At scale `2^62` it keeps an enclosure +`c_lo ≤ 2^62·cos(2π/2^k) ≤ c_hi` and `s_lo ≤ 2^62·sin(2π/2^k) ≤ s_hi`, starting +from `(cos, sin)(π/2) = (0, 1)`, which is exact, and halves the angle with +`cos(θ/2) = √((1 + cos θ)/2)` and `sin(θ/2) = sin θ / (2 cos(θ/2))`, which has no +cancellation, rounding every bound outward: + +``` +S = 2^62 +ceil_isqrt(x) = r if r·r = x, else r + 1, where r = isqrt(x) +R(x) = (x + 2^31) >> 32 -- scale 2^62 to 2^30, half up +(c_lo, c_hi, s_lo, s_hi) = (0, 0, S, S) +for k in 3..=32: + c_lo' = isqrt(((S + c_lo)·S) >> 1) + c_hi' = ceil_isqrt(⌈(S + c_hi)·S / 2⌉) + s_lo' = ⌊s_lo·S / (2·c_hi')⌋ + s_hi' = ⌈s_hi·S / (2·c_lo')⌉ + require R(c_lo') = R(c_hi') = re P2(k) + require R(s_lo') = R(s_hi') = im P2(k) + require |(x mod 2^32) − 2^31| > 2^20 for x in {c_lo', c_hi', s_lo', s_hi'} + (c_lo, c_hi, s_lo, s_hi) = (c_lo', c_hi', s_lo', s_hi') +``` + +Every step keeps a true enclosure, and when both ends of an enclosure round to +the same integer, that integer is the correctly rounded value. Every product +fits 125 bits, every enclosure is at most 3 units of `2^−62` wide, and no end +comes within 19,590,902 units of `2^−62` of a tie. The reference implementation +runs this procedure as a test, and the corpus referee runs it independently. + ## 4. Conformance ### 4.1 statevector-equivalence -> Given the same `wai.quantum.circuit`, a conforming sink MUST reconstruct the -> identical fixed-point statevector, hence the identical BLAKE3 — on every -> machine, no tolerance parameter. +> Given the same circuit under the same capability, a conforming sink MUST +> reconstruct the identical fixed-point statevector, hence the identical +> BLAKE3 — on every machine, no tolerance parameter. ``` statevector_hash = BLAKE3("wai:quantum-statevector\x01" - || n_qubits || frac_le || canonical amps) + ‖ u8 n_qubits ‖ u32le frac + ‖ per amplitude, in index order: i64le re ‖ i64le im) circuit_hash = BLAKE3("wai:quantum-circuit\x01" - || contract_bytes || oplog_bytes) + ‖ canonical contract (§2.1) of the capability for n_qubits + ‖ canonical op-log) ``` -`circuit_hash` is the frame-independent identity of the circuit; each sink's -`statevector_hash` is the verifiable result. +The canonical op-log is the §2.2 encoding of the circuit's ops: `u32le n_ops`, +then each op, and nothing after. For circuit2 the canonical contract and op-log +are the bytes received, since the reader requires them. For circuit/1 the +contract is the re-rendered template, however the received one was formatted, +and bytes after the last op are not part of the op-log. + +`circuit_hash` is the identity of the circuit, and its contract names the +capability, so it binds the capability and its phase table: the same ops under +circuit/1 and circuit2 have different circuit hashes. `statevector_hash` hashes +the amplitudes alone and names no capability: the two capabilities give the same +statevector, and so the same hash, for a circuit in which no gate that reads the +table acts on a nonzero amplitude (one of `X` gates only, say). A run is +therefore identified by its `circuit_hash`, and each sink's `statevector_hash` +is the verifiable result. ### 4.2 shot-histogram-equivalence (measured circuits) For a circuit with a §2.3 measurement, a conforming sink MUST sample the -computational-basis outcome with the pinned `splitmix64` PRNG seeded by `seed` -(walking the un-normalized `|amp|²` prefix sums — no divide), producing an -identical per-basis-state count vector, hence: +computational-basis outcome as follows, producing an identical per-basis-state +count vector: + +- The weight of basis state `i` is `w_i = re_i² + im_i²`, exact; `W` is their + sum, and `cum_i` the sum of `w_0..w_i`. The weights are not normalised. +- `splitmix64` with state `s`, starting at `seed`, all arithmetic mod `2^64`: + `s ← s + 0x9E3779B97F4A7C15`; `z ← s`; + `z ← (z ⊕ (z >> 30))·0xBF58476D1CE4E5B9`; + `z ← (z ⊕ (z >> 27))·0x94D049BB133111EB`; the draw is `z ⊕ (z >> 31)`. +- Each of the `shots` shots draws `x`, takes `r = x mod W`, and counts the first + basis state `i` with `cum_i > r`. If `W` is 0, every count is 0. ``` histogram_hash = BLAKE3("wai:quantum-histogram\x01" - || n_qubits || seed_le || shots_le || counts_le) + ‖ u8 n_qubits ‖ u64le seed ‖ u64le shots + ‖ per basis state, in index order: u64le count) ``` +`seed`, `shots` and every count are `u64le`; `n_qubits` is a `u8` prefix. The +histogram depends only on the statevector and the measurement, so it names no +capability either. + ## 5. Fidelity and size (honest) Two honesties the format states rather than implies: @@ -163,22 +420,48 @@ Two honesties the format states rather than implies: 1. **The reconstruction is exact; the model it reconstructs is an approximation.** The statevector is byte-identical across sinks (statevector-equivalence), but it is a *fixed-point* approximation of the - ideal complex-amplitude unitary evolution — each gate carries a bounded - rounding error (`≈ 2^−FRAC` per op, accumulating roughly linearly). This is - the same call as `wai.video.int_motion`: byte-exact reconstruction of a - defined-lossy quantity. The tier is therefore `Lossless` in the *simulation* - sense (like world replay), and the per-gate error bound is the contract, not - a hidden claim. + ideal complex-amplitude unitary evolution. This is the same call as + `wai.video.int_motion`: byte-exact reconstruction of a defined-lossy quantity. + The tier is therefore `Lossless` in the *simulation* sense (like world + replay), and the error bound is part of the contract, not a hidden claim. + Per capability: + + - **circuit2.** Every table constant is within ½ unit of its ideal value, + which §3.2 proves, and every product within ½ unit of the exact product. + One gate applied to a pair of amplitudes whose squared moduli sum to at + most 1 therefore lands within 2 units per component of the ideal gate + applied to the same pair: each component takes at most two rounded + products, ½ unit each, and the constants' error of ½ unit each meets + components whose squares sum to at most 1, which gives `1 + √2/2 ≈ 1.71`. + The worst measured on random normalised pairs is under 1.5 units. Errors + accumulate roughly linearly in the number of gates. Some table entries have + a modulus slightly above 1, by at most 0.3603 unit (`||P2(k)|² − 1|` is at + most 0.73 unit), so a long run of one phase drifts the norm upward, at the + order of the product rounding: `P2(19)` applied `2^18` times to `|1⟩`, a + half turn, ends at modulus 1.0000189 and within `10^−5` rad of the half + turn. The resolution of `2^−30` is a floor no rounding removes: + `P2(26..32)` carry imaginary parts of 101 down to 2, so `P2(32)` is correct + to the nearest unit (ideal 1.5708, stored 2) and 27% off in relative angle. + A finer resolution needs a new capability. + - **circuit/1.** The same arithmetic with the constants of §3.1, which are + off by up to 12868 units, and `P1(19..32)` apply no phase. The bound above + does not hold: one `P1(k)` errs by as much as its constant does. `P1(19)` + applied `2^18` times to `|1⟩` ends at exactly `(1073479680, 0)`: each step + loses a unit and none rotates. + + A QFT on `n` qubits needs `P(2..n)`, so a 26-qubit QFT needs `P(19..26)` and + is circuit2-only. 2. **The state is exponential; the circuit is not.** A statevector is `2^n` amplitudes × 16 bytes — the reconstruction is heavy and grows exponentially. The *transportable* object is the circuit op-log, which is - linear in gate count (the reference corpus's QFT-5 circuit is 261 bytes and - reconstructs a 32-amplitude state; a 26-qubit circuit is still a few kilobytes - and reconstructs ~1 GiB). The win is exactly WAI's thesis — the wire carries - instructions, the sink reconstructs — and it is *why* the receipt matters: - because reconstruction cost is exponential, an energy-metered, byte-exact, - signed record of the computation is a genuinely novel artifact. + linear in gate count (the reference corpus's QFT-5 circuit is 261 bytes as + circuit/1 and 263 as circuit2, and reconstructs a 32-amplitude state; a + 26-qubit QFT is still a few kilobytes and reconstructs ~1 GiB). The win is + exactly WAI's thesis — the wire carries instructions, the sink + reconstructs — and it is *why* the receipt matters: because reconstruction + cost is exponential, an energy-metered, byte-exact, signed record of the + computation is a genuinely novel artifact. This extension is a way to make a quantum computation portable and verifiable. It is not a media codec and it is not a route to quantum advantage. @@ -192,7 +475,7 @@ is trusted the way it honestly can be.** | field | what it is | how it is trusted | |---|---|---| -| `circuit_hash` | identity — the `WQC` circuit's hash | re-checkable | +| `circuit_hash` | identity — the `WQC` circuit's hash, which names its capability | re-checkable | | `statevector_hash` | the byte-exact reconstruction result | **re-checkable** (re-simulate) | | `measurement` | optional `{seed, shots, histogram_hash}` | re-checkable | | `work_amp_updates` | `n_ops · 2^n` — the exponential reconstruction cost | **verified by recomputation** | @@ -209,6 +492,9 @@ over a Merkle root of the leaf hashes (`circuit`, `statevector`, `[histogram]`) the worlds / provenance / film receipt shape reused, `parent_receipt_hash` chaining a derivation exactly as they do. +The receipt format is the same for both capabilities. A receipt names its +capability through `circuit_hash`, whose contract names it (§4.1). + Two verification depths: - `verify()` — Merkle root recomputes, `work == n_ops · 2^n`, signature valid. @@ -220,6 +506,15 @@ Two verification depths: attested-only — re-simulation checks the *result and cost*, never the signer's energy draw.) +`verify_reconstruction` re-simulates the circuit under its own capability: the +circuit as decoded from its container, whose magic names it. A receipt sealed +under one capability therefore does not verify against the same ops under the +other. A receipt sealed before circuit2 existed (by `wai-quantum` 0.3.x) is a +circuit/1 receipt: re-verify it with its `WQC1` file, or with the circuit built +in circuit/1 (`Gateset::V1`). The reference's `gateset_of` reports under which +capability a circuit's ops match a receipt's `circuit_hash`; it explains a failed +reconstruction and decides nothing. + `to_json()` / `from_json()` give the portable, sorted-key JSON an out-of-band verifier reads. `wai_quantum_meter [receipt.json]` fills the energy honestly: on a host that exposes its SoC's power interface to @@ -238,9 +533,11 @@ measured figure the meter writes a measurement report (energy-measurement §6). verified with `Labelled::verify` / `Labelled::verify_reconstruction`. The `wai-quantum verify` CLI and the HTTP handler's `POST /verify` accept a receipt in either form (`quantum_energy::MaybeLabelled`) and restate its class. A receipt -sealed without a label keeps its legacy bytes and its figure is unlabelled. A -representative run on the reference QFT-5 vector: **≈43 µJ measured per -reconstruction, `work_amp_updates = 672`** (21 ops × 2⁵), sealed and self-verified. +sealed without a label keeps its legacy bytes and its figure is unlabelled. The +meter decodes the circuit from its container, so the receipt binds the capability +the file names. A representative run on the reference QFT-5 vector: **≈43 µJ +measured per reconstruction, `work_amp_updates = 672`** (21 ops × 2⁵), sealed and +self-verified. Because a quantum computation carries no speedup here, this is exactly the value the standard adds: an energy-accounted, byte-exact, signed record whose *result* @@ -261,7 +558,45 @@ stochastic. Such a prior is `NeuralFloat`/`Behavioral`: it MUST NOT back `wai.semantic.*` conforms by a CLIP-similarity threshold. Naming the metric now fixes the axis before that capability is ever registered. -## Appendix +## 8. Versions + +**Why circuit2 is a capability of its own.** The circuit/1 table comes from a +recurrence whose floors compound (§3.1). Correcting it under the same string +would change the payload format of a registered capability, which SPEC §8 +forbids: once a capability string is registered, its payload format is fixed +forever, and a later change never widens an existing string. It would also +change the reconstruction, the `circuit_hash` and every receipt of the content +made with it. So the corrected table is a new capability, `wai.quantum.circuit2`, +with a magic of its own, and circuit/1 stays as it was. + +**What a reader that predates circuit2 does.** It requires the magic `WQC1`, so +it refuses a `WQC2` payload at the magic, before it reads anything else. It does +not advertise `wai.quantum.circuit2`, so an envelope that names that capability +is inert to it (SPEC §4). It recomputes `circuit_hash` with the circuit/1 +contract, so a circuit2 receipt does not verify there. Each of these fails +closed: no older reader reconstructs circuit2 content with the circuit/1 table. + +**Payloads this revision makes non-conforming.** This revision states reader +rules for circuit/1 that enforce its registration as it was made (SPEC §8): the +contract's values (§2.1), the range of `n_qubits`, sections within the payload, +and one section of each known kind (§2). Earlier reference readers accepted +payloads that break them, and a reader of this revision refuses: + +- a `WQC1` contract whose `ext`, `numeric`, `frac` or `gateset` is missing or + carries another value, a circuit2 contract among them; +- an `n_qubits` that is not an unsigned integer of at most 255 (an earlier + reader read 258 as 2); +- a known section kind that appears more than once (an earlier reader kept the + last); +- a section whose bounds overflow (an earlier reader could wrap on a 32-bit + target). + +No file written by any reference version is affected: every reference writer +wrote the canonical contract, one section of each kind, and sections within the +payload. The reference implementation adds circuit2 in `wai-quantum` 0.4.0, where +a circuit built in code is circuit2 unless it is built as circuit/1. + +## Appendix A — One floor, one more kind of state statevector-equivalence is replay-equivalence with the world state replaced by a quantum amplitude vector, and the deterministic floor extended from real @@ -271,3 +606,94 @@ cloud reconstruct now makes a quantum state reconstruct — one floor, one more cargo class, one verification. The state is heavy and the model approximate; the *reconstruction* of it is exact and auditable, and that — not a quantum speedup WAI does not claim — is the honest thing the standard adds. + +## Appendix B — The phase tables + +`P(k)` as `(re, im)` at scale `2^30`, for `k = 1..32`, with each table's digest: +BLAKE3 over `i64le(re) ‖ i64le(im)` for `k = 1..32`, 512 bytes, no domain prefix. +The tables and digests below are rendered from the reference implementation's +constants (`PHASE_V1`, `PHASE_V2`) by `wai_quantum_vectors appendix`, and a test +fails when this text differs from what the constants render. B.1 is circuit/1's +table (§3.1) and B.2 is circuit2's (§3.2). + + + +### B.1 `wai.quantum.circuit` + +| k | re P(k) | im P(k) | +|---:|---:|---:| +| 1 | -1073741824 | 0 | +| 2 | 0 | 1073741824 | +| 3 | 759250124 | 759250124 | +| 4 | 992008094 | 410903207 | +| 5 | 1053110175 | 209476638 | +| 6 | 1068571463 | 105245102 | +| 7 | 1072448454 | 52686012 | +| 8 | 1073418432 | 26350953 | +| 9 | 1073660972 | 13176485 | +| 10 | 1073721610 | 6588405 | +| 11 | 1073736770 | 3294284 | +| 12 | 1073740560 | 1647223 | +| 13 | 1073741507 | 823774 | +| 14 | 1073741744 | 411887 | +| 15 | 1073741803 | 207243 | +| 16 | 1073741818 | 103621 | +| 17 | 1073741822 | 56755 | +| 18 | 1073741823 | 32768 | +| 19 | 1073741823 | 0 | +| 20 | 1073741823 | 0 | +| 21 | 1073741823 | 0 | +| 22 | 1073741823 | 0 | +| 23 | 1073741823 | 0 | +| 24 | 1073741823 | 0 | +| 25 | 1073741823 | 0 | +| 26 | 1073741823 | 0 | +| 27 | 1073741823 | 0 | +| 28 | 1073741823 | 0 | +| 29 | 1073741823 | 0 | +| 30 | 1073741823 | 0 | +| 31 | 1073741823 | 0 | +| 32 | 1073741823 | 0 | + +Digest: `3fa9805ff96feb819a1cf371fedcd3b5228f93eddf933d0edcf401cbf8c28466` + +### B.2 `wai.quantum.circuit2` + +| k | re P(k) | im P(k) | +|---:|---:|---:| +| 1 | -1073741824 | 0 | +| 2 | 0 | 1073741824 | +| 3 | 759250125 | 759250125 | +| 4 | 992008094 | 410903207 | +| 5 | 1053110176 | 209476638 | +| 6 | 1068571464 | 105245103 | +| 7 | 1072448455 | 52686014 | +| 8 | 1073418433 | 26350943 | +| 9 | 1073660973 | 13176464 | +| 10 | 1073721611 | 6588356 | +| 11 | 1073736771 | 3294193 | +| 12 | 1073740561 | 1647099 | +| 13 | 1073741508 | 823550 | +| 14 | 1073741745 | 411775 | +| 15 | 1073741804 | 205887 | +| 16 | 1073741819 | 102944 | +| 17 | 1073741823 | 51472 | +| 18 | 1073741824 | 25736 | +| 19 | 1073741824 | 12868 | +| 20 | 1073741824 | 6434 | +| 21 | 1073741824 | 3217 | +| 22 | 1073741824 | 1608 | +| 23 | 1073741824 | 804 | +| 24 | 1073741824 | 402 | +| 25 | 1073741824 | 201 | +| 26 | 1073741824 | 101 | +| 27 | 1073741824 | 50 | +| 28 | 1073741824 | 25 | +| 29 | 1073741824 | 13 | +| 30 | 1073741824 | 6 | +| 31 | 1073741824 | 3 | +| 32 | 1073741824 | 2 | + +Digest: `d34cb793715348e7c92db027c0bc3dddb6008f5730470bafaed978ea7fb23510` + + diff --git a/wai/quantum-conformance/README.md b/wai/quantum-conformance/README.md index 5aba94b8f..49cb31eb2 100644 --- a/wai/quantum-conformance/README.md +++ b/wai/quantum-conformance/README.md @@ -1,12 +1,21 @@ # quantum-conformance -Statevector-equivalence for the `wai.quantum.circuit` extension: given the same -`WQC` circuit, a conforming sink MUST reconstruct the identical fixed-point -statevector — **hash equality, no tolerance**. +Statevector-equivalence for both capabilities of the quantum-sim extension +(`../extensions/quantum-sim.md`): given the same `WQC` circuit, a conforming sink +MUST reconstruct the identical fixed-point statevector — **hash equality, no +tolerance**. **Provenance.** Generated here by `wai_quantum_vectors` from circuits authored in code. No media. -`vectors/*.wqc` plus `expected_hashes.json` are the pinned corpus. They are the +Three trees, each a pinned corpus: + +| tree | capability | what | +|---|---|---| +| `vectors/` + `expected_hashes.json` | `wai.quantum.circuit` (`WQC1`) | bell, ghz3, qft3 and qft5, frozen as they were first written, then qft5_x22, dyadic_ladder and qft8_ones | +| `v2/vectors/` + `v2/expected_hashes.json` | `wai.quantum.circuit2` (`WQC2`) | the same seven circuits; each entry names its `capability` | +| `reject/` + `reject/reject.json` | both | payloads a conforming reader refuses, each with the class it is refused as: `malformed`, `contract` or `invalid` (extension §2) | + +An entry with no `capability` is a `wai.quantum.circuit` vector. The trees are the one reference all three surfaces are checked against, rather than being checked against each other. @@ -54,15 +63,30 @@ python3 -I wai/quantum-conformance/tools/referee.py wai/quantum-conformance A second implementation of the extension in Python, integers only, sharing no code with the Rust reference. It derives both phase tables (circuit/1 by its recurrence, circuit2 by an interval-certified procedure) and requires their literal values and -digests; rebuilds every vector file byte for byte and recomputes every manifest -entry; refuses every reject file with its listed class and by the rule its name -states; and requires `dyadic_ladder` to change its statevector hash under every ±1 -change of every phase-table entry, in both tables. CI runs it on Linux -(`wai-conformance.yml`, job `independent-verifiers`). +digests, and the extension's Appendix B to state the same; rebuilds every vector +file byte for byte and recomputes every manifest entry; refuses every reject file +with its listed class and by the rule its name states; and requires +`dyadic_ladder` to change its statevector hash under every ±1 change of every +phase-table entry, in both tables. CI runs it on Linux (`wai-conformance.yml`, +job `independent-verifiers`). ## Scope -The corpus covers the **byte-exact dyadic core**, where hash equality is the right -bar. The floating-point layers (`quantum_source`, `quantum_channel`) are held to +The corpus pins the container, the reader's refusals, the fixed-point arithmetic +and both phase tables, where hash equality is the right bar. What it covers, and +where it stops: + +- **Every phase-table entry.** Under either table, a change of ±1 to either + component of any `P(1..32)`, with `H` following `re P(3)`, changes + `dyadic_ladder`'s statevector hash; the referee checks this on every run. A + change of ±1 to `H` alone changes the hash of every vector that applies `H`. +- **The four original vectors see no `P(k)`.** They run the QFT from + `|0…0⟩`, where every controlled phase acts on zero amplitudes, so no `P(k)` change + moves their hashes; the three vectors after them are there for that. +- **One control at most.** Gates with more than one control appear only in the + reject tree; no vector applies one. +- **One measurement.** The shot sampler is pinned by bell's measurement alone. + +The floating-point layers (`quantum_source`, `quantum_channel`) are held to exact IEEE-754 bits by unit tests instead, since they have no `WQC` circuit to pin — see `entropy_is_bit_identical_across_architectures`. diff --git a/wai/quantum-conformance/tools/referee.py b/wai/quantum-conformance/tools/referee.py index ede11b41a..be9b0aae9 100644 --- a/wai/quantum-conformance/tools/referee.py +++ b/wai/quantum-conformance/tools/referee.py @@ -15,8 +15,9 @@ It checks, in this order, and reports every failure rather than stopping at one: by an interval-certified procedure in exact integers (§3.2): each step keeps a true enclosure of 2^62·cos(2π/2^k) and 2^62·sin(2π/2^k), and both ends of every enclosure round to the same integer far from a tie, so that integer is the - nearest one. Each derived table must equal its literal table (Appendix B, restated - below) and its BLAKE3 digest. + nearest one. Each derived table must equal its literal table (restated below) and + its BLAKE3 digest, and the extension's Appendix B must state those same tables and + digests. 3. The vector trees: `./` (circuit/1) and `v2/` (circuit2). Every file is rebuilt byte for byte from the circuit definitions below and read back by the reader here, and every manifest entry must equal the entry computed here: n_qubits, n_ops, the @@ -31,8 +32,9 @@ It checks, in this order, and reports every failure rather than stopping at one: It was written from the same reading of the text as the Rust reference, so a misreading the two share would pass both. - python3 -I referee.py [quantum-conformance dir] + python3 -I referee.py [quantum-conformance dir [quantum-sim.md]] +The extension defaults to ../extensions/quantum-sim.md beside the corpus directory. Exit status 0 only when every check passes. """ import json @@ -655,6 +657,68 @@ def check_tables(rep): rep.check("circuit2 table digest", None if d2 == PHASE_V2_DIGEST else "got " + d2) +APPENDIX_BEGIN = "" +APPENDIX_END = "" + + +def read_appendix(path): + """Appendix B of the extension as {subsection: (capability, table, digest)}, or the + reason it cannot be read. A subsection is a `### B.n `capability`` heading, rows + `| k | re | im |`, and a `Digest: `hex`` line.""" + try: + with open(path, encoding="utf-8") as f: + text = f.read() + except OSError as e: + return None, str(e) + begin, end = text.find(APPENDIX_BEGIN), text.find(APPENDIX_END) + if begin < 0 or end < begin: + return None, "no generated Appendix B" + out, current = {}, None + for line in text[begin + len(APPENDIX_BEGIN):end].splitlines(): + line = line.strip() + if line.startswith("### B."): + sub, _, cap = line[4:].partition(" ") + current = sub + out[sub] = (cap.strip("`"), [None], None) + elif current and line.startswith("| ") and line[2:3].isdigit(): + try: + k, re_, im = (int(c.strip()) for c in line.strip("|").split("|")) + except ValueError: + return None, "%s: unreadable row %r" % (current, line) + table = out[current][1] + if k != len(table): + return None, "%s: row k = %d out of order" % (current, k) + table.append((re_, im)) + elif current and line.startswith("Digest: `"): + out[current] = (out[current][0], out[current][1], line[len("Digest: `"):].rstrip("`")) + return out, None + + +def check_appendix(rep, path): + """The extension's Appendix B states the literal tables and digests above.""" + appendix, why = read_appendix(path) + if appendix is None: + rep.check("extension Appendix B (%s)" % path, why) + return + for sub, gs, digest in (("B.1", V1, PHASE_V1_DIGEST), ("B.2", V2, PHASE_V2_DIGEST)): + got = appendix.get(sub) + if got is None: + rep.check("extension Appendix %s" % sub, "missing") + continue + cap, table, stated = got + problems = [] + if cap != gs.capability: + problems.append("names %s" % cap) + if table != gs.table: + problems.append("k = %s differ" % [k for k in range(1, DYADIC_MAX + 1) + if k >= len(table) or table[k] != gs.table[k]]) + if stated != digest: + problems.append("digest %s" % stated) + rep.check("extension Appendix %s is the %s table and digest" % (sub, gs.label), "; ".join(problems) or None) + extra = sorted(set(appendix) - {"B.1", "B.2"}) + rep.check("extension Appendix B has no other table", "has %s" % extra if extra else None) + + def read_object(path): """A JSON object from `path`, or the reason it is not one.""" try: @@ -781,9 +845,11 @@ def check_mutations(rep): def main(): root = sys.argv[1] if len(sys.argv) > 1 else os.path.join(os.path.dirname(os.path.abspath(__file__)), "..") + extension = sys.argv[2] if len(sys.argv) > 2 else os.path.join(root, "..", "extensions", "quantum-sim.md") rep = Report() check_blake3(rep) check_tables(rep) + check_appendix(rep, extension) check_tree(rep, root, "", V1, False) check_tree(rep, root, "v2", V2, True) check_rejects(rep, root) diff --git a/wai/registry/capabilities.json b/wai/registry/capabilities.json index 3e8d28181..ca312240b 100644 --- a/wai/registry/capabilities.json +++ b/wai/registry/capabilities.json @@ -1451,10 +1451,15 @@ } ], "identifiers": [ + { + "identifier": "wai.det.amp64", + "spec_section": "§5 Interactive, temporal & computed content", + "meaning": "the complex fixed-point arithmetic of wai.quantum.circuit2: re and im each an i64 at scale 2^frac, products exact then rounded half up, sums exact (quantum-sim §2.1, §3.0)" + }, { "identifier": "wai.det.fixed64", "spec_section": "§5 Interactive, temporal & computed content", - "meaning": "the fixed-point arithmetic floor the deterministic *-equivalence criteria are computed in" + "meaning": "the fixed-point arithmetic floor the deterministic *-equivalence criteria are computed in (Q32.32, interactive-worlds §6.2); inside a wai.quantum.circuit contract it names the quantum-sim §3.0 arithmetic at frac 30, products rounded half up" }, { "identifier": "wai.world.sim.feed0", diff --git a/wai/wai-quantum/README.md b/wai/wai-quantum/README.md index a10b06a3a..dba47d4f0 100644 --- a/wai/wai-quantum/README.md +++ b/wai/wai-quantum/README.md @@ -10,7 +10,7 @@ the edge, producing **byte-identical results and receipts on all three**. ```toml [dependencies] -wai-quantum = "0.3" +wai-quantum = "0.4" ``` ```rust @@ -54,7 +54,7 @@ wai-quantum qutrit # d = 3, not just qubits `channel` prints the Holevo bound before and after, and it can only fall: noise never creates information. -| `quantum` | byte-exact statevector simulation (dyadic Clifford+T+P(k)) | +| `quantum` | byte-exact statevector simulation (dyadic Clifford+T+P(k)), as `wai.quantum.circuit2` by default or as the frozen `wai.quantum.circuit` | | `quantum_toolchain` | algorithm library, backend recommendation, **OpenQASM 2/3 ingest of what the mainstream toolchains actually export** — both register and both measurement spellings, `rx`/`ry`/`rz`/`sx`/`u`/`u2`/`u3`/`crz`, and exact rational-multiple-of-pi angles (non-dyadic angles are refused, never rounded) — Bloch / entanglement / purity analysis | | `quantum_stabilizer` | stabilizer (CHP) tableau — `O(n²)`, scales far past statevector | | `quantum_mps` | tensor-network (matrix-product-state) backend | @@ -439,6 +439,16 @@ that four of these layers once gave different bits natively and as a WASI component. `tests/bit_identity.rs` pins one result from each, so any platform whose arithmetic differs fails there. +A circuit carries the `Gateset` its phases come from, and its `circuit_hash` +names it, so the same gates in the two gatesets are different circuits. +`Circuit::new` and `Circuit::qft` build `wai.quantum.circuit2` (`Gateset::V2`, +container magic `WQC2`), whose table holds the integers nearest to each +`e^{2πi/2^k}`. `wai.quantum.circuit` (`Gateset::V1`, `WQC1`) is kept bit for bit +so that every file, hash and receipt made with it reproduces; its table deviates +from the ideal phase for `P(k ≥ 8)` and applies none for `k ≥ 19`. A circuit +read from a container takes the container's gateset, and one derived from +another keeps it. + A receipt separates the two halves of a cost honestly: **work** is portable-exact (`n_ops · 2^n` amplitude updates, recomputed by the verifier, so a sink cannot inflate it) and **energy** is measured-attested (only the signer can @@ -456,6 +466,23 @@ its calibration in either form. A label's declared uncertainty must be at least the `0.5/√3` µJ (≈ 0.289 µJ) that rounding the figure to whole microjoules adds: `joules_micro × relative_ppm` of at least 288 676. +## Upgrading from 0.3 + +- `Circuit::new` builds `Gateset::V2`. To reproduce or verify an artefact made + with 0.3, read its `WQC1` file, or set `c.gateset = Gateset::V1` on a circuit + built in code. +- `quantum::phase`, `quantum::inv_sqrt2` and `quantum::CAPABILITY` are removed; + `Gateset::phase`, `Gateset::inv_sqrt2` and `Gateset::capability` replace them. +- `to_wqc`, `circuit_hash` and `quantum_toolchain::circuit_id` return a `Result`. + They fail only on a circuit its gateset does not allow, so never on one that + has simulated. +- `Circuit` and `QuantumError` are `#[non_exhaustive]`, and `QuantumError` has a + `Contract` variant and a `kind()`. +- `quantum_fabric::certify_lanes` and `lanes::random_circuit` take a gateset. +- `from_wqc` refuses the circuit/1 payloads extensions/quantum-sim §8 lists; + `validate` refuses a gate with more than 255 controls, and a receipt's + `from_json` a qubit or op count its fields cannot hold. + ## Also available - A CLI (`wai-quantum` in the [`wai`](https://crates.io/crates/wai) crate) — run diff --git a/wai/wai-quantum/src/quantum.rs b/wai/wai-quantum/src/quantum.rs index ce9105118..731722ed8 100644 --- a/wai/wai-quantum/src/quantum.rs +++ b/wai/wai-quantum/src/quantum.rs @@ -1,5 +1,6 @@ //! Deterministic quantum-circuit **simulation** as transportable, receiptable -//! content — `wai.quantum.circuit` (extensions/quantum-sim). +//! content — `wai.quantum.circuit2` and the frozen `wai.quantum.circuit` +//! (extensions/quantum-sim). //! //! The framing first: WAI's core property is *determinism* — byte-identical //! reconstruction on every machine, "conformance is a hash". Real quantum @@ -10,12 +11,12 @@ //! compact **circuit** (a gate op-log), the sink CLASSICALLY simulates it, and the //! reconstructed statevector hashes identically everywhere. //! -//! The reconstruction runs in a fixed-point complex floor (the `wai.det.fixed64` -//! discipline applied to amplitudes): every amplitude is an [`Amp`] — real and -//! imaginary parts as `i64` at scale `2^FRAC` — and every gate is an integer -//! fixed-point matrix–vector update with a pinned rounding rule and a *stated -//! error bound* versus the ideal unitary (the same contract as -//! the WAI integer transform floor). No float anywhere on the decode, so: +//! The reconstruction runs in a fixed-point complex floor (`wai.det.amp64`, the +//! arithmetic a circuit/1 contract calls `wai.det.fixed64`): every amplitude is +//! an [`Amp`] — real and imaginary parts as `i64` at scale `2^FRAC` — and every +//! gate is an integer fixed-point matrix–vector update with a pinned rounding +//! rule and a *stated error bound* versus the ideal unitary (the same contract +//! as the WAI integer transform floor). No float anywhere on the decode, so: //! //! - **statevector-equivalence** — the whole amplitude vector is byte-identical on //! every machine (a portable statevector hash), and @@ -32,10 +33,20 @@ //! //! Gate set: a *controlled-1-qubit-gate* IR over Clifford+T + dyadic phase //! (`I,X,Y,Z,H,S,S†,T,T†,P(k)` with any number of controls). Clifford+T is dense -//! in `SU(2^n)`, so this is universal; the dyadic phase `P(k) = e^{2πi/2^k}` gives -//! the controlled-phase ladder the **QFT** ("quantum FFT") needs, transported as a -//! circuit and reconstructed byte-exact. Pure i64, no float, no `ort`; compiles to -//! wasm32. +//! in `SU(2^n)`, so this is universal. The dyadic phase `P(k) = e^{2πi/2^k}` is +//! read from the circuit's [`Gateset`] table: +//! +//! - [`Gateset::V2`] (`wai.quantum.circuit2`, magic `WQC2`) holds the integers +//! nearest to each phase, so its controlled-phase ladder gives the **QFT** +//! ("quantum FFT"), transported as a circuit and reconstructed byte-exact. +//! Circuits built in code are V2 unless built otherwise. +//! - [`Gateset::V1`] (`wai.quantum.circuit`, magic `WQC1`) is frozen so that +//! every circuit/1 file, hash and receipt reproduces. Its `P(k)` deviates from +//! the ideal phase for `k ≥ 8` and applies none for `k ≥ 19`. +//! +//! A circuit decoded from a container takes the container's gateset, and its +//! [`circuit_hash`] names the capability. Pure i64, no float, no `ort`; +//! compiles to wasm32. //! //! Out of scope (and honestly so): variational-quantum-circuit / "quantum AI" //! priors whose measurement is stochastic — those are at best a float/behavioral @@ -143,7 +154,8 @@ impl Amp { /// the imaginary part is too large, by up to 7032 units at `P(18)`, and /// `P(19..=32)` apply no phase at all. The table is kept exactly as it is so that /// every circuit/1 file, hash and receipt reproduces. [`PHASE_V2`] is the -/// correctly rounded table. +/// correctly rounded table. The extension states this table in its §3.1 and +/// Appendix B.1, rendered from this constant. pub const PHASE_V1: [Amp; 33] = [ Amp::ZERO, Amp { re: -1073741824, im: 0 }, // k = 1 @@ -189,7 +201,8 @@ pub const PHASE_V1: [Amp; 33] = [ /// half-integer and the nearest integer is unique; the closest any comes to a /// tie is 0.00456 unit, at the imaginary part of `P(22)`. Every component is /// within ½ unit of the ideal, which the crate's tests prove with integer -/// interval arithmetic. The literal values are normative. +/// interval arithmetic. The literal values are normative: the extension +/// states them in its §3.2 and Appendix B.2, rendered from this constant. /// /// `re P(3) == im P(3)`, so `T` keeps `re == im`. The resolution of `2^-30` is a /// floor no rounding removes: `P(26..=32)` carry imaginary parts of 101 down to diff --git a/wai/wai-rs/src/bin/wai_quantum_vectors.rs b/wai/wai-rs/src/bin/wai_quantum_vectors.rs index 58a6e1ee1..ef8cf8b82 100644 --- a/wai/wai-rs/src/bin/wai_quantum_vectors.rs +++ b/wai/wai-rs/src/bin/wai_quantum_vectors.rs @@ -20,11 +20,15 @@ //! `verify` checks all three: each vector decodes to the capability its entry //! names and reproduces its hashes, and each reject file fails with its class. //! -//! wai_quantum_vectors gen | verify +//! `appendix` writes the phase tables of the extension's Appendix B, rendered +//! from the reference constants, into the generated part of the file it is +//! given (`extensions/quantum-sim.md`), so the tables are never typed by hand. +//! +//! wai_quantum_vectors gen | verify | appendix use std::collections::BTreeSet; use std::path::Path; -use wai::quantum::{circuit_hash, from_wqc, to_wqc, BaseGate, Circuit, Gate, Gateset, Measure}; +use wai::quantum::{circuit_hash, from_wqc, to_wqc, BaseGate, Circuit, Gate, Gateset, Measure, DYADIC_MAX}; /// The circuit2 tree, under the corpus directory. const V2_DIR: &str = "v2"; @@ -45,8 +49,9 @@ fn main() { } println!("all vectors pass: statevector-equivalence holds, and every reject file is refused as listed"); } + (Some("appendix"), Some(f)) => write_appendix(Path::new(f)), _ => { - eprintln!("usage: wai_quantum_vectors "); + eprintln!("usage: wai_quantum_vectors | wai_quantum_vectors appendix "); std::process::exit(2); } } @@ -315,6 +320,54 @@ fn generate(dir: &Path) { println!("generated quantum vectors into {}", dir.display()); } +// ---- Appendix B of the extension --------------------------------------------------- + +/// The markers around the generated part of the extension's Appendix B. +const APPENDIX_BEGIN: &str = ""; +const APPENDIX_END: &str = ""; + +/// The phase tables of Appendix B, rendered from the reference constants: for +/// each gateset, oldest first, a subsection `B.` named by its capability, +/// `P(1..=DYADIC_MAX)` as `(re, im)`, and the table's digest. +fn appendix_tables() -> String { + let mut o = String::from("\n"); + for (i, gs) in Gateset::ALL.into_iter().enumerate() { + o.push_str(&format!("### B.{} `{}`\n\n", i + 1, gs.capability())); + o.push_str("| k | re P(k) | im P(k) |\n|---:|---:|---:|\n"); + for k in 1..=DYADIC_MAX { + let p = gs.phase(k); + o.push_str(&format!("| {k} | {} | {} |\n", p.re, p.im)); + } + o.push_str(&format!("\nDigest: `{}`\n\n", hex(&gs.table_digest()))); + } + o +} + +/// `text` with the generated part of Appendix B replaced by +/// [`appendix_tables`], or `None` when it lacks the markers. +fn splice_appendix(text: &str) -> Option { + let b = text.find(APPENDIX_BEGIN)? + APPENDIX_BEGIN.len(); + let e = text[b..].find(APPENDIX_END)? + b; + Some(format!("{}\n{}{}", &text[..b], appendix_tables(), &text[e..])) +} + +fn write_appendix(path: &Path) { + let text = std::fs::read_to_string(path).unwrap_or_else(|e| { + eprintln!("{}: {e}", path.display()); + std::process::exit(1); + }); + let Some(new) = splice_appendix(&text) else { + eprintln!("{}: no generated Appendix B markers ({APPENDIX_BEGIN} … {APPENDIX_END})", path.display()); + std::process::exit(1); + }; + if new == text { + println!("{}: Appendix B is current", path.display()); + } else { + std::fs::write(path, new).expect("write"); + println!("{}: Appendix B rewritten from the phase tables", path.display()); + } +} + // ---- Verification ---------------------------------------------------------------- /// Verify all three trees under `dir`; the number of failures. @@ -492,6 +545,33 @@ mod tests { assert_eq!(verify(&dir), 0); } + /// The extension's Appendix B is what the reference constants render, and + /// each table's digest, which §3.1 and §3.2 also state, appears there and + /// in the appendix and nowhere else. + #[test] + fn the_extension_appendix_is_the_tables() { + let path = Path::new(env!("CARGO_MANIFEST_DIR")).join("../extensions/quantum-sim.md"); + // A checkout may convert line endings of text files. + let text = std::fs::read_to_string(&path).unwrap().replace("\r\n", "\n"); + let want = splice_appendix(&text).expect("the extension has the generated markers"); + assert!( + text == want, + "Appendix B of extensions/quantum-sim.md differs from the phase tables: \ + run `wai_quantum_vectors appendix ../extensions/quantum-sim.md` from wai/wai-rs" + ); + for gs in Gateset::ALL { + let digest = hex(&gs.table_digest()); + assert_eq!(text.matches(digest.as_str()).count(), 2, "{} digest {digest}", gs.capability()); + } + let tables = appendix_tables(); + assert!(tables.contains("### B.1 `wai.quantum.circuit`\n")); + assert!(tables.contains("### B.2 `wai.quantum.circuit2`\n")); + assert!(tables.contains("| 3 | 759250124 | 759250124 |\n")); + assert!(tables.contains("| 22 | 1073741824 | 1608 |\n")); + let rows = tables.lines().filter(|l| l.starts_with("| ") && l.as_bytes()[2].is_ascii_digit()).count(); + assert_eq!(rows, Gateset::ALL.len() * DYADIC_MAX); + } + /// The reject corpus is the plan's 26 cases, named once each. #[test] fn every_reject_case_is_refused_as_listed() { diff --git a/wai/wai-rs/src/codecs/registry.rs b/wai/wai-rs/src/codecs/registry.rs index 1bbe37d07..31dc32e2e 100644 --- a/wai/wai-rs/src/codecs/registry.rs +++ b/wai/wai-rs/src/codecs/registry.rs @@ -345,10 +345,15 @@ pub struct Identifier { /// Every [`Identifier`] SPEC uses. pub const IDENTIFIERS: &[Identifier] = &[ + Identifier { + identifier: "wai.det.amp64", + spec_section: S_INTERACTIVE, + meaning: "the complex fixed-point arithmetic of wai.quantum.circuit2: re and im each an i64 at scale 2^frac, products exact then rounded half up, sums exact (quantum-sim §2.1, §3.0)", + }, Identifier { identifier: "wai.det.fixed64", spec_section: S_INTERACTIVE, - meaning: "the fixed-point arithmetic floor the deterministic *-equivalence criteria are computed in", + meaning: "the fixed-point arithmetic floor the deterministic *-equivalence criteria are computed in (Q32.32, interactive-worlds §6.2); inside a wai.quantum.circuit contract it names the quantum-sim §3.0 arithmetic at frac 30, products rounded half up", }, Identifier { identifier: "wai.world.sim.feed0", -- 2.54.0 (Apple Git-157) From a661860934ec11501c22e8aa2fda6c93b2007265 Mon Sep 17 00:00:00 2001 From: Transaction Science Date: Fri, 9 Oct 2026 14:07:48 -0400 Subject: [PATCH 10/13] wai-quantum 0.4.0, wai-quantum-serve 0.5.0 wai-quantum is 0.4.0, the breaking release that carries the circuit2 capability and the gateset-tagged circuits. wai-quantum-serve is 0.5.0, since its /run, /qasm and /receipt take a gateset and build circuit2 by default. wai-rs requires wai-quantum 0.4.0. The five lockfiles that pin wai-quantum (wai-quantum, wai-quantum-serve, wai-rs, wai-web and wai-pack) change only in their version lines, and each resolves with cargo metadata --locked --offline. The changelog names the serve component's new version. Co-Authored-By: Claude Opus 5.5 --- wai/CHANGELOG.md | 4 ++-- wai/wai-pack/Cargo.lock | 2 +- wai/wai-quantum-serve/Cargo.lock | 4 ++-- wai/wai-quantum-serve/Cargo.toml | 2 +- wai/wai-quantum/Cargo.lock | 2 +- wai/wai-quantum/Cargo.toml | 2 +- wai/wai-rs/Cargo.lock | 2 +- wai/wai-rs/Cargo.toml | 2 +- wai/wai-web/Cargo.lock | 2 +- 9 files changed, 11 insertions(+), 11 deletions(-) diff --git a/wai/CHANGELOG.md b/wai/CHANGELOG.md index 19905ced0..3ec67078d 100644 --- a/wai/CHANGELOG.md +++ b/wai/CHANGELOG.md @@ -67,8 +67,8 @@ wire format may change and such changes are recorded here. JSON; with `--gateset v1` and a phase of `k ≥ 8` it prints the circuit/1 deviation on standard error. `wai_quantum_meter` seals the capability the file names. The browser sink reads both magics; the IDE has `set_gateset`, and its `add()` sets the parameter of - every gate but `P` to 0. The serve component's `/run`, `/qasm` and `/receipt` take - `gateset=v1|v2`. + every gate but `P` to 0. The serve component, `wai-quantum-serve` 0.5.0, takes + `gateset=v1|v2` on `/run`, `/qasm` and `/receipt`. - **Migration from 0.3** (a breaking release): - `Circuit::new` is V2. To reproduce or verify a 0.3 artefact, read its `WQC1` file, or set `c.gateset = Gateset::V1` on a circuit built in code; a receipt's `gateset_of` diff --git a/wai/wai-pack/Cargo.lock b/wai/wai-pack/Cargo.lock index b5be75c42..11894f19c 100644 --- a/wai/wai-pack/Cargo.lock +++ b/wai/wai-pack/Cargo.lock @@ -682,7 +682,7 @@ dependencies = [ [[package]] name = "wai-quantum" -version = "0.3.41" +version = "0.4.0" dependencies = [ "blake3", "ed25519-dalek", diff --git a/wai/wai-quantum-serve/Cargo.lock b/wai/wai-quantum-serve/Cargo.lock index 9b768f3ce..b196c468c 100644 --- a/wai/wai-quantum-serve/Cargo.lock +++ b/wai/wai-quantum-serve/Cargo.lock @@ -589,7 +589,7 @@ dependencies = [ [[package]] name = "wai-quantum" -version = "0.3.41" +version = "0.4.0" dependencies = [ "blake3", "ed25519-dalek", @@ -598,7 +598,7 @@ dependencies = [ [[package]] name = "wai-quantum-serve" -version = "0.4.0" +version = "0.5.0" dependencies = [ "ed25519-dalek", "wai", diff --git a/wai/wai-quantum-serve/Cargo.toml b/wai/wai-quantum-serve/Cargo.toml index f30447a9c..578953629 100644 --- a/wai/wai-quantum-serve/Cargo.toml +++ b/wai/wai-quantum-serve/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "wai-quantum-serve" -version = "0.4.0" +version = "0.5.0" edition = "2021" license = "Apache-2.0" description = "The WAI quantum stack as a WASI-HTTP component — deterministic quantum computation as a serverless function. Runs on any wasi:http/proxy host (wasmtime serve, and other component-model runtimes); returns the same statevector hashes and the same signed receipt bytes as a native run." diff --git a/wai/wai-quantum/Cargo.lock b/wai/wai-quantum/Cargo.lock index 24e5f81a5..51a8fe778 100644 --- a/wai/wai-quantum/Cargo.lock +++ b/wai/wai-quantum/Cargo.lock @@ -415,7 +415,7 @@ checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" [[package]] name = "wai-quantum" -version = "0.3.41" +version = "0.4.0" dependencies = [ "blake3", "ed25519-dalek", diff --git a/wai/wai-quantum/Cargo.toml b/wai/wai-quantum/Cargo.toml index 2831bd971..9561e8e23 100644 --- a/wai/wai-quantum/Cargo.toml +++ b/wai/wai-quantum/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "wai-quantum" -version = "0.3.41" +version = "0.4.0" edition = "2024" rust-version = "1.97" license = "Apache-2.0" diff --git a/wai/wai-rs/Cargo.lock b/wai/wai-rs/Cargo.lock index b4b666cda..f43953910 100644 --- a/wai/wai-rs/Cargo.lock +++ b/wai/wai-rs/Cargo.lock @@ -3900,7 +3900,7 @@ dependencies = [ [[package]] name = "wai-quantum" -version = "0.3.41" +version = "0.4.0" dependencies = [ "blake3", "ed25519-dalek", diff --git a/wai/wai-rs/Cargo.toml b/wai/wai-rs/Cargo.toml index 925f47882..56d8aaba8 100644 --- a/wai/wai-rs/Cargo.toml +++ b/wai/wai-rs/Cargo.toml @@ -93,7 +93,7 @@ curve25519-dalek = { version = "4", optional = true } # ---- quantum (optional, behind the `quantum*` features) ---- # The quantum stack now lives in its own dependency-light crate so it can be used # without the media/codec chain. `wai` re-exports it unchanged. -wai-quantum = { version = "0.3.0", path = "../wai-quantum", optional = true, default-features = false } +wai-quantum = { version = "0.4.0", path = "../wai-quantum", optional = true, default-features = false } [features] default = ["codecs"] diff --git a/wai/wai-web/Cargo.lock b/wai/wai-web/Cargo.lock index 9a9e1e3ef..cf84498df 100644 --- a/wai/wai-web/Cargo.lock +++ b/wai/wai-web/Cargo.lock @@ -755,7 +755,7 @@ dependencies = [ [[package]] name = "wai-quantum" -version = "0.3.41" +version = "0.4.0" dependencies = [ "blake3", "ed25519-dalek", -- 2.54.0 (Apple Git-157) From 956003d0c84c5afd3e279bfd977e753a3921077c Mon Sep 17 00:00:00 2001 From: Transaction Science Date: Fri, 9 Oct 2026 16:20:47 -0400 Subject: [PATCH 11/13] wai-quantum circuit2: sections read as I-JSON, a reject payload for every refusal, review fixes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The review of the branch upheld nine findings; this fixes all of them. - The contract and the measurement are read as I-JSON (RFC 7493, quantum-sim §2): UTF-8, no member name twice in one object, no unpaired surrogate escape, no number that rounds to an infinite double, at most 127 nested levels. An unsigned JSON integer has no sign, fraction or exponent, so `-0` is not one. The Rust reader (a visitor refuses repeated names) and the referee enforce the same profile; the repeated member name is the one newly refused payload, listed in §8 and SPEC §5. - `reject_corpus()` in wai-quantum is the one list of reject cases: the crate's tests and `wai_quantum_vectors gen` both read it. It now holds a payload for each refusal a payload can carry: the short payload, a table or section past the end, a missing contract or op-log, a short op-log, truncated controls, target or control out of range, a control on its target, P(0) and P(33), the n_qubits forms, the JSON profile, and the measurement rules. - wai-energy-receipts runs on a change to wai/quantum-conformance, since its wai_quantum_vectors tests require the committed corpus to be what gen writes. - Wording: the statevector changes where H, T, T†, P(3) or P(k ≥ 5) act (P(1), P(2) and P(4) are equal in both tables); each component of every circuit2 P(k) is within half a unit; only circuit2's P(k) gives the QFT ladder; the original vectors' hashes move with a P(k) entry only through H. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/wai-energy-receipts.yml | 6 + wai/CHANGELOG.md | 25 +- wai/SPEC.md | 7 +- wai/extensions/quantum-sim.md | 33 +- wai/quantum-conformance/README.md | 5 +- .../reject/no_contract.wqc | Bin 0 -> 30 bytes wai/quantum-conformance/reject/no_oplog.wqc | Bin 0 -> 127 bytes wai/quantum-conformance/reject/reject.json | 29 ++ .../reject/section_past_end.wqc | Bin 0 -> 151 bytes .../reject/short_payload.wqc | Bin 0 -> 5 bytes .../reject/table_past_end.wqc | Bin 0 -> 15 bytes .../reject/unknown_section_past_end.wqc | Bin 0 -> 160 bytes .../reject/v1_contract_duplicate_member.wqc | Bin 0 -> 161 bytes .../reject/v1_contract_lone_surrogate.wqc | Bin 0 -> 167 bytes .../reject/v1_contract_not_utf8.wqc | Bin 0 -> 162 bytes .../v1_contract_number_out_of_range.wqc | Bin 0 -> 161 bytes .../reject/v1_contract_too_deep.wqc | Bin 0 -> 410 bytes .../reject/v1_control_is_target.wqc | Bin 0 -> 146 bytes .../reject/v1_control_out_of_range.wqc | Bin 0 -> 146 bytes .../reject/v1_measure_duplicate_member.wqc | Bin 0 -> 190 bytes .../reject/v1_measure_not_json.wqc | Bin 0 -> 170 bytes .../reject/v1_n_qubits_fraction.wqc | Bin 0 -> 153 bytes .../reject/v1_n_qubits_minus0.wqc | Bin 0 -> 152 bytes .../reject/v1_n_qubits_missing.wqc | Bin 0 -> 138 bytes .../reject/v1_op_truncated_controls.wqc | Bin 0 -> 146 bytes .../reject/v1_opcode_10.wqc | Bin 0 -> 145 bytes .../reject/v1_oplog_short.wqc | Bin 0 -> 139 bytes wai/quantum-conformance/reject/v1_phase_0.wqc | Bin 0 -> 145 bytes .../reject/v1_phase_33.wqc | Bin 0 -> 145 bytes .../reject/v1_target_out_of_range.wqc | Bin 0 -> 145 bytes .../reject/v2_contract_not_object.wqc | Bin 0 -> 155 bytes .../reject/v2_measure_seed_signed.wqc | Bin 0 -> 208 bytes .../reject/v2_measure_twice.wqc | Bin 0 -> 261 bytes .../reject/v2_n_qubits_256.wqc | Bin 0 -> 155 bytes wai/quantum-conformance/reject/v2_phase_0.wqc | Bin 0 -> 147 bytes wai/quantum-conformance/tools/referee.py | 75 +++- wai/wai-pack/Cargo.lock | 1 + wai/wai-quantum-serve/Cargo.lock | 1 + wai/wai-quantum/Cargo.lock | 1 + wai/wai-quantum/Cargo.toml | 3 + wai/wai-quantum/src/quantum.rs | 402 ++++++++++++------ wai/wai-rs/Cargo.lock | 1 + wai/wai-rs/src/bin/wai_quantum_vectors.rs | 150 +------ wai/wai-rs/src/codecs/mod.rs | 15 +- wai/wai-web/Cargo.lock | 1 + 45 files changed, 442 insertions(+), 313 deletions(-) create mode 100644 wai/quantum-conformance/reject/no_contract.wqc create mode 100644 wai/quantum-conformance/reject/no_oplog.wqc create mode 100644 wai/quantum-conformance/reject/section_past_end.wqc create mode 100644 wai/quantum-conformance/reject/short_payload.wqc create mode 100644 wai/quantum-conformance/reject/table_past_end.wqc create mode 100644 wai/quantum-conformance/reject/unknown_section_past_end.wqc create mode 100644 wai/quantum-conformance/reject/v1_contract_duplicate_member.wqc create mode 100644 wai/quantum-conformance/reject/v1_contract_lone_surrogate.wqc create mode 100644 wai/quantum-conformance/reject/v1_contract_not_utf8.wqc create mode 100644 wai/quantum-conformance/reject/v1_contract_number_out_of_range.wqc create mode 100644 wai/quantum-conformance/reject/v1_contract_too_deep.wqc create mode 100644 wai/quantum-conformance/reject/v1_control_is_target.wqc create mode 100644 wai/quantum-conformance/reject/v1_control_out_of_range.wqc create mode 100644 wai/quantum-conformance/reject/v1_measure_duplicate_member.wqc create mode 100644 wai/quantum-conformance/reject/v1_measure_not_json.wqc create mode 100644 wai/quantum-conformance/reject/v1_n_qubits_fraction.wqc create mode 100644 wai/quantum-conformance/reject/v1_n_qubits_minus0.wqc create mode 100644 wai/quantum-conformance/reject/v1_n_qubits_missing.wqc create mode 100644 wai/quantum-conformance/reject/v1_op_truncated_controls.wqc create mode 100644 wai/quantum-conformance/reject/v1_opcode_10.wqc create mode 100644 wai/quantum-conformance/reject/v1_oplog_short.wqc create mode 100644 wai/quantum-conformance/reject/v1_phase_0.wqc create mode 100644 wai/quantum-conformance/reject/v1_phase_33.wqc create mode 100644 wai/quantum-conformance/reject/v1_target_out_of_range.wqc create mode 100644 wai/quantum-conformance/reject/v2_contract_not_object.wqc create mode 100644 wai/quantum-conformance/reject/v2_measure_seed_signed.wqc create mode 100644 wai/quantum-conformance/reject/v2_measure_twice.wqc create mode 100644 wai/quantum-conformance/reject/v2_n_qubits_256.wqc create mode 100644 wai/quantum-conformance/reject/v2_phase_0.wqc diff --git a/.github/workflows/wai-energy-receipts.yml b/.github/workflows/wai-energy-receipts.yml index 86c3751a0..9dec45179 100644 --- a/.github/workflows/wai-energy-receipts.yml +++ b/.github/workflows/wai-energy-receipts.yml @@ -99,6 +99,9 @@ on: - 'wai/CHANGELOG.md' # tests/video_receipt.rs reads the WIV1 refusal fixtures. - 'wai/int-payload-conformance/**' + # wai_quantum_vectors' tests require the committed quantum corpus to be + # what `gen` writes. + - 'wai/quantum-conformance/**' - '.github/workflows/wai-energy-receipts.yml' pull_request: paths: @@ -110,6 +113,9 @@ on: - 'wai/CHANGELOG.md' # tests/video_receipt.rs reads the WIV1 refusal fixtures. - 'wai/int-payload-conformance/**' + # wai_quantum_vectors' tests require the committed quantum corpus to be + # what `gen` writes. + - 'wai/quantum-conformance/**' - '.github/workflows/wai-energy-receipts.yml' jobs: diff --git a/wai/CHANGELOG.md b/wai/CHANGELOG.md index 3ec67078d..44e73b255 100644 --- a/wai/CHANGELOG.md +++ b/wai/CHANGELOG.md @@ -25,7 +25,8 @@ wire format may change and such changes are recorded here. 7032 units of `2^−30`, and `P(19..32)` apply no phase. The claim that this table gives the QFT is withdrawn. Writers SHOULD NOT emit new circuit/1 content. - **Both capabilities are specified to the bit** (§2–§4): little-endian integers, section - bounds computed without overflow, a repeated known section refused, the three refusal + bounds computed without overflow, a repeated known section refused, the contract and + the measurement read as I-JSON (RFC 7493, at most 127 levels deep), the three refusal classes (malformed, contract, invalid) in the order a reader checks them, the complex-product composition, the index convention, the gate matrices, the hash layouts and the shot sampler. `circuit_hash` covers the canonical contract, so it names the @@ -35,13 +36,13 @@ wire format may change and such changes are recorded here. - **Newly non-conforming circuit/1 payloads** (quantum-sim §8, SPEC §5): a contract whose `ext`, `numeric`, `frac` or `gateset` is missing or carries another value; an `n_qubits` that is not an unsigned integer of at most 255; a known section kind more - than once; section bounds that overflow. No file a reference writer wrote is among - them. + than once; section bounds that overflow; a contract or measurement that repeats a + member name within one object. No file a reference writer wrote is among them. - **Corpus.** `quantum-conformance/` keeps its four original vectors and manifest entries byte for byte and adds qft5_x22, dyadic_ladder and qft8_ones, which see the phase table: under either table, `dyadic_ladder`'s statevector hash changes with every ±1 change of any `P(1..32)` component. `v2/` holds the same seven circuits as circuit2, and `reject/` - a payload for each refusal with its class. `tools/referee.py`, integers only, derives + at least one payload for each refusal a payload can carry, with its class. `tools/referee.py`, integers only, derives both tables, requires Appendix B to state them, and rebuilds and checks every file. The Appendix B tables are rendered from the reference constants by `wai_quantum_vectors appendix`, and a test fails when the text differs. @@ -54,13 +55,17 @@ wire format may change and such changes are recorded here. derived from another keeps it. `PHASE_V1` and `PHASE_V2` are literal constants: no recurrence runs at load. `simulate`, `to_wqc`, `circuit_hash`, receipts, the fabric's lanes and the toolchain follow the circuit's gateset. `from_wqc` reads both magics, - checks a circuit/1 contract's values and a circuit2 contract's bytes, refuses repeated - sections and wrapping bounds, and no longer sizes an allocation from a claimed op count. + checks a circuit/1 contract's values and a circuit2 contract's bytes, reads the contract + and the measurement as I-JSON (a member name repeated within an object is refused), + refuses repeated sections and wrapping bounds, and no longer sizes an allocation from a + claimed op count. `reject_corpus()` returns the payloads a reader must refuse, each with + its class; the corpus's `reject/` tree is written from it. - **Every circuit built in code changes its `circuit_hash`**, since the contract names the - capability, and its statevector wherever `H`, `T`, `T†` or a phase acts on a nonzero - amplitude. Every circuit/1 artefact reproduces: the four original vectors, the legacy - receipt pins, the lanes32 certificate corpus and the other pins are unchanged, built in - `Gateset::V1`, and new V2 pins sit beside them. + capability, and its statevector wherever `H`, `T`, `T†`, `P(3)` or `P(k ≥ 5)` acts on a + nonzero amplitude (`P(1)`, `P(2)` and `P(4)` are the same in both tables). Every + circuit/1 artefact reproduces: the four original vectors, the legacy receipt pins, the + lanes32 certificate corpus and the other pins are unchanged, built in `Gateset::V1`, and + new V2 pins sit beside them. - **wai-rs** registers `wai.quantum.circuit2`; `quantum_envelope` tags an envelope with the circuit's capability and refuses one whose capability and magic disagree. The `wai-quantum` CLI takes `--gateset v1|v2` (default v2) and names the capability in its diff --git a/wai/SPEC.md b/wai/SPEC.md index 839544ec1..8406acf03 100644 --- a/wai/SPEC.md +++ b/wai/SPEC.md @@ -730,7 +730,7 @@ standard. See the linked extension for each. | `wai.film.reel` | WREL container (sealed world + a sequence of shots = camera track + schedule each) | reel-equivalence (film-equivalence with cuts) · [linear-film](extensions/linear-film.md) | | `wai.ternary.tt` | WQTT container (balanced-ternary MPS cores — packed trits + integer per-column scales) | a **quantum-inspired** (tensor-network) transform ported from the ternary/MPS fabric with ONE change: the per-column scale is an **integer**, so the whole decode is `i64`/`i128` and the reconstruction is byte-identical on every machine — **reconstruction-equivalence** (a portable recon hash), receiptable. Scope: a **provenance** transform, measured at 13.3 dB SNR for 3.8× with six residual layers (`Lossy` tier; the extension's §5). Encoder-side float SVD only · [ternary-tt](extensions/ternary-tt.md) | | `wai.quantum.circuit` | WQC container (gate op-log — Clifford+T + dyadic phase) | the wire carries a **circuit** (kilobytes, linear in gate count), the sink CLASSICALLY simulates it in a fixed-point **complex** floor (`Amp` = i64 re/im), and the reconstructed `2^n`-amplitude statevector is byte-identical on every machine — **statevector-equivalence** (a portable statevector hash); measurement is sampled by the pinned `splitmix64` so a shot histogram at a pinned `(seed, shots)` converges too (**shot-histogram-equivalence**). The dyadic phase ladder transports the **QFT** ("quantum FFT") as a circuit, reconstructed byte-exact; the phase table is frozen as existing content was made with it, so `P(k)` deviates from the ideal phase for k ≥ 8 and applies none for k ≥ 19, and new content uses `wai.quantum.circuit2`. No quantum hardware — NISQ noise breaks the determinism the standard rests on; this makes a quantum *computation* transportable + receiptable, with **no** speedup claimed · [quantum-sim](extensions/quantum-sim.md) | -| `wai.quantum.circuit2` | WQC container under magic `WQC2` (the same gate op-log) | the successor of `wai.quantum.circuit`, with the same statevector- and shot-histogram-equivalence: a correctly rounded phase table (every `P(k)` within half a unit of 2^30·e^{2πi/2^k}), so the dyadic phase ladder gives the **QFT**; a byte-canonical contract; an op-log that ends exactly after its last op. A new capability, not a change to the old one, because a registered capability's payload format is fixed (§8): a reader that predates it rejects a `WQC2` payload at the magic · [quantum-sim](extensions/quantum-sim.md) | +| `wai.quantum.circuit2` | WQC container under magic `WQC2` (the same gate op-log) | the successor of `wai.quantum.circuit`, with the same statevector- and shot-histogram-equivalence: a correctly rounded phase table (each component of every `P(k)` within half a unit of 2^30·e^{2πi/2^k}), so the dyadic phase ladder gives the **QFT**; a byte-canonical contract; an op-log that ends exactly after its last op. A new capability, not a change to the old one, because a registered capability's payload format is fixed (§8): a reader that predates it rejects a `WQC2` payload at the magic · [quantum-sim](extensions/quantum-sim.md) | The contract of a `wai.quantum.circuit2` payload names its arithmetic `wai.det.amp64`: complex amplitudes as `i64` pairs at scale `2^frac`, each @@ -744,8 +744,9 @@ enforce the registration of `wai.quantum.circuit` as it was made (§8). A sink r when it decodes it, a `wai.quantum.circuit` payload whose contract does not carry the registered `ext`, `numeric`, `frac` and `gateset`, a `wai.quantum.circuit2` contract among them; whose `n_qubits` is not an unsigned integer of at most 255; -that carries a known section kind more than once; or that has a section whose -bounds overflow. No payload a reference writer wrote is among them; +that carries a known section kind more than once; that has a section whose +bounds overflow; or whose contract or measurement repeats a member name within one +object. No payload a reference writer wrote is among them; [quantum-sim](extensions/quantum-sim.md) §8 lists them with the rules. And the verify half — a signed, energy-accounted lineage for opaque diff --git a/wai/extensions/quantum-sim.md b/wai/extensions/quantum-sim.md index 9bdc3ed76..bce9bbb8b 100644 --- a/wai/extensions/quantum-sim.md +++ b/wai/extensions/quantum-sim.md @@ -92,6 +92,15 @@ different sections of one payload. The reference writer emits the contract, the op-log and the measurement, if any, in that order and contiguous from offset 0, and no other section. +**JSON in a section.** The contract and the measurement are JSON texts, read as +I-JSON ([RFC 7493](https://www.rfc-editor.org/rfc/rfc7493)): UTF-8, no member +name repeated within one object, no `\u` escape of an unpaired surrogate, and no +number that rounds to an infinite IEEE 754 double. A reader also refuses nesting +deeper than 127 arrays and objects, the outermost value counted as the first. +An integer is read exactly, so `seed` and `shots` (§2.3) span the whole `u64` +range. Where a rule asks for an *unsigned JSON integer*, it means a number +written with no sign, fraction or exponent: `-0`, `2.0` and `2e0` are not one. + A reader refuses a payload that breaks a rule of this section or of §2.1–§2.3. It checks in this order and refuses with the class of the first rule broken: @@ -106,8 +115,10 @@ It checks in this order and refuses with the class of the first rule broken: 5. The circuit keeps the rules of its capability (§2.2); else **invalid**. 6. The measurement, if present (§2.3); else **malformed**. -The corpus's `reject/` tree holds one payload for each of these refusals, and -`reject.json` names each one's class. +The corpus's `reject/` tree holds at least one payload for each refusal of this +section and of §2.1–§2.3 that a payload can carry, and `reject.json` names each +one's class. The two limits it cannot carry bind a writer instead: the wire +counts a gate's controls in a `u8` and a circuit's ops in a `u32`. ### 2.1 Contract @@ -138,7 +149,8 @@ differ. `numeric` names the arithmetic: floor, with products rounded toward −∞, that [interactive-worlds](interactive-worlds.md) §6.2 names `wai.det.fixed64`. -Reading a circuit/1 contract. The section MUST be a JSON object, else contract. +Reading a circuit/1 contract. The section MUST be an I-JSON object (§2), else +contract. Its `ext`, `numeric` and `gateset` MUST be the strings of the template above and its `frac` the JSON integer `30` (`30.0` is not); a member that is missing or carries another value is a contract refusal, so a circuit2 contract in a `WQC1` @@ -146,7 +158,7 @@ container is refused. Then `n_qubits` MUST be an unsigned JSON integer of at mos 255, else malformed. Member order, whitespace and further members are free; `circuit_hash` covers the canonical bytes, not the received ones (§4.1). -Reading a circuit2 contract. The section MUST be a JSON object, else contract, +Reading a circuit2 contract. The section MUST be an I-JSON object (§2), else contract, whose `n_qubits` is an unsigned JSON integer of at most 255, else malformed. Its bytes MUST then equal the canonical circuit2 contract for that `n_qubits`, else contract: no whitespace, no other member order and no further member, and a @@ -207,7 +219,7 @@ example is the corpus's bell measurement. - circuit2: the section MUST be exactly `{"seed":S,"shots":N,"basis":"computational"}`, with `S` and `N` in decimal with no sign, leading zero, fraction, exponent or whitespace; else malformed. -- circuit/1: the section MUST be JSON, else malformed. A `seed` or `shots` that +- circuit/1: the section MUST be I-JSON (§2), else malformed. A `seed` or `shots` that is missing or is not an unsigned integer in the `u64` range reads as 0, and `basis` is not read. The reference writer writes the circuit2 form. @@ -264,9 +276,9 @@ The canonical reconstructed quantity is the amplitude vector serialized as `re_le(i64) ‖ im_le(i64)` per amplitude, in index order. `cliffordT+dyadicP` and `cliffordT+dyadicP.rn` are universal (Clifford+T is -dense in `SU(2^n)`), and `P(k)` gives the controlled-phase ladder of the QFT -("quantum FFT"), transported as a circuit (`H` + controlled-`P(k)` + swaps) and -reconstructed byte-exact. No float runs on the decode: the phase tables are +dense in `SU(2^n)`). In circuit2, `P(k)` gives the controlled-phase ladder of +the QFT ("quantum FFT"), transported as a circuit (`H` + controlled-`P(k)` + +swaps) and reconstructed byte-exact; the circuit/1 table does not (§3.1). No float runs on the decode: the phase tables are integer constants. ### 3.1 The circuit/1 phase table (frozen) @@ -589,7 +601,10 @@ payloads that break them, and a reader of this revision refuses: - a known section kind that appears more than once (an earlier reader kept the last); - a section whose bounds overflow (an earlier reader could wrap on a 32-bit - target). + target); +- a contract or measurement that repeats a member name within one object (an + earlier reader kept the last). The other I-JSON rules of §2 state what earlier + reference readers already refused. No file written by any reference version is affected: every reference writer wrote the canonical contract, one section of each kind, and sections within the diff --git a/wai/quantum-conformance/README.md b/wai/quantum-conformance/README.md index 49cb31eb2..e84e9b232 100644 --- a/wai/quantum-conformance/README.md +++ b/wai/quantum-conformance/README.md @@ -81,8 +81,9 @@ where it stops: `dyadic_ladder`'s statevector hash; the referee checks this on every run. A change of ±1 to `H` alone changes the hash of every vector that applies `H`. - **The four original vectors see no `P(k)`.** They run the QFT from - `|0…0⟩`, where every controlled phase acts on zero amplitudes, so no `P(k)` change - moves their hashes; the three vectors after them are there for that. + `|0…0⟩`, where every controlled phase acts on zero amplitudes, so no change to a + `P(k)` entry moves their hashes except through `H`, which follows `re P(3)`; the + three vectors after them are there for that. - **One control at most.** Gates with more than one control appear only in the reject tree; no vector applies one. - **One measurement.** The shot sampler is pinned by bell's measurement alone. diff --git a/wai/quantum-conformance/reject/no_contract.wqc b/wai/quantum-conformance/reject/no_contract.wqc new file mode 100644 index 0000000000000000000000000000000000000000..9994546a8db765bab7da5b663e28f728c2bcdbf6 GIT binary patch literal 30 ccmWF!bT(vUU}69Qejo-hSU?;`Mn)hB03o~qH~;_u literal 0 HcmV?d00001 diff --git a/wai/quantum-conformance/reject/no_oplog.wqc b/wai/quantum-conformance/reject/no_oplog.wqc new file mode 100644 index 0000000000000000000000000000000000000000..d7f946b2c2990a679970ab297e9a7555ab23f2b4 GIT binary patch literal 127 zcmWF!bT(vUU}OM-0wAtdO06hSvQjEf%+xC^P0TAP&DBfJEJ`lTEYUYq(oxEbFDy;U zEGbs9G6Hf+b5n~llcCB}QcLvGGAmM3%uIj^(~1&-62=BPO6iFusl}-vLz8nd)6(*b QQbM#-Dic#OlLM4$0TbFM6#xJL literal 0 HcmV?d00001 diff --git a/wai/quantum-conformance/reject/reject.json b/wai/quantum-conformance/reject/reject.json index bcafe420f..e85719011 100644 --- a/wai/quantum-conformance/reject/reject.json +++ b/wai/quantum-conformance/reject/reject.json @@ -12,12 +12,41 @@ "v1_n_qubits_258.wqc": "malformed", "v1_contract_twice.wqc": "malformed", "v1_oplog_twice.wqc": "malformed", + "v1_contract_duplicate_member.wqc": "contract", + "v1_contract_lone_surrogate.wqc": "contract", + "v1_contract_too_deep.wqc": "contract", + "v1_n_qubits_minus0.wqc": "malformed", + "v1_measure_duplicate_member.wqc": "malformed", + "v1_contract_not_utf8.wqc": "contract", + "v1_contract_number_out_of_range.wqc": "contract", + "v1_n_qubits_fraction.wqc": "malformed", + "v1_n_qubits_missing.wqc": "malformed", + "v1_measure_not_json.wqc": "malformed", + "short_payload.wqc": "malformed", + "table_past_end.wqc": "malformed", + "section_past_end.wqc": "malformed", + "unknown_section_past_end.wqc": "malformed", + "no_contract.wqc": "malformed", + "no_oplog.wqc": "malformed", + "v1_oplog_short.wqc": "malformed", + "v1_op_truncated_controls.wqc": "malformed", + "v1_opcode_10.wqc": "malformed", + "v1_target_out_of_range.wqc": "invalid", + "v1_control_out_of_range.wqc": "invalid", + "v1_control_is_target.wqc": "invalid", + "v1_phase_0.wqc": "invalid", + "v1_phase_33.wqc": "invalid", "v1_n_ops_u32max.wqc": "malformed", "v1_offset_wrap.wqc": "malformed", "v2_contract_whitespace.wqc": "contract", "v2_contract_key_order.wqc": "contract", "v2_contract_extra_member.wqc": "contract", "v2_contract1_in_wqc2.wqc": "contract", + "v2_n_qubits_256.wqc": "malformed", + "v2_contract_not_object.wqc": "contract", + "v2_phase_0.wqc": "invalid", + "v2_measure_twice.wqc": "malformed", + "v2_measure_seed_signed.wqc": "malformed", "v2_duplicate_controls.wqc": "invalid", "v2_param_on_h.wqc": "invalid", "v2_oplog_trailing_byte.wqc": "malformed", diff --git a/wai/quantum-conformance/reject/section_past_end.wqc b/wai/quantum-conformance/reject/section_past_end.wqc new file mode 100644 index 0000000000000000000000000000000000000000..cd91fbe7c321a1f02b27389281583b9c8856548c GIT binary patch literal 151 zcmWF!bT(vSU}OM-0w87r5duJ5t(00(qGY91o|vguSelquQktumoLQ7ynpvW6sHCHm z7hhPKlvz@&WMu^8l;)-uWhO(Fr=*tXrDay6rkI%k6{Zy>0ws(Mbd=H)OHzwdL53#h gWTvI%7o~(~r&K1UWF`kF)iMF?WC3CZMn;fh0I_x_p#T5? literal 0 HcmV?d00001 diff --git a/wai/quantum-conformance/reject/short_payload.wqc b/wai/quantum-conformance/reject/short_payload.wqc new file mode 100644 index 0000000000000000000000000000000000000000..9762d59e387b4a86fd3e17e6b12e116d7972e4b0 GIT binary patch literal 5 McmWF!bT(oD00b!k9smFU literal 0 HcmV?d00001 diff --git a/wai/quantum-conformance/reject/table_past_end.wqc b/wai/quantum-conformance/reject/table_past_end.wqc new file mode 100644 index 0000000000000000000000000000000000000000..24b1ff320c244f9f36b727a9cc04fb6b3ad9c366 GIT binary patch literal 15 TcmWF!bT(vSU}OM-0w4we5hwwW literal 0 HcmV?d00001 diff --git a/wai/quantum-conformance/reject/unknown_section_past_end.wqc b/wai/quantum-conformance/reject/unknown_section_past_end.wqc new file mode 100644 index 0000000000000000000000000000000000000000..2b83adaf368309c0c455bb206812e0bb49fbe048 GIT binary patch literal 160 zcmXYqK?}k#429e2-T#oQqdFB2yZQ$d1dmeNR07*{U5XQt{dH3?@I6TKc<*vLR5PWO z%u^_`EQDzi$>$H?$WX(}lIfG}IJL%6bcuLXK?&WpPdDNL>WwU^#YpaN_sAyDhTbq$5^i2l6vZ|lh&;af=OTu@r)`X5UxD4+lU literal 0 HcmV?d00001 diff --git a/wai/quantum-conformance/reject/v1_contract_duplicate_member.wqc b/wai/quantum-conformance/reject/v1_contract_duplicate_member.wqc new file mode 100644 index 0000000000000000000000000000000000000000..7571545f884c127874368778ed1ab3664d3153b8 GIT binary patch literal 161 zcmWF!bT(vSU}OM-Dj;S85&S@0t(00(qGY91o|vguSelquQktumoLQ7ynpvW6sHCHm z7hhPKlvz@&WMu^8l;)-uWhO(Fr=*tXrDay6rkI%k6{Zy>0ws(MkXh-8C8@=!AmfvB gGSkxXi&8?gQz{cvGLr+8YMFqxvj8yzBO}OJ0A*+@T>t<8 literal 0 HcmV?d00001 diff --git a/wai/quantum-conformance/reject/v1_contract_lone_surrogate.wqc b/wai/quantum-conformance/reject/v1_contract_lone_surrogate.wqc new file mode 100644 index 0000000000000000000000000000000000000000..e7046e2f5fae5e43b62abed22a36e2a62db008ec GIT binary patch literal 167 zcmXYr!3x4K5Jc19-GA7tCAKOez4`+Rf)_zzvXKSb(sY9r5&U#F;KExNW*Fvt+}Bza zO71J@{N}~bVseLx{J$QMZKN5)VK2p)oCLc~ l1rNtKVHx!;#Wj%10D_H;l literal 0 HcmV?d00001 diff --git a/wai/quantum-conformance/reject/v1_contract_number_out_of_range.wqc b/wai/quantum-conformance/reject/v1_contract_number_out_of_range.wqc new file mode 100644 index 0000000000000000000000000000000000000000..e42d53722f6d2912d16004772464125663bc132a GIT binary patch literal 161 zcmXYr!3x4K42Ij`-FL{ mw{WuwXRJ%zokDm-e_HvM_2drh6)+w4T7Mh9zF>ru^{pRu1S$Xk literal 0 HcmV?d00001 diff --git a/wai/quantum-conformance/reject/v1_contract_too_deep.wqc b/wai/quantum-conformance/reject/v1_contract_too_deep.wqc new file mode 100644 index 0000000000000000000000000000000000000000..87e1b93b9c5d01f10f2ac39b92651772913638a2 GIT binary patch literal 410 zcmWF!bT(vSU}OM-Vj#%`BKU!vYNgbQ5+y68^2AKN!qUXNlG0qgq|B0HB`YH!r!+UUC^H$VJSDY6FD0ws(Mbd=H)OHzwdL53#h cWTvI%7o~(~r&K1UWF`kF)q-5W$Ot4E0q8s@mjD0& literal 0 HcmV?d00001 diff --git a/wai/quantum-conformance/reject/v1_control_out_of_range.wqc b/wai/quantum-conformance/reject/v1_control_out_of_range.wqc new file mode 100644 index 0000000000000000000000000000000000000000..02bd8fefce5aff1cbee1f71d52b8c5da2c6c868f GIT binary patch literal 146 zcmXX=K?=e!5NvYv8GE%vtKy+oKcFCZl$dN}!L~Nt#ftcMH{iew3^NRKIUO2XnM(0g z+9C_(2XJO+;pNGV;(M0P19k3`P5J?;p6x)>1nvF1qyx6a<$t&SW%TTRRV|l;W?Ofd4`Del?DJW)-5 DT);6x literal 0 HcmV?d00001 diff --git a/wai/quantum-conformance/reject/v1_measure_not_json.wqc b/wai/quantum-conformance/reject/v1_measure_not_json.wqc new file mode 100644 index 0000000000000000000000000000000000000000..24bb3f3bf254d3d1cc3e3071f98b1af0368e1e67 GIT binary patch literal 170 zcmXYr%?iRm421jR$&2rht7Ww+9`@=BC9-OZMOt#&tyl_I{nDH!+$LWY@KPKUb8 z@=WF_6q783@+&eIM!l~PN_^x U$xIGVs$~LN!ve$%jEo@r0es~nMF0Q* literal 0 HcmV?d00001 diff --git a/wai/quantum-conformance/reject/v1_op_truncated_controls.wqc b/wai/quantum-conformance/reject/v1_op_truncated_controls.wqc new file mode 100644 index 0000000000000000000000000000000000000000..9ddf43b9d74d257c1d8985b086e7920239e51af2 GIT binary patch literal 146 zcmWF!bT(vSU}OM-0w87r5nMoAt(00(qGY91o|vguSelquQktumoLQ7ynpvW6sHCHm z7hhPKlvz@&WMu^8l;)-uWhO(Fr=*tXrDay6rkI%k6{Zy>0ws(Mbd=H)OHzwdL53#h bWTvI%7o~(~r&K1UWF`kF)q-5W$OIw*=s+iz literal 0 HcmV?d00001 diff --git a/wai/quantum-conformance/reject/v1_opcode_10.wqc b/wai/quantum-conformance/reject/v1_opcode_10.wqc new file mode 100644 index 0000000000000000000000000000000000000000..52f13107d6aaa66223cb56c0742949fe6341371b GIT binary patch literal 145 zcmWF!bT(vSU}OM-0w87r5u89=t(00(qGY91o|vguSelquQktumoLQ7ynpvW6sHCHm z7hhPKlvz@&WMu^8l;)-uWhO(Fr=*tXrDay6rkI%k6{Zy>0ws(Mbd=H)OHzwdL53#h bWTvI%7o~(~r&K1UWF`kF)q-5W1;h*hz)mNe literal 0 HcmV?d00001 diff --git a/wai/quantum-conformance/reject/v1_oplog_short.wqc b/wai/quantum-conformance/reject/v1_oplog_short.wqc new file mode 100644 index 0000000000000000000000000000000000000000..4ae862bec3d786dbb0ec92ce54cc461218b20bb2 GIT binary patch literal 139 zcmXX<(F(#q44d;$`sz5HiVu7B2NVP!0ws(Mbd=H)OHzwdL53#h bWTvI%7o~(~r&K1UWF`kF)q-3AvX=n>z)2^Y literal 0 HcmV?d00001 diff --git a/wai/quantum-conformance/reject/v1_phase_33.wqc b/wai/quantum-conformance/reject/v1_phase_33.wqc new file mode 100644 index 0000000000000000000000000000000000000000..c270690eb43ef2d744b3089169d8753fa818e64a GIT binary patch literal 145 zcmWF!bT(vSU}OM-0w87r5u89=t(00(qGY91o|vguSelquQktumoLQ7ynpvW6sHCHm z7hhPKlvz@&WMu^8l;)-uWhO(Fr=*tXrDay6rkI%k6{Zy>0ws(Mbd=H)OHzwdL53#h cWTvI%7o~(~r&K1UWF`kF)q-3Av{#V<0KkhUyZ`_I literal 0 HcmV?d00001 diff --git a/wai/quantum-conformance/reject/v1_target_out_of_range.wqc b/wai/quantum-conformance/reject/v1_target_out_of_range.wqc new file mode 100644 index 0000000000000000000000000000000000000000..ddb09d24eab72a9da71c15a584a5f2f430c29a43 GIT binary patch literal 145 zcmXX=K?=e!5RAc#-`J}qS``ny`T+&Oqr_w*3vIROE>^_9y8#DgV3=W;%jr;CGc$^( z(iT}LKY$ZM120c*5Z|+OE>I3aysDvs{yNBwc!Xx7Mmo$C{`H9L5=|KQJ1wT{HQ25z cxO>JC%V=mRrALnEWqkWc;RxUIU~a52KfpF8m;e9( literal 0 HcmV?d00001 diff --git a/wai/quantum-conformance/reject/v2_contract_not_object.wqc b/wai/quantum-conformance/reject/v2_contract_not_object.wqc new file mode 100644 index 0000000000000000000000000000000000000000..8e0fafe8267197cd6b0e5328b899e1ce7cbc8685 GIT binary patch literal 155 zcmWF!bT(pQU}OM-5+G&*5&S?LU9FT_QKDp}RGyfrS6G^uS5lg*mz-IYT$)*8q;IIC zqm&n4SelesQmkZU1mu+FrWR!;L)E9Gmgpts7MPg;m8BIW0)>qYbd=H)OHzwdL1rfB kWTvI%7o~(~r&K1UWF`mb73C?_#xeozWdULaMn;f>09T|a&j0`b literal 0 HcmV?d00001 diff --git a/wai/quantum-conformance/reject/v2_measure_seed_signed.wqc b/wai/quantum-conformance/reject/v2_measure_seed_signed.wqc new file mode 100644 index 0000000000000000000000000000000000000000..3efe7370ef0d7a81924b5e6b09ac24999dff9f8e GIT binary patch literal 208 zcmXYr%L>9k3`JY(-har}I@&5Oy7dPX1ecP|Of}HyV;-oKF8p&dVBlPm5bn+Gaw@YV zO}H$xh_Yg`FNWqo-`CX?m-!YxO}z@+@oz^MX^COp>PYuL+#i3aU+!gM0Qds9={G R9dQe?H$(8~sqe6b`3DUEH}?Pl literal 0 HcmV?d00001 diff --git a/wai/quantum-conformance/reject/v2_measure_twice.wqc b/wai/quantum-conformance/reject/v2_measure_twice.wqc new file mode 100644 index 0000000000000000000000000000000000000000..7517c37d8a9eb6e9d5fe806a0936e50d750d75fe GIT binary patch literal 261 zcmb78OA5kJ3~X!n-XU9SwN+en>je}9my*8ss)2sr2bI!=7xIkeK`&rnhDjJ^ay=jO zI0yqC&4@fL8H*3o+KcZjMo=#c1w4_`!C`A%lM2$pkX%e$v zDw|@{ANFjkH1c`3P2i4J8RauYO;y!td0ELfN)o43w=hNA95aG2^bd|;l#)@mbw;enEo>V0M08%tN;K2 literal 0 HcmV?d00001 diff --git a/wai/quantum-conformance/reject/v2_n_qubits_256.wqc b/wai/quantum-conformance/reject/v2_n_qubits_256.wqc new file mode 100644 index 0000000000000000000000000000000000000000..d14a5b329ba99f0ad7c04c979e920dde3e498719 GIT binary patch literal 155 zcmWF!bT(pQU}OM-5+G&*5&S@0t(00(qGY91o|vguSelquQktumoLQ7ynpt9`Z>XfB zlowxEnv_{mtYl?mY6j$%=B5^9CPQ_kq?YI<<`$Tl02QVcB?5(w4Rn;!6H8KyQ$eOC k=VYd(06%Uhf&c&j literal 0 HcmV?d00001 diff --git a/wai/quantum-conformance/reject/v2_phase_0.wqc b/wai/quantum-conformance/reject/v2_phase_0.wqc new file mode 100644 index 0000000000000000000000000000000000000000..c0696e78971296ca1d75cba996dcf011657fb472 GIT binary patch literal 147 zcmWF!bT(pQU}OM-A|Pf05u89=t(00(qGY91o|vguSelquQktumoLQ7ynpt9`Z>XfB zlowxEnv_{mtYl>b MAX_DEPTH: + raise ValueError("nested too deeply") + items = v.items() if isinstance(v, dict) else enumerate(v) + for k, x in items: + if isinstance(k, str): + k.encode("utf-8") + walk(x, depth + 1) + elif isinstance(v, str): + v.encode("utf-8") + try: - return json.loads(raw.decode("utf-8"), parse_constant=no_constant) - except (UnicodeDecodeError, ValueError, RecursionError): + v = json.loads(raw.decode("utf-8"), parse_constant=no_constant, parse_float=finite, + parse_int=integer, object_pairs_hook=unique) + walk(v, 0) + return v + except (UnicodeDecodeError, UnicodeEncodeError, ValueError, RecursionError): return _NOT_JSON @@ -607,12 +647,41 @@ REJECTS = { "v1_n_qubits_258": ("malformed", "n_qubits"), "v1_contract_twice": ("malformed", "duplicate section"), "v1_oplog_twice": ("malformed", "duplicate section"), + "v1_contract_duplicate_member": ("contract", "not JSON"), + "v1_contract_lone_surrogate": ("contract", "not JSON"), + "v1_contract_too_deep": ("contract", "not JSON"), + "v1_n_qubits_minus0": ("malformed", "n_qubits"), + "v1_measure_duplicate_member": ("malformed", "measure JSON"), + "v1_contract_not_utf8": ("contract", "not JSON"), + "v1_contract_number_out_of_range": ("contract", "not JSON"), + "v1_n_qubits_fraction": ("malformed", "n_qubits"), + "v1_n_qubits_missing": ("malformed", "n_qubits"), + "v1_measure_not_json": ("malformed", "measure JSON"), + "short_payload": ("malformed", "magic"), + "table_past_end": ("malformed", "section table"), + "section_past_end": ("malformed", "section bounds"), + "unknown_section_past_end": ("malformed", "section bounds"), + "no_contract": ("malformed", "missing contract"), + "no_oplog": ("malformed", "missing op-log"), + "v1_oplog_short": ("malformed", "op-log header"), + "v1_op_truncated_controls": ("malformed", "op controls"), + "v1_opcode_10": ("malformed", "opcode"), + "v1_target_out_of_range": ("invalid", "target"), + "v1_control_out_of_range": ("invalid", "control"), + "v1_control_is_target": ("invalid", "control is target"), + "v1_phase_0": ("invalid", "phase index"), + "v1_phase_33": ("invalid", "phase index"), "v1_n_ops_u32max": ("malformed", "op header"), "v1_offset_wrap": ("malformed", "section bounds"), "v2_contract_whitespace": ("contract", "canonical contract"), "v2_contract_key_order": ("contract", "canonical contract"), "v2_contract_extra_member": ("contract", "canonical contract"), "v2_contract1_in_wqc2": ("contract", "canonical contract"), + "v2_n_qubits_256": ("malformed", "n_qubits"), + "v2_contract_not_object": ("contract", "not an object"), + "v2_phase_0": ("invalid", "phase index"), + "v2_measure_twice": ("malformed", "duplicate section"), + "v2_measure_seed_signed": ("malformed", "measure seed"), "v2_duplicate_controls": ("invalid", "repeated control"), "v2_param_on_h": ("invalid", "param"), "v2_oplog_trailing_byte": ("malformed", "trailing bytes"), diff --git a/wai/wai-pack/Cargo.lock b/wai/wai-pack/Cargo.lock index 11894f19c..b69d57caa 100644 --- a/wai/wai-pack/Cargo.lock +++ b/wai/wai-pack/Cargo.lock @@ -686,6 +686,7 @@ version = "0.4.0" dependencies = [ "blake3", "ed25519-dalek", + "serde", "serde_json", ] diff --git a/wai/wai-quantum-serve/Cargo.lock b/wai/wai-quantum-serve/Cargo.lock index b196c468c..ec3a4d98f 100644 --- a/wai/wai-quantum-serve/Cargo.lock +++ b/wai/wai-quantum-serve/Cargo.lock @@ -593,6 +593,7 @@ version = "0.4.0" dependencies = [ "blake3", "ed25519-dalek", + "serde", "serde_json", ] diff --git a/wai/wai-quantum/Cargo.lock b/wai/wai-quantum/Cargo.lock index 51a8fe778..d8d34a1da 100644 --- a/wai/wai-quantum/Cargo.lock +++ b/wai/wai-quantum/Cargo.lock @@ -419,6 +419,7 @@ version = "0.4.0" dependencies = [ "blake3", "ed25519-dalek", + "serde", "serde_json", ] diff --git a/wai/wai-quantum/Cargo.toml b/wai/wai-quantum/Cargo.toml index 9561e8e23..d2db8c404 100644 --- a/wai/wai-quantum/Cargo.toml +++ b/wai/wai-quantum/Cargo.toml @@ -23,6 +23,9 @@ all-features = true blake3 = "1" # Canonical receipt JSON (sorted keys, byte-stable round-trip). serde_json = { version = "1", features = ["preserve_order"] } +# Reading a container's JSON sections as I-JSON: a visitor refuses a member +# name repeated within an object. +serde = "1" # Receipt signatures. Optional — only the sealing features need it. ed25519-dalek = { version = "2", optional = true } diff --git a/wai/wai-quantum/src/quantum.rs b/wai/wai-quantum/src/quantum.rs index 731722ed8..d20db5f14 100644 --- a/wai/wai-quantum/src/quantum.rs +++ b/wai/wai-quantum/src/quantum.rs @@ -1022,17 +1022,75 @@ fn shown(v: Option<&serde_json::Value>) -> String { } } +/// A JSON text in which no object repeats a member name: the visitor walks +/// every value and refuses the second occurrence of a name. +struct Unique; + +impl<'de> serde::Deserialize<'de> for Unique { + fn deserialize>(d: D) -> Result { + struct Walk; + impl<'de> serde::de::Visitor<'de> for Walk { + type Value = Unique; + fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + f.write_str("a JSON value") + } + fn visit_bool(self, _: bool) -> Result { + Ok(Unique) + } + fn visit_i64(self, _: i64) -> Result { + Ok(Unique) + } + fn visit_u64(self, _: u64) -> Result { + Ok(Unique) + } + fn visit_f64(self, _: f64) -> Result { + Ok(Unique) + } + fn visit_str(self, _: &str) -> Result { + Ok(Unique) + } + fn visit_unit(self) -> Result { + Ok(Unique) + } + fn visit_seq>(self, mut seq: A) -> Result { + while seq.next_element::()?.is_some() {} + Ok(Unique) + } + fn visit_map>(self, mut map: A) -> Result { + let mut seen = std::collections::BTreeSet::new(); + while let Some(name) = map.next_key::()? { + if !seen.insert(name.clone()) { + return Err(serde::de::Error::custom(format!("member {name:?} appears twice"))); + } + map.next_value::()?; + } + Ok(Unique) + } + } + d.deserialize_any(Walk) + } +} + +/// A section read as I-JSON (RFC 7493), as §2 of the extension requires: +/// UTF-8, no unpaired surrogate escape, no number that rounds to an infinite +/// double, at most 127 nested arrays and objects, and no member name twice in +/// one object. +fn i_json(raw: &[u8]) -> Result { + serde_json::from_slice::(raw).map_err(|e| e.to_string())?; + serde_json::from_slice(raw).map_err(|e: serde_json::Error| e.to_string()) +} + /// Read a contract section under the gateset its container's magic names, and /// return its `n_qubits`. /// -/// The section must be a JSON object, or it is a contract mismatch, and +/// The section must be an I-JSON object ([`i_json`]), or it is a contract +/// mismatch, and /// `n_qubits` must be a JSON unsigned integer of at most 255, or the container /// is malformed. circuit/1 requires `ext`, `numeric`, `frac` (the integer 30) /// and `gateset` to carry its values, and leaves member order, whitespace and /// other members free. circuit2 requires the canonical bytes exactly. fn read_contract(gateset: Gateset, raw: &[u8]) -> Result { - let cv: serde_json::Value = serde_json::from_slice(raw) - .map_err(|e| QuantumError::Contract(format!("the contract is not JSON: {e}")))?; + let cv = i_json(raw).map_err(|e| QuantumError::Contract(format!("the contract is not I-JSON: {e}")))?; let Some(obj) = cv.as_object() else { return Err(QuantumError::Contract(format!("the contract is not a JSON object: {}", shown(Some(&cv))))); }; @@ -1074,13 +1132,13 @@ fn read_contract(gateset: Gateset, raw: &[u8]) -> Result { } } -/// Read a measurement section. circuit/1 reads it as it always has: the JSON -/// must parse, and a missing or non-integer `seed` or `shots` reads as 0. +/// Read a measurement section. It must be I-JSON ([`i_json`]). circuit/1 then +/// reads it as it always has: a missing or non-integer `seed` or `shots` reads +/// as 0. /// circuit2 requires a `u64` `seed` and `shots` and the canonical bytes for /// them, which fixes the basis as `computational`. fn read_measure(gateset: Gateset, raw: &[u8]) -> Result { - let mv: serde_json::Value = - serde_json::from_slice(raw).map_err(|e| QuantumError::Malformed(format!("measure json: {e}")))?; + let mv = i_json(raw).map_err(|e| QuantumError::Malformed(format!("the measurement is not I-JSON: {e}")))?; match gateset { Gateset::V1 => Ok(Measure { seed: mv["seed"].as_u64().unwrap_or(0), shots: mv["shots"].as_u64().unwrap_or(0) }), Gateset::V2 => { @@ -1150,16 +1208,21 @@ pub fn to_wqc(c: &Circuit, measure: Option) -> Result, QuantumE /// known section (`0x01`, `0x02`, `0x03`) appears twice; the contract and /// op-log are present. Unknown sections are ignored. Else malformed. /// 3. The contract declares the capability the magic names. In `WQC1` it must -/// be a JSON object carrying circuit/1's `ext`, `numeric`, `frac` and +/// be an I-JSON object carrying circuit/1's `ext`, `numeric`, `frac` and /// `gateset`, in any formatting; in `WQC2` it must be the canonical bytes. /// Else a contract mismatch, except that an `n_qubits` that is not an -/// integer in `0..=255` is malformed. +/// unsigned JSON integer in `0..=255` is malformed. /// 4. The op-log parses within its section: every op lies inside it and has /// an opcode of at most 9. In `WQC2` it must end exactly after its last op; /// in `WQC1` any bytes after it are ignored. Else malformed. /// 5. The circuit passes `validate` under its gateset, else invalid. -/// 6. The measurement, if present, is JSON, and in `WQC2` it is the canonical -/// bytes. Else malformed. +/// 6. The measurement, if present, is I-JSON, and in `WQC2` it is the +/// canonical bytes. Else malformed. +/// +/// I-JSON is extensions/quantum-sim §2's reading of a section: UTF-8, no +/// member name twice in one object, no unpaired surrogate escape, no number +/// beyond the double range, and at most 127 nested arrays and objects. +/// [`reject_corpus`] holds a payload for each refusal. pub fn from_wqc(bytes: &[u8]) -> Result<(Circuit, Option), QuantumError> { if bytes.len() < 6 { return Err(QuantumError::Malformed("magic".into())); @@ -1232,6 +1295,197 @@ pub fn circuit_hash(c: &Circuit) -> Result<[u8; 32], QuantumError> { Ok(*h.finalize().as_bytes()) } +// ---- The reject corpus --------------------------------------------------------- + +/// A container from raw section-table entries `(kind, offset, len)` and a blob. +fn wqc_table(magic: &[u8; 4], table: &[(u8, u32, u32)], blob: &[u8]) -> Vec { + let mut out = magic.to_vec(); + out.extend_from_slice(&u16::try_from(table.len()).expect("a short table").to_le_bytes()); + for &(kind, off, len) in table { + out.push(kind); + out.extend_from_slice(&off.to_le_bytes()); + out.extend_from_slice(&len.to_le_bytes()); + } + out.extend_from_slice(blob); + out +} + +/// A container holding `sections` in order, each table entry pointing at its +/// own bytes. +fn wqc_raw(magic: &[u8; 4], sections: &[(u8, &[u8])]) -> Vec { + let (mut table, mut blob) = (Vec::new(), Vec::new()); + for &(kind, data) in sections { + let at = |n: usize| u32::try_from(n).expect("a small section"); + table.push((kind, at(blob.len()), at(data.len()))); + blob.extend_from_slice(data); + } + wqc_table(magic, &table, &blob) +} + +/// One op-log entry, as the wire has it. +fn op(opcode: u8, controls: &[u8], target: u8, param: u16) -> Vec { + let mut out = vec![opcode, u8::try_from(controls.len()).expect("a few controls"), target]; + out.extend_from_slice(¶m.to_le_bytes()); + out.extend_from_slice(controls); + out +} + +/// An op-log holding `ops`. +fn oplog_of(ops: &[Vec]) -> Vec { + let mut out = u32::try_from(ops.len()).expect("a few ops").to_le_bytes().to_vec(); + for o in ops { + out.extend_from_slice(o); + } + out +} + +/// `bytes` with the first `from` replaced by `to`; `from` must be there. +fn swap(bytes: &[u8], from: &str, to: &str) -> Vec { + let s = std::str::from_utf8(bytes).expect("a contract is text"); + assert!(s.contains(from), "{from:?} is not in {s}"); + s.replacen(from, to, 1).into_bytes() +} + +/// The reject corpus of extensions/quantum-sim §2, as `(name, payload, class)`: +/// payloads a reader must refuse, each with the class [`QuantumError::kind`] +/// gives it. Each is a two-qubit Bell container with one rule broken and every +/// rule checked before it kept, so the check order of [`from_wqc`] decides the +/// class. The conformance corpus's `reject/` tree is written from this list. +pub fn reject_corpus() -> Vec<(&'static str, Vec, &'static str)> { + let bell = oplog_of(&[op(4, &[], 0, 0), op(1, &[0], 1, 0)]); + let canon1 = Gateset::V1.contract_bytes(2); + let canon2 = Gateset::V2.contract_bytes(2); + let v1 = |contract: &[u8]| wqc_raw(b"WQC1", &[(SECT_CONTRACT, contract), (SECT_OPLOG, &bell)]); + let v1_oplog = |oplog: &[u8]| wqc_raw(b"WQC1", &[(SECT_CONTRACT, &canon1), (SECT_OPLOG, oplog)]); + let v2 = |contract: &[u8], oplog: &[u8], measure: Option<&[u8]>| { + let mut sections = vec![(SECT_CONTRACT, contract), (SECT_OPLOG, oplog)]; + sections.extend(measure.map(|m| (SECT_MEASURE, m))); + wqc_raw(b"WQC2", §ions) + }; + let v2_contract = |contract: &[u8]| v2(contract, &bell, None); + let v2_oplog = |oplog: &[u8]| v2(&canon2, oplog, None); + let v2_measure = |measure: &[u8]| v2(&canon2, &bell, Some(measure)); + let mut magic_wqc3 = v2_contract(&canon2); + magic_wqc3[0..4].copy_from_slice(b"WQC3"); + // An op-log of 9 bytes that claims u32::MAX ops. + let n_ops_u32max = [u32::MAX.to_le_bytes().as_slice(), &op(4, &[], 0, 0)].concat(); + // An op-log section whose offset plus length wraps a u32. + let wrap = wqc_table( + b"WQC1", + &[(SECT_CONTRACT, 0, u32::try_from(canon1.len()).expect("a short contract")), (SECT_OPLOG, 0xFFFF_FFF0, 0x20)], + &[canon1.as_slice(), &bell].concat(), + ); + // The Bell container cut off inside its section table: two entries claimed, one present. + let mut table_past_end = v1(&canon1); + table_past_end.truncate(6 + 9); + let at = |n: usize| u32::try_from(n).expect("a short section"); + let (c1, b1) = (at(canon1.len()), at(bell.len())); + let both = [canon1.as_slice(), &bell].concat(); + // A contract whose last member's string holds the byte 0xFF. + let not_utf8 = [&canon1[..canon1.len() - 1], b",\"note\":\"\xff\"}"].concat(); + // An op that names two controls where the section holds one. + let truncated_controls = [1u32.to_le_bytes().as_slice(), &[1, 2, 1, 0, 0, 0]].concat(); + let measure = b"{\"seed\":1,\"shots\":10,\"basis\":\"computational\"}"; + // A circuit/1 contract nesting 127 arrays inside its object: 128 levels, + // one past what §2.1 allows. + let deep = [&canon1[..canon1.len() - 1], b",\"x\":", &b"[".repeat(127), &b"]".repeat(127), b"}"].concat(); + let reordered2 = format!( + r#"{{"n_qubits":2,"ext":"{}","numeric":"{}","frac":30,"gateset":"{}"}}"#, + Gateset::V2.ext(), + Gateset::V2.numeric(), + Gateset::V2.name() + ); + vec![ + ("magic_wqc3", magic_wqc3, "malformed"), + ("v1_ext_other", v1(&swap(&canon1, "circuit/1", "circuit/2")), "contract"), + ("v1_numeric_other", v1(&swap(&canon1, "wai.det.fixed64", "wai.det.amp64")), "contract"), + ("v1_frac_31", v1(&swap(&canon1, "\"frac\":30", "\"frac\":31")), "contract"), + ("v1_frac_float", v1(&swap(&canon1, "\"frac\":30", "\"frac\":30.0")), "contract"), + ("v1_gateset_rn", v1(&swap(&canon1, "dyadicP\"", "dyadicP.rn\"")), "contract"), + ("v1_contract2_in_wqc1", v1(&canon2), "contract"), + ("v1_fields_missing", v1(br#"{"n_qubits":3}"#), "contract"), + ("v1_contract_not_object", v1(&[b"[".as_slice(), &canon1, b"]"].concat()), "contract"), + ("v1_n_qubits_256", v1(&swap(&canon1, "\"n_qubits\":2", "\"n_qubits\":256")), "malformed"), + ("v1_n_qubits_258", v1(&swap(&canon1, "\"n_qubits\":2", "\"n_qubits\":258")), "malformed"), + ( + "v1_contract_twice", + wqc_raw(b"WQC1", &[(SECT_CONTRACT, &canon1), (SECT_OPLOG, &bell), (SECT_CONTRACT, &Gateset::V1.contract_bytes(3))]), + "malformed", + ), + ( + "v1_oplog_twice", + wqc_raw(b"WQC1", &[(SECT_CONTRACT, &canon1), (SECT_OPLOG, &bell), (SECT_OPLOG, &oplog_of(&[op(1, &[], 0, 0)]))]), + "malformed", + ), + // §2.1: the contract is I-JSON. + ("v1_contract_duplicate_member", v1(&swap(&canon1, "\"frac\":30", "\"frac\":30,\"frac\":30")), "contract"), + ("v1_contract_lone_surrogate", v1(&swap(&canon1, "}", ",\"note\":\"\\ud800\"}")), "contract"), + ("v1_contract_too_deep", v1(&deep), "contract"), + ("v1_n_qubits_minus0", v1(&swap(&canon1, "\"n_qubits\":2", "\"n_qubits\":-0")), "malformed"), + // §2.3: so is the measurement. + ( + "v1_measure_duplicate_member", + wqc_raw(b"WQC1", &[(SECT_CONTRACT, &canon1), (SECT_OPLOG, &bell), (SECT_MEASURE, br#"{"seed":1,"seed":2,"shots":10}"#)]), + "malformed", + ), + ("v1_contract_not_utf8", v1(¬_utf8), "contract"), + ("v1_contract_number_out_of_range", v1(&swap(&canon1, "}", ",\"x\":1e400}")), "contract"), + ("v1_n_qubits_fraction", v1(&swap(&canon1, "\"n_qubits\":2", "\"n_qubits\":2.0")), "malformed"), + ("v1_n_qubits_missing", v1(&swap(&canon1, "\"n_qubits\":2,", "")), "malformed"), + ( + "v1_measure_not_json", + wqc_raw(b"WQC1", &[(SECT_CONTRACT, &canon1), (SECT_OPLOG, &bell), (SECT_MEASURE, br#"{"seed":1,"#)]), + "malformed", + ), + // §2: the payload, its table and its sections. + ("short_payload", b"WQC2\x00".to_vec(), "malformed"), + ("table_past_end", table_past_end, "malformed"), + ("section_past_end", wqc_table(b"WQC1", &[(SECT_CONTRACT, 0, c1), (SECT_OPLOG, c1, b1 + 1)], &both), "malformed"), + ( + "unknown_section_past_end", + wqc_table(b"WQC1", &[(SECT_CONTRACT, 0, c1), (SECT_OPLOG, c1, b1), (0x7F, c1 + b1, 1)], &both), + "malformed", + ), + ("no_contract", wqc_raw(b"WQC1", &[(SECT_OPLOG, &bell)]), "malformed"), + ("no_oplog", wqc_raw(b"WQC1", &[(SECT_CONTRACT, &canon1)]), "malformed"), + // §2.2: the op-log parses, and the circuit keeps its rules. + ("v1_oplog_short", v1_oplog(&[0, 0, 0]), "malformed"), + ("v1_op_truncated_controls", v1_oplog(&truncated_controls), "malformed"), + ("v1_opcode_10", v1_oplog(&oplog_of(&[op(10, &[], 0, 0)])), "malformed"), + ("v1_target_out_of_range", v1_oplog(&oplog_of(&[op(4, &[], 2, 0)])), "invalid"), + ("v1_control_out_of_range", v1_oplog(&oplog_of(&[op(1, &[2], 1, 0)])), "invalid"), + ("v1_control_is_target", v1_oplog(&oplog_of(&[op(1, &[1], 1, 0)])), "invalid"), + ("v1_phase_0", v1_oplog(&oplog_of(&[op(9, &[], 0, 0)])), "invalid"), + ("v1_phase_33", v1_oplog(&oplog_of(&[op(9, &[], 0, 33)])), "invalid"), + ("v1_n_ops_u32max", v1_oplog(&n_ops_u32max), "malformed"), + ("v1_offset_wrap", wrap, "malformed"), + ("v2_contract_whitespace", v2_contract(&swap(&canon2, "\"n_qubits\":2", "\"n_qubits\": 2")), "contract"), + ("v2_contract_key_order", v2_contract(reordered2.as_bytes()), "contract"), + ("v2_contract_extra_member", v2_contract(&swap(&canon2, "}", ",\"note\":\"x\"}")), "contract"), + ("v2_contract1_in_wqc2", v2_contract(&canon1), "contract"), + ("v2_n_qubits_256", v2_contract(&swap(&canon2, "\"n_qubits\":2", "\"n_qubits\":256")), "malformed"), + ("v2_contract_not_object", v2_contract(&[b"[".as_slice(), &canon2, b"]"].concat()), "contract"), + ("v2_phase_0", v2_oplog(&oplog_of(&[op(9, &[], 0, 0)])), "invalid"), + ( + "v2_measure_twice", + wqc_raw(b"WQC2", &[(SECT_CONTRACT, &canon2), (SECT_OPLOG, &bell), (SECT_MEASURE, measure), (SECT_MEASURE, measure)]), + "malformed", + ), + ("v2_measure_seed_signed", v2_measure(br#"{"seed":-1,"shots":10,"basis":"computational"}"#), "malformed"), + ( + "v2_duplicate_controls", + v2(&Gateset::V2.contract_bytes(3), &oplog_of(&[op(1, &[0, 0], 2, 0)]), None), + "invalid", + ), + ("v2_param_on_h", v2_oplog(&oplog_of(&[op(4, &[], 0, 7), op(1, &[0], 1, 0)])), "invalid"), + ("v2_oplog_trailing_byte", v2_oplog(&[bell.as_slice(), &[0]].concat()), "malformed"), + ("v2_opcode_10", v2_oplog(&oplog_of(&[op(10, &[], 0, 0)])), "malformed"), + ("v2_measure_spaced", v2_measure(br#"{"seed": 1, "shots": 10, "basis": "computational"}"#), "malformed"), + ("v2_measure_no_shots", v2_measure(br#"{"seed":1,"basis":"computational"}"#), "malformed"), + ("v2_measure_basis_x", v2_measure(br#"{"seed":1,"shots":10,"basis":"x"}"#), "malformed"), + ] +} + #[cfg(test)] mod tests { use super::*; @@ -1701,54 +1955,6 @@ mod tests { ] } - /// A container from raw table entries `(kind, offset, len)` and a blob. - fn wqc_table(magic: &[u8; 4], table: &[(u8, u32, u32)], blob: &[u8]) -> Vec { - let mut out = magic.to_vec(); - out.extend_from_slice(&(table.len() as u16).to_le_bytes()); - for &(kind, off, len) in table { - out.push(kind); - out.extend_from_slice(&off.to_le_bytes()); - out.extend_from_slice(&len.to_le_bytes()); - } - out.extend_from_slice(blob); - out - } - - /// A container holding `sections` in order, each table entry pointing at - /// its own bytes. - fn wqc_raw(magic: &[u8; 4], sections: &[(u8, &[u8])]) -> Vec { - let (mut table, mut blob) = (Vec::new(), Vec::new()); - for &(kind, data) in sections { - table.push((kind, blob.len() as u32, data.len() as u32)); - blob.extend_from_slice(data); - } - wqc_table(magic, &table, &blob) - } - - /// One op-log entry, written as the wire has it. - fn op(opcode: u8, controls: &[u8], target: u8, param: u16) -> Vec { - let mut out = vec![opcode, controls.len() as u8, target]; - out.extend_from_slice(¶m.to_le_bytes()); - out.extend_from_slice(controls); - out - } - - /// An op-log holding `ops`. - fn oplog_of(ops: &[Vec]) -> Vec { - let mut out = (ops.len() as u32).to_le_bytes().to_vec(); - for o in ops { - out.extend_from_slice(o); - } - out - } - - /// `bytes` with the first `from` replaced by `to`; `from` must be there. - fn swap(bytes: &[u8], from: &str, to: &str) -> Vec { - let s = std::str::from_utf8(bytes).unwrap(); - assert!(s.contains(from), "{from:?} is not in {s}"); - s.replacen(from, to, 1).into_bytes() - } - /// A circuit/1 container is written byte for byte as 0.3.x wrote it: the /// four corpus files reproduce, and so do their manifest circuit hashes. /// The measurement, now rendered directly, matches the serializer's render. @@ -1930,79 +2136,6 @@ mod tests { } } - /// The reject corpus: each case is an accepted file with one defect, named - /// with the error class a reader must give it. - fn reject_cases() -> Vec<(&'static str, Vec, &'static str)> { - let bell = oplog_of(&[op(4, &[], 0, 0), op(1, &[0], 1, 0)]); - let canon1 = Gateset::V1.contract_bytes(2); - let canon2 = Gateset::V2.contract_bytes(2); - let v1 = |contract: &[u8]| wqc_raw(b"WQC1", &[(SECT_CONTRACT, contract), (SECT_OPLOG, &bell)]); - let v1_oplog = |oplog: &[u8]| wqc_raw(b"WQC1", &[(SECT_CONTRACT, &canon1), (SECT_OPLOG, oplog)]); - let v2 = |contract: &[u8], oplog: &[u8], measure: Option<&[u8]>| { - let mut sections = vec![(SECT_CONTRACT, contract), (SECT_OPLOG, oplog)]; - sections.extend(measure.map(|m| (SECT_MEASURE, m))); - wqc_raw(b"WQC2", §ions) - }; - let v2_contract = |contract: &[u8]| v2(contract, &bell, None); - let v2_oplog = |oplog: &[u8]| v2(&canon2, oplog, None); - let v2_measure = |measure: &[u8]| v2(&canon2, &bell, Some(measure)); - let mut magic_wqc3 = v2_contract(&canon2); - magic_wqc3[0..4].copy_from_slice(b"WQC3"); - let n_ops_u32max = [u32::MAX.to_le_bytes().as_slice(), &op(4, &[], 0, 0)].concat(); - assert_eq!(n_ops_u32max.len(), 9); - let wrap = wqc_table( - b"WQC1", - &[(SECT_CONTRACT, 0, canon1.len() as u32), (SECT_OPLOG, 0xFFFF_FFF0, 0x20)], - &[canon1.as_slice(), &bell].concat(), - ); - let reordered2 = format!( - r#"{{"n_qubits":2,"ext":"{}","numeric":"{}","frac":30,"gateset":"{}"}}"#, - Gateset::V2.ext(), - Gateset::V2.numeric(), - Gateset::V2.name() - ); - vec![ - ("magic_wqc3", magic_wqc3, "malformed"), - ("v1_ext_other", v1(&swap(&canon1, "circuit/1", "circuit/2")), "contract"), - ("v1_numeric_other", v1(&swap(&canon1, "wai.det.fixed64", "wai.det.amp64")), "contract"), - ("v1_frac_31", v1(&swap(&canon1, "\"frac\":30", "\"frac\":31")), "contract"), - ("v1_frac_float", v1(&swap(&canon1, "\"frac\":30", "\"frac\":30.0")), "contract"), - ("v1_gateset_rn", v1(&swap(&canon1, "dyadicP\"", "dyadicP.rn\"")), "contract"), - ("v1_contract2_in_wqc1", v1(&canon2), "contract"), - ("v1_fields_missing", v1(br#"{"n_qubits":3}"#), "contract"), - ("v1_contract_not_object", v1(&[b"[".as_slice(), &canon1, b"]"].concat()), "contract"), - ("v1_n_qubits_256", v1(&swap(&canon1, "\"n_qubits\":2", "\"n_qubits\":256")), "malformed"), - ("v1_n_qubits_258", v1(&swap(&canon1, "\"n_qubits\":2", "\"n_qubits\":258")), "malformed"), - ( - "v1_contract_twice", - wqc_raw(b"WQC1", &[(SECT_CONTRACT, &canon1), (SECT_OPLOG, &bell), (SECT_CONTRACT, &Gateset::V1.contract_bytes(3))]), - "malformed", - ), - ( - "v1_oplog_twice", - wqc_raw(b"WQC1", &[(SECT_CONTRACT, &canon1), (SECT_OPLOG, &bell), (SECT_OPLOG, &oplog_of(&[op(1, &[], 0, 0)]))]), - "malformed", - ), - ("v1_n_ops_u32max", v1_oplog(&n_ops_u32max), "malformed"), - ("v1_offset_wrap", wrap, "malformed"), - ("v2_contract_whitespace", v2_contract(&swap(&canon2, "\"n_qubits\":2", "\"n_qubits\": 2")), "contract"), - ("v2_contract_key_order", v2_contract(reordered2.as_bytes()), "contract"), - ("v2_contract_extra_member", v2_contract(&swap(&canon2, "}", ",\"note\":\"x\"}")), "contract"), - ("v2_contract1_in_wqc2", v2_contract(&canon1), "contract"), - ( - "v2_duplicate_controls", - v2(&Gateset::V2.contract_bytes(3), &oplog_of(&[op(1, &[0, 0], 2, 0)]), None), - "invalid", - ), - ("v2_param_on_h", v2_oplog(&oplog_of(&[op(4, &[], 0, 7), op(1, &[0], 1, 0)])), "invalid"), - ("v2_oplog_trailing_byte", v2_oplog(&[bell.as_slice(), &[0]].concat()), "malformed"), - ("v2_opcode_10", v2_oplog(&oplog_of(&[op(10, &[], 0, 0)])), "malformed"), - ("v2_measure_spaced", v2_measure(br#"{"seed": 1, "shots": 10, "basis": "computational"}"#), "malformed"), - ("v2_measure_no_shots", v2_measure(br#"{"seed":1,"basis":"computational"}"#), "malformed"), - ("v2_measure_basis_x", v2_measure(br#"{"seed":1,"shots":10,"basis":"x"}"#), "malformed"), - ] - } - /// Every case of the reject corpus fails with its class; so do the /// structural edge cases; the rules apply in the order the spec gives. /// More than 255 controls is refused everywhere a circuit is checked, and @@ -2023,10 +2156,11 @@ mod tests { from_wqc(&file).unwrap(); } - let cases = reject_cases(); - assert_eq!(cases.len(), 26); + let cases = reject_corpus(); let names: std::collections::BTreeSet<_> = cases.iter().map(|c| c.0).collect(); assert_eq!(names.len(), cases.len(), "case names repeat"); + let kinds: std::collections::BTreeSet<_> = cases.iter().map(|c| c.2).collect(); + assert_eq!(kinds, std::collections::BTreeSet::from(["contract", "invalid", "malformed"])); for (name, file, kind) in &cases { match from_wqc(file) { Ok(c) => panic!("{name} was accepted: {c:?}"), diff --git a/wai/wai-rs/Cargo.lock b/wai/wai-rs/Cargo.lock index f43953910..da53a5114 100644 --- a/wai/wai-rs/Cargo.lock +++ b/wai/wai-rs/Cargo.lock @@ -3904,6 +3904,7 @@ version = "0.4.0" dependencies = [ "blake3", "ed25519-dalek", + "serde", "serde_json", ] diff --git a/wai/wai-rs/src/bin/wai_quantum_vectors.rs b/wai/wai-rs/src/bin/wai_quantum_vectors.rs index ef8cf8b82..8317b03f1 100644 --- a/wai/wai-rs/src/bin/wai_quantum_vectors.rs +++ b/wai/wai-rs/src/bin/wai_quantum_vectors.rs @@ -28,7 +28,7 @@ use std::collections::BTreeSet; use std::path::Path; -use wai::quantum::{circuit_hash, from_wqc, to_wqc, BaseGate, Circuit, Gate, Gateset, Measure, DYADIC_MAX}; +use wai::quantum::{circuit_hash, from_wqc, reject_corpus, to_wqc, BaseGate, Circuit, Gate, Gateset, Measure, DYADIC_MAX}; /// The circuit2 tree, under the corpus directory. const V2_DIR: &str = "v2"; @@ -154,143 +154,13 @@ fn tree_files(prefix: &str, gs: Gateset, name_capability: bool) -> Vec<(String, files } -// ---- The reject corpus --------------------------------------------------------- - -const SECT_CONTRACT: u8 = 0x01; -const SECT_OPLOG: u8 = 0x02; -const SECT_MEASURE: u8 = 0x03; - -/// A container from raw section-table entries `(kind, offset, len)` and a blob. -fn wqc_table(magic: &[u8; 4], table: &[(u8, u32, u32)], blob: &[u8]) -> Vec { - let mut out = magic.to_vec(); - out.extend_from_slice(&u16::try_from(table.len()).expect("a short table").to_le_bytes()); - for &(kind, off, len) in table { - out.push(kind); - out.extend_from_slice(&off.to_le_bytes()); - out.extend_from_slice(&len.to_le_bytes()); - } - out.extend_from_slice(blob); - out -} - -/// A container holding `sections` in order, each table entry pointing at its -/// own bytes. -fn wqc_raw(magic: &[u8; 4], sections: &[(u8, &[u8])]) -> Vec { - let (mut table, mut blob) = (Vec::new(), Vec::new()); - for &(kind, data) in sections { - let at = |n: usize| u32::try_from(n).expect("a small section"); - table.push((kind, at(blob.len()), at(data.len()))); - blob.extend_from_slice(data); - } - wqc_table(magic, &table, &blob) -} - -/// One op-log entry, as the wire has it. -fn op(opcode: u8, controls: &[u8], target: u8, param: u16) -> Vec { - let mut out = vec![opcode, u8::try_from(controls.len()).expect("a few controls"), target]; - out.extend_from_slice(¶m.to_le_bytes()); - out.extend_from_slice(controls); - out -} - -/// An op-log holding `ops`. -fn oplog_of(ops: &[Vec]) -> Vec { - let mut out = u32::try_from(ops.len()).expect("a few ops").to_le_bytes().to_vec(); - for o in ops { - out.extend_from_slice(o); - } - out -} - -/// `bytes` with the first `from` replaced by `to`; `from` must be there. -fn swap(bytes: &[u8], from: &str, to: &str) -> Vec { - let s = std::str::from_utf8(bytes).expect("a contract is text"); - assert!(s.contains(from), "{from:?} is not in {s}"); - s.replacen(from, to, 1).into_bytes() -} - -/// The reject corpus: (name, file, the class a reader refuses it as). Each case -/// is a valid two-qubit Bell container with one thing broken, so that the -/// listed rule is the one that refuses it. -fn reject_cases() -> Vec<(&'static str, Vec, &'static str)> { - let bell = oplog_of(&[op(4, &[], 0, 0), op(1, &[0], 1, 0)]); - let canon1 = Gateset::V1.contract_bytes(2); - let canon2 = Gateset::V2.contract_bytes(2); - let v1 = |contract: &[u8]| wqc_raw(b"WQC1", &[(SECT_CONTRACT, contract), (SECT_OPLOG, &bell)]); - let v1_oplog = |oplog: &[u8]| wqc_raw(b"WQC1", &[(SECT_CONTRACT, &canon1), (SECT_OPLOG, oplog)]); - let v2 = |contract: &[u8], oplog: &[u8], measure: Option<&[u8]>| { - let mut sections = vec![(SECT_CONTRACT, contract), (SECT_OPLOG, oplog)]; - sections.extend(measure.map(|m| (SECT_MEASURE, m))); - wqc_raw(b"WQC2", §ions) - }; - let v2_contract = |contract: &[u8]| v2(contract, &bell, None); - let v2_oplog = |oplog: &[u8]| v2(&canon2, oplog, None); - let v2_measure = |measure: &[u8]| v2(&canon2, &bell, Some(measure)); - let mut magic_wqc3 = v2_contract(&canon2); - magic_wqc3[0..4].copy_from_slice(b"WQC3"); - // An op-log of 9 bytes that claims u32::MAX ops. - let n_ops_u32max = [u32::MAX.to_le_bytes().as_slice(), &op(4, &[], 0, 0)].concat(); - // An op-log section whose offset plus length wraps a u32. - let wrap = wqc_table( - b"WQC1", - &[(SECT_CONTRACT, 0, u32::try_from(canon1.len()).expect("a short contract")), (SECT_OPLOG, 0xFFFF_FFF0, 0x20)], - &[canon1.as_slice(), &bell].concat(), - ); - let reordered2 = format!( - r#"{{"n_qubits":2,"ext":"{}","numeric":"{}","frac":30,"gateset":"{}"}}"#, - Gateset::V2.ext(), - Gateset::V2.numeric(), - Gateset::V2.name() - ); - vec![ - ("magic_wqc3", magic_wqc3, "malformed"), - ("v1_ext_other", v1(&swap(&canon1, "circuit/1", "circuit/2")), "contract"), - ("v1_numeric_other", v1(&swap(&canon1, "wai.det.fixed64", "wai.det.amp64")), "contract"), - ("v1_frac_31", v1(&swap(&canon1, "\"frac\":30", "\"frac\":31")), "contract"), - ("v1_frac_float", v1(&swap(&canon1, "\"frac\":30", "\"frac\":30.0")), "contract"), - ("v1_gateset_rn", v1(&swap(&canon1, "dyadicP\"", "dyadicP.rn\"")), "contract"), - ("v1_contract2_in_wqc1", v1(&canon2), "contract"), - ("v1_fields_missing", v1(br#"{"n_qubits":3}"#), "contract"), - ("v1_contract_not_object", v1(&[b"[".as_slice(), &canon1, b"]"].concat()), "contract"), - ("v1_n_qubits_256", v1(&swap(&canon1, "\"n_qubits\":2", "\"n_qubits\":256")), "malformed"), - ("v1_n_qubits_258", v1(&swap(&canon1, "\"n_qubits\":2", "\"n_qubits\":258")), "malformed"), - ( - "v1_contract_twice", - wqc_raw(b"WQC1", &[(SECT_CONTRACT, &canon1), (SECT_OPLOG, &bell), (SECT_CONTRACT, &Gateset::V1.contract_bytes(3))]), - "malformed", - ), - ( - "v1_oplog_twice", - wqc_raw(b"WQC1", &[(SECT_CONTRACT, &canon1), (SECT_OPLOG, &bell), (SECT_OPLOG, &oplog_of(&[op(1, &[], 0, 0)]))]), - "malformed", - ), - ("v1_n_ops_u32max", v1_oplog(&n_ops_u32max), "malformed"), - ("v1_offset_wrap", wrap, "malformed"), - ("v2_contract_whitespace", v2_contract(&swap(&canon2, "\"n_qubits\":2", "\"n_qubits\": 2")), "contract"), - ("v2_contract_key_order", v2_contract(reordered2.as_bytes()), "contract"), - ("v2_contract_extra_member", v2_contract(&swap(&canon2, "}", ",\"note\":\"x\"}")), "contract"), - ("v2_contract1_in_wqc2", v2_contract(&canon1), "contract"), - ( - "v2_duplicate_controls", - v2(&Gateset::V2.contract_bytes(3), &oplog_of(&[op(1, &[0, 0], 2, 0)]), None), - "invalid", - ), - ("v2_param_on_h", v2_oplog(&oplog_of(&[op(4, &[], 0, 7), op(1, &[0], 1, 0)])), "invalid"), - ("v2_oplog_trailing_byte", v2_oplog(&[bell.as_slice(), &[0]].concat()), "malformed"), - ("v2_opcode_10", v2_oplog(&oplog_of(&[op(10, &[], 0, 0)])), "malformed"), - ("v2_measure_spaced", v2_measure(br#"{"seed": 1, "shots": 10, "basis": "computational"}"#), "malformed"), - ("v2_measure_no_shots", v2_measure(br#"{"seed":1,"basis":"computational"}"#), "malformed"), - ("v2_measure_basis_x", v2_measure(br#"{"seed":1,"shots":10,"basis":"x"}"#), "malformed"), - ] -} - /// The reject tree: `.wqc` for each case and the listing, as (path under /// `prefix`, bytes). Each case is checked here to fail with its class, so `gen` /// never writes a reject file the reader accepts. fn reject_files(prefix: &str) -> Vec<(String, Vec)> { let mut files = Vec::new(); let mut listing = serde_json::Map::new(); - for (name, bytes, kind) in reject_cases() { + for (name, bytes, kind) in reject_corpus() { match from_wqc(&bytes) { Ok(_) => panic!("reject case {name} is accepted"), Err(e) => assert_eq!(e.kind(), kind, "reject case {name}: {e}"), @@ -533,7 +403,9 @@ mod tests { fn the_committed_corpus_is_what_gen_writes() { let dir = corpus(); let files = corpus_files(); - assert_eq!(files.len(), 2 * 8 + 27); + // Two trees of seven vectors and a manifest, and the reject tree: a + // file per case and the listing. + assert_eq!(files.len(), 2 * 8 + reject_corpus().len() + 1); for (rel, want) in files { let mut got = std::fs::read(dir.join(&rel)).unwrap_or_else(|e| panic!("{rel}: {e}")); if rel.ends_with(".json") { @@ -572,18 +444,6 @@ mod tests { assert_eq!(rows, Gateset::ALL.len() * DYADIC_MAX); } - /// The reject corpus is the plan's 26 cases, named once each. - #[test] - fn every_reject_case_is_refused_as_listed() { - let cases = reject_cases(); - assert_eq!(cases.len(), 26); - let names: BTreeSet<_> = cases.iter().map(|c| c.0).collect(); - assert_eq!(names.len(), cases.len()); - for (name, bytes, kind) in &cases { - assert_eq!(from_wqc(bytes).map(|_| ()).unwrap_err().kind(), *kind, "{name}"); - } - } - /// `verify` fails a corpus whose v2 entry names the other capability, whose /// reject listing names an accepted file or the wrong class, or that holds a /// file its listing does not. diff --git a/wai/wai-rs/src/codecs/mod.rs b/wai/wai-rs/src/codecs/mod.rs index d6029fd01..a9318580e 100644 --- a/wai/wai-rs/src/codecs/mod.rs +++ b/wai/wai-rs/src/codecs/mod.rs @@ -295,13 +295,14 @@ pub const CAP_AVATAR_POSE: &str = "wai.avatar.pose"; pub const CAP_QUANTUM_CIRCUIT: &str = "wai.quantum.circuit"; // The successor of wai.quantum.circuit (extensions/quantum-sim): the same WQC -// layout under magic WQC2, with a correctly rounded phase table (every P(k) within -// half a unit of 2^30·e^{2πi/2^k}, so the controlled-phase ladder gives the QFT), -// a byte-canonical contract and a strict op-log. A new capability rather than a -// change to the old one, because a registered capability's payload format is fixed -// (SPEC §8): a reader that predates it rejects a WQC2 payload at the magic, and an -// envelope under this string is inert to it. Same equivalence, tier and replicate -// class as wai.quantum.circuit. Engine behind the `quantum` feature. +// layout under magic WQC2, with a correctly rounded phase table (each component +// of every P(k) within half a unit of 2^30·e^{2πi/2^k}, so the controlled-phase +// ladder gives the QFT), a byte-canonical contract and a strict op-log. A new +// capability rather than a change to the old one, because a registered +// capability's payload format is fixed (SPEC §8): a reader that predates it +// rejects a WQC2 payload at the magic, and an envelope under this string is inert +// to it. Same equivalence, tier and replicate class as wai.quantum.circuit. Engine +// behind the `quantum` feature. pub const CAP_QUANTUM_CIRCUIT2: &str = "wai.quantum.circuit2"; // Balanced-ternary matrix-product-state transform (extensions/ternary-tt): a diff --git a/wai/wai-web/Cargo.lock b/wai/wai-web/Cargo.lock index cf84498df..a448303a5 100644 --- a/wai/wai-web/Cargo.lock +++ b/wai/wai-web/Cargo.lock @@ -759,6 +759,7 @@ version = "0.4.0" dependencies = [ "blake3", "ed25519-dalek", + "serde", "serde_json", ] -- 2.54.0 (Apple Git-157) From d06afe78dd4def454e5806291e7953736b87538e Mon Sep 17 00:00:00 2001 From: Transaction Science Date: Fri, 9 Oct 2026 17:46:02 -0400 Subject: [PATCH 12/13] wai-quantum circuit2: a strict I-JSON reader, the rules stated in full, an accept corpus MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A second review of the I-JSON fix upheld two defects. - The Rust reader left the number-range rule to serde_json. Built without float_roundtrip, its fast path is not correctly rounded, so near the largest double the reader and the referee disagreed both ways, and in WQC2 on the class; and whether wai-rs features that enable float_roundtrip were linked changed the verdict. - The spec claimed I-JSON, but neither reader refused noncharacters. wai-quantum now reads a contract or measurement section with a strict reader of its own (src/ijson.rs) that decides every rule from the text: UTF-8; one JSON value with JSON whitespace; no member name twice, compared after unescaping; no surrogate or noncharacter, raw or escaped; no number of magnitude 2^1024 − 2^970 or more, decided by std's correctly rounding parse; at most 127 levels. A number keeps its literal, and an unsigned integer is one written with no sign, fraction or exponent. The serde dependency added for the visitor is gone. The referee decides the range rule on the literal's digits, integers only, and names the rule it refuses by. quantum-sim §2 states the rules in full, says that only the class is normative when a section breaks several, and that a reader reads integers exactly. §8 adds the noncharacter to the newly refused payloads and notes the numbers just below the limit that some earlier builds refused. `accept_corpus()` holds payloads at the edges of the rules that a reader must accept, each with its circuit and measurement; the corpus gains `accept/` and `accept.json`. `gen`, `verify`, the referee and the browser check read it. The reject corpus gains the boundary cases (the integer 2^1024 − 2^970, a literal just above it, the WQC2 n_qubits forms on both sides of the class order), noncharacters, a lone low surrogate, a BOM, NaN and a raw control character, and the referee and the crate check each JSON case's rule. The wasm32-wasip2 leg runs the reader's tests. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/wai-conformance.yml | 2 +- wai/CHANGELOG.md | 25 +- wai/SPEC.md | 2 +- wai/extensions/quantum-sim.md | 51 +- wai/quantum-conformance/README.md | 10 +- wai/quantum-conformance/accept/accept.json | 170 +++++++ .../accept/v1_depth_127.wqc | Bin 0 -> 408 bytes .../accept/v1_escapes_and_characters.wqc | Bin 0 -> 222 bytes .../accept/v1_further_members.wqc | Bin 0 -> 253 bytes .../accept/v1_measure_lax.wqc | Bin 0 -> 207 bytes .../accept/v1_measure_missing_shots.wqc | Bin 0 -> 194 bytes .../accept/v1_measure_not_object.wqc | Bin 0 -> 165 bytes .../accept/v1_measure_not_u64.wqc | Bin 0 -> 208 bytes .../accept/v1_measure_other_types.wqc | Bin 0 -> 197 bytes .../accept/v1_measure_u64_max.wqc | Bin 0 -> 218 bytes .../accept/v1_members_reordered.wqc | Bin 0 -> 151 bytes .../accept/v1_n_qubits_255.wqc | Bin 0 -> 153 bytes .../accept/v1_numbers_largest_finite.wqc | Bin 0 -> 835 bytes .../accept/v1_numbers_toward_zero.wqc | Bin 0 -> 208 bytes .../accept/v1_oplog_trailing_bytes.wqc | Bin 0 -> 154 bytes .../accept/v1_param_on_h.wqc | Bin 0 -> 151 bytes .../accept/v1_repeated_control.wqc | Bin 0 -> 152 bytes .../accept/v1_unknown_sections.wqc | Bin 0 -> 174 bytes .../accept/v1_whitespace.wqc | Bin 0 -> 179 bytes .../accept/v2_measure_u64_max.wqc | Bin 0 -> 244 bytes .../accept/v2_n_qubits_255.wqc | Bin 0 -> 155 bytes .../accept/v2_unknown_sections.wqc | Bin 0 -> 176 bytes wai/quantum-conformance/browser_verify.mjs | 54 ++- wai/quantum-conformance/reject/reject.json | 10 + .../reject/v1_contract_bom.wqc | Bin 0 -> 154 bytes .../reject/v1_contract_control_character.wqc | Bin 0 -> 164 bytes ...v1_contract_integer_rounds_to_infinity.wqc | Bin 0 -> 465 bytes .../reject/v1_contract_lone_low_surrogate.wqc | Bin 0 -> 167 bytes .../reject/v1_contract_nan.wqc | Bin 0 -> 159 bytes .../reject/v1_contract_noncharacter.wqc | Bin 0 -> 167 bytes .../v1_contract_number_rounds_to_infinity.wqc | Bin 0 -> 178 bytes .../reject/v1_contract_raw_noncharacter.wqc | Bin 0 -> 164 bytes .../reject/v2_n_qubits_largest_finite.wqc | Bin 0 -> 174 bytes .../reject/v2_n_qubits_rounds_to_infinity.wqc | Bin 0 -> 174 bytes wai/quantum-conformance/tools/referee.py | 184 ++++++-- wai/wai-pack/Cargo.lock | 1 - wai/wai-quantum-serve/Cargo.lock | 1 - wai/wai-quantum/Cargo.lock | 1 - wai/wai-quantum/Cargo.toml | 3 - wai/wai-quantum/src/ijson.rs | 437 ++++++++++++++++++ wai/wai-quantum/src/lib.rs | 4 + wai/wai-quantum/src/quantum.rs | 344 +++++++++----- wai/wai-rs/Cargo.lock | 1 - wai/wai-rs/src/bin/wai_quantum_vectors.rs | 97 +++- wai/wai-web/Cargo.lock | 1 - 50 files changed, 1190 insertions(+), 208 deletions(-) create mode 100644 wai/quantum-conformance/accept/accept.json create mode 100644 wai/quantum-conformance/accept/v1_depth_127.wqc create mode 100644 wai/quantum-conformance/accept/v1_escapes_and_characters.wqc create mode 100644 wai/quantum-conformance/accept/v1_further_members.wqc create mode 100644 wai/quantum-conformance/accept/v1_measure_lax.wqc create mode 100644 wai/quantum-conformance/accept/v1_measure_missing_shots.wqc create mode 100644 wai/quantum-conformance/accept/v1_measure_not_object.wqc create mode 100644 wai/quantum-conformance/accept/v1_measure_not_u64.wqc create mode 100644 wai/quantum-conformance/accept/v1_measure_other_types.wqc create mode 100644 wai/quantum-conformance/accept/v1_measure_u64_max.wqc create mode 100644 wai/quantum-conformance/accept/v1_members_reordered.wqc create mode 100644 wai/quantum-conformance/accept/v1_n_qubits_255.wqc create mode 100644 wai/quantum-conformance/accept/v1_numbers_largest_finite.wqc create mode 100644 wai/quantum-conformance/accept/v1_numbers_toward_zero.wqc create mode 100644 wai/quantum-conformance/accept/v1_oplog_trailing_bytes.wqc create mode 100644 wai/quantum-conformance/accept/v1_param_on_h.wqc create mode 100644 wai/quantum-conformance/accept/v1_repeated_control.wqc create mode 100644 wai/quantum-conformance/accept/v1_unknown_sections.wqc create mode 100644 wai/quantum-conformance/accept/v1_whitespace.wqc create mode 100644 wai/quantum-conformance/accept/v2_measure_u64_max.wqc create mode 100644 wai/quantum-conformance/accept/v2_n_qubits_255.wqc create mode 100644 wai/quantum-conformance/accept/v2_unknown_sections.wqc create mode 100644 wai/quantum-conformance/reject/v1_contract_bom.wqc create mode 100644 wai/quantum-conformance/reject/v1_contract_control_character.wqc create mode 100644 wai/quantum-conformance/reject/v1_contract_integer_rounds_to_infinity.wqc create mode 100644 wai/quantum-conformance/reject/v1_contract_lone_low_surrogate.wqc create mode 100644 wai/quantum-conformance/reject/v1_contract_nan.wqc create mode 100644 wai/quantum-conformance/reject/v1_contract_noncharacter.wqc create mode 100644 wai/quantum-conformance/reject/v1_contract_number_rounds_to_infinity.wqc create mode 100644 wai/quantum-conformance/reject/v1_contract_raw_noncharacter.wqc create mode 100644 wai/quantum-conformance/reject/v2_n_qubits_largest_finite.wqc create mode 100644 wai/quantum-conformance/reject/v2_n_qubits_rounds_to_infinity.wqc create mode 100644 wai/wai-quantum/src/ijson.rs diff --git a/.github/workflows/wai-conformance.yml b/.github/workflows/wai-conformance.yml index 266221a29..c291e4f7b 100644 --- a/.github/workflows/wai-conformance.yml +++ b/.github/workflows/wai-conformance.yml @@ -528,7 +528,7 @@ jobs: CARGO_TARGET_WASM32_WASIP2_RUNNER: wasmtime run: | set -euo pipefail - mods="quantum receipt_pins quantum_fabric quantum_source quantum_channel quantum_qudit quantum_receipt" + mods="quantum ijson receipt_pins quantum_fabric quantum_source quantum_channel quantum_qudit quantum_receipt" filters=() for m in $mods; do filters+=("$m::"); done cargo test --locked --target wasm32-wasip2 --features full --lib -- "${filters[@]}" 2>&1 \ diff --git a/wai/CHANGELOG.md b/wai/CHANGELOG.md index 44e73b255..4cd85fe96 100644 --- a/wai/CHANGELOG.md +++ b/wai/CHANGELOG.md @@ -26,7 +26,9 @@ wire format may change and such changes are recorded here. QFT is withdrawn. Writers SHOULD NOT emit new circuit/1 content. - **Both capabilities are specified to the bit** (§2–§4): little-endian integers, section bounds computed without overflow, a repeated known section refused, the contract and - the measurement read as I-JSON (RFC 7493, at most 127 levels deep), the three refusal + the measurement read as I-JSON (RFC 7493) by rules stated in full: no repeated member + name, no surrogate or noncharacter, no number of magnitude 2^1024 − 2^970 or more, at + most 127 levels deep, and an integer read exactly; the three refusal classes (malformed, contract, invalid) in the order a reader checks them, the complex-product composition, the index convention, the gate matrices, the hash layouts and the shot sampler. `circuit_hash` covers the canonical contract, so it names the @@ -37,13 +39,16 @@ wire format may change and such changes are recorded here. `ext`, `numeric`, `frac` or `gateset` is missing or carries another value; an `n_qubits` that is not an unsigned integer of at most 255; a known section kind more than once; section bounds that overflow; a contract or measurement that repeats a - member name within one object. No file a reference writer wrote is among them. + member name within one object or holds a noncharacter. No file a reference writer + wrote is among them. - **Corpus.** `quantum-conformance/` keeps its four original vectors and manifest entries byte for byte and adds qft5_x22, dyadic_ladder and qft8_ones, which see the phase table: under either table, `dyadic_ladder`'s statevector hash changes with every ±1 change of - any `P(1..32)` component. `v2/` holds the same seven circuits as circuit2, and `reject/` - at least one payload for each refusal a payload can carry, with its class. `tools/referee.py`, integers only, derives - both tables, requires Appendix B to state them, and rebuilds and checks every file. The + any `P(1..32)` component. `v2/` holds the same seven circuits as circuit2; `reject/` at + least one payload for each refusal a payload can carry, with its class; and `accept/` + payloads at the edges of the same rules that a reader must accept, with how each reads. + `tools/referee.py`, integers only, derives both tables, requires Appendix B to state + them, reads every JSON section by the same rules, and rebuilds and checks every file. The Appendix B tables are rendered from the reference constants by `wai_quantum_vectors appendix`, and a test fails when the text differs. @@ -56,10 +61,12 @@ wire format may change and such changes are recorded here. recurrence runs at load. `simulate`, `to_wqc`, `circuit_hash`, receipts, the fabric's lanes and the toolchain follow the circuit's gateset. `from_wqc` reads both magics, checks a circuit/1 contract's values and a circuit2 contract's bytes, reads the contract - and the measurement as I-JSON (a member name repeated within an object is refused), - refuses repeated sections and wrapping bounds, and no longer sizes an allocation from a - claimed op count. `reject_corpus()` returns the payloads a reader must refuse, each with - its class; the corpus's `reject/` tree is written from it. + and the measurement as I-JSON with a strict reader of its own that decides every rule + from the text (so no build feature changes a verdict), refuses repeated sections and + wrapping bounds, and no longer sizes an allocation from a claimed op count. + `reject_corpus()` and `accept_corpus()` return the payloads a reader must refuse, each + with its class, and must accept, each with its reading; the corpus's `reject/` and + `accept/` trees are written from them. - **Every circuit built in code changes its `circuit_hash`**, since the contract names the capability, and its statevector wherever `H`, `T`, `T†`, `P(3)` or `P(k ≥ 5)` acts on a nonzero amplitude (`P(1)`, `P(2)` and `P(4)` are the same in both tables). Every diff --git a/wai/SPEC.md b/wai/SPEC.md index 8406acf03..2bd2b5703 100644 --- a/wai/SPEC.md +++ b/wai/SPEC.md @@ -746,7 +746,7 @@ the registered `ext`, `numeric`, `frac` and `gateset`, a `wai.quantum.circuit2` contract among them; whose `n_qubits` is not an unsigned integer of at most 255; that carries a known section kind more than once; that has a section whose bounds overflow; or whose contract or measurement repeats a member name within one -object. No payload a reference writer wrote is among them; +object or holds a noncharacter. No payload a reference writer wrote is among them; [quantum-sim](extensions/quantum-sim.md) §8 lists them with the rules. And the verify half — a signed, energy-accounted lineage for opaque diff --git a/wai/extensions/quantum-sim.md b/wai/extensions/quantum-sim.md index bce9bbb8b..63a368788 100644 --- a/wai/extensions/quantum-sim.md +++ b/wai/extensions/quantum-sim.md @@ -92,14 +92,37 @@ different sections of one payload. The reference writer emits the contract, the op-log and the measurement, if any, in that order and contiguous from offset 0, and no other section. -**JSON in a section.** The contract and the measurement are JSON texts, read as -I-JSON ([RFC 7493](https://www.rfc-editor.org/rfc/rfc7493)): UTF-8, no member -name repeated within one object, no `\u` escape of an unpaired surrogate, and no -number that rounds to an infinite IEEE 754 double. A reader also refuses nesting -deeper than 127 arrays and objects, the outermost value counted as the first. -An integer is read exactly, so `seed` and `shots` (§2.3) span the whole `u64` +**JSON in a section.** The contract and the measurement are I-JSON texts +([RFC 7493](https://www.rfc-editor.org/rfc/rfc7493)), and a reader decides each +of these rules from the section's bytes: + +- The section is UTF-8 and holds one JSON value + ([RFC 8259](https://www.rfc-editor.org/rfc/rfc8259)), with only space, tab, + line feed and carriage return around its tokens. A byte-order mark is not + whitespace. +- No object names a member twice. Names are compared after their escapes are + decoded, so `"a"` and `"\u0061"` are the same name. +- No name or string holds a surrogate code point or a noncharacter (U+FDD0 to + U+FDEF, and the last two code points of every plane), written raw or as an + escape. An escaped surrogate pair stands for the one character it encodes. +- No number has a magnitude of 2^1024 − 2^970 or more. These are exactly the + numbers that round to an infinite IEEE 754 double, rounding to nearest with + ties to even, so a correctly rounding parse decides the rule, and so does an + exact comparison of the literal's digits. A number that rounds to zero is in + range. +- Arrays and objects nest at most 127 deep, the outermost counted as the first. + +RFC 7493 also asks a sender to keep a number within a double's precision and an +integer within ±(2^53 − 1). A reader here does not hold what it receives to that: +it reads an integer exactly, so `seed` and `shots` (§2.3) span the whole `u64` range. Where a rule asks for an *unsigned JSON integer*, it means a number -written with no sign, fraction or exponent: `-0`, `2.0` and `2e0` are not one. +written with no sign, fraction or exponent, whose value is in the range the rule +names: `-0`, `2.0` and `2e0` are not one. + +A section that breaks one of these rules is a contract refusal for the contract +and a malformed one for the measurement. A section that breaks several is +refused with that class whichever of them a reader meets first; readers may name +different rules for it, and only the class is specified. A reader refuses a payload that breaks a rule of this section or of §2.1–§2.3. It checks in this order and refuses with the class of the first rule broken: @@ -118,7 +141,10 @@ It checks in this order and refuses with the class of the first rule broken: The corpus's `reject/` tree holds at least one payload for each refusal of this section and of §2.1–§2.3 that a payload can carry, and `reject.json` names each one's class. The two limits it cannot carry bind a writer instead: the wire -counts a gate's controls in a `u8` and a circuit's ops in a `u32`. +counts a gate's controls in a `u8` and a circuit's ops in a `u32`. Its `accept/` +tree holds payloads at the edges of the same rules that a reader MUST accept, +and `accept.json` gives how each one reads: its capability, `n_qubits`, op count, +`circuit_hash` and measurement. ### 2.1 Contract @@ -603,8 +629,13 @@ payloads that break them, and a reader of this revision refuses: - a section whose bounds overflow (an earlier reader could wrap on a 32-bit target); - a contract or measurement that repeats a member name within one object (an - earlier reader kept the last). The other I-JSON rules of §2 state what earlier - reference readers already refused. + earlier reader kept the last), or that holds a noncharacter in a name or a + string. + +The other rules of §2 refuse what earlier reference readers refused, with one +difference the other way: those readers, depending on how they were built, also +refused some numbers just below 2^1024 − 2^970, which a reader of this revision +accepts. No file written by any reference version is affected: every reference writer wrote the canonical contract, one section of each kind, and sections within the diff --git a/wai/quantum-conformance/README.md b/wai/quantum-conformance/README.md index e84e9b232..293ef3cbb 100644 --- a/wai/quantum-conformance/README.md +++ b/wai/quantum-conformance/README.md @@ -7,13 +7,14 @@ tolerance**. **Provenance.** Generated here by `wai_quantum_vectors` from circuits authored in code. No media. -Three trees, each a pinned corpus: +Four trees, each a pinned corpus: | tree | capability | what | |---|---|---| | `vectors/` + `expected_hashes.json` | `wai.quantum.circuit` (`WQC1`) | bell, ghz3, qft3 and qft5, frozen as they were first written, then qft5_x22, dyadic_ladder and qft8_ones | | `v2/vectors/` + `v2/expected_hashes.json` | `wai.quantum.circuit2` (`WQC2`) | the same seven circuits; each entry names its `capability` | | `reject/` + `reject/reject.json` | both | payloads a conforming reader refuses, each with the class it is refused as: `malformed`, `contract` or `invalid` (extension §2) | +| `accept/` + `accept/accept.json` | both | payloads a conforming reader accepts, at the edges of the same rules, each with how it reads: capability, `n_qubits`, op count, circuit hash and measurement (extension §2) | An entry with no `capability` is a `wai.quantum.circuit` vector. The trees are the one reference all three surfaces are checked against, rather than being checked @@ -48,8 +49,8 @@ node wai/quantum-conformance/browser_verify.mjs [path/to/quantum-ml/pkg] [module ``` Drives the deployed wasm-bindgen bundle in headless Chromium: the sink opens every -file of both vector trees to its manifest entry and refuses every reject file with -its listed class, and the IDE builds its catalog circuits in each gateset to the +file of both vector trees to its manifest entry, refuses every reject file with +its listed class and reads every accept file as listed, and the IDE builds its catalog circuits in each gateset to the matching manifest. It is run by hand, before the website deploys a bundle. Note that the IDE's `statevector_hash_hex()` is empty until `run()` is called — the circuit is built eagerly, the state is not. @@ -65,7 +66,8 @@ with the Rust reference. It derives both phase tables (circuit/1 by its recurren circuit2 by an interval-certified procedure) and requires their literal values and digests, and the extension's Appendix B to state the same; rebuilds every vector file byte for byte and recomputes every manifest entry; refuses every reject file -with its listed class and by the rule its name states; and requires +with its listed class and by the rule its name states; accepts every accept file +and reads it as listed; and requires `dyadic_ladder` to change its statevector hash under every ±1 change of every phase-table entry, in both tables. CI runs it on Linux (`wai-conformance.yml`, job `independent-verifiers`). diff --git a/wai/quantum-conformance/accept/accept.json b/wai/quantum-conformance/accept/accept.json new file mode 100644 index 000000000..e46112e5d --- /dev/null +++ b/wai/quantum-conformance/accept/accept.json @@ -0,0 +1,170 @@ +{ + "v1_members_reordered.wqc": { + "capability": "wai.quantum.circuit", + "n_qubits": 2, + "n_ops": 2, + "circuit_hash": "894d9a9582b19f27b8d08d06938357237fc00f7f30bf4bfc2be0c04fa5e47a4e", + "measure": null + }, + "v1_whitespace.wqc": { + "capability": "wai.quantum.circuit", + "n_qubits": 2, + "n_ops": 2, + "circuit_hash": "894d9a9582b19f27b8d08d06938357237fc00f7f30bf4bfc2be0c04fa5e47a4e", + "measure": null + }, + "v1_further_members.wqc": { + "capability": "wai.quantum.circuit", + "n_qubits": 2, + "n_ops": 2, + "circuit_hash": "894d9a9582b19f27b8d08d06938357237fc00f7f30bf4bfc2be0c04fa5e47a4e", + "measure": null + }, + "v1_escapes_and_characters.wqc": { + "capability": "wai.quantum.circuit", + "n_qubits": 2, + "n_ops": 2, + "circuit_hash": "894d9a9582b19f27b8d08d06938357237fc00f7f30bf4bfc2be0c04fa5e47a4e", + "measure": null + }, + "v1_depth_127.wqc": { + "capability": "wai.quantum.circuit", + "n_qubits": 2, + "n_ops": 2, + "circuit_hash": "894d9a9582b19f27b8d08d06938357237fc00f7f30bf4bfc2be0c04fa5e47a4e", + "measure": null + }, + "v1_numbers_largest_finite.wqc": { + "capability": "wai.quantum.circuit", + "n_qubits": 2, + "n_ops": 2, + "circuit_hash": "894d9a9582b19f27b8d08d06938357237fc00f7f30bf4bfc2be0c04fa5e47a4e", + "measure": null + }, + "v1_numbers_toward_zero.wqc": { + "capability": "wai.quantum.circuit", + "n_qubits": 2, + "n_ops": 2, + "circuit_hash": "894d9a9582b19f27b8d08d06938357237fc00f7f30bf4bfc2be0c04fa5e47a4e", + "measure": null + }, + "v1_n_qubits_255.wqc": { + "capability": "wai.quantum.circuit", + "n_qubits": 255, + "n_ops": 2, + "circuit_hash": "eaa87747d510e6d6cff04212fbcecf37aa22fa6fed8fe716ea3110092e6045e0", + "measure": null + }, + "v1_measure_lax.wqc": { + "capability": "wai.quantum.circuit", + "n_qubits": 2, + "n_ops": 2, + "circuit_hash": "894d9a9582b19f27b8d08d06938357237fc00f7f30bf4bfc2be0c04fa5e47a4e", + "measure": { + "seed": 1, + "shots": 10 + } + }, + "v1_measure_missing_shots.wqc": { + "capability": "wai.quantum.circuit", + "n_qubits": 2, + "n_ops": 2, + "circuit_hash": "894d9a9582b19f27b8d08d06938357237fc00f7f30bf4bfc2be0c04fa5e47a4e", + "measure": { + "seed": 1, + "shots": 0 + } + }, + "v1_measure_u64_max.wqc": { + "capability": "wai.quantum.circuit", + "n_qubits": 2, + "n_ops": 2, + "circuit_hash": "894d9a9582b19f27b8d08d06938357237fc00f7f30bf4bfc2be0c04fa5e47a4e", + "measure": { + "seed": 18446744073709551615, + "shots": 18446744073709551615 + } + }, + "v1_measure_not_u64.wqc": { + "capability": "wai.quantum.circuit", + "n_qubits": 2, + "n_ops": 2, + "circuit_hash": "894d9a9582b19f27b8d08d06938357237fc00f7f30bf4bfc2be0c04fa5e47a4e", + "measure": { + "seed": 0, + "shots": 0 + } + }, + "v1_measure_other_types.wqc": { + "capability": "wai.quantum.circuit", + "n_qubits": 2, + "n_ops": 2, + "circuit_hash": "894d9a9582b19f27b8d08d06938357237fc00f7f30bf4bfc2be0c04fa5e47a4e", + "measure": { + "seed": 0, + "shots": 0 + } + }, + "v1_measure_not_object.wqc": { + "capability": "wai.quantum.circuit", + "n_qubits": 2, + "n_ops": 2, + "circuit_hash": "894d9a9582b19f27b8d08d06938357237fc00f7f30bf4bfc2be0c04fa5e47a4e", + "measure": { + "seed": 0, + "shots": 0 + } + }, + "v1_oplog_trailing_bytes.wqc": { + "capability": "wai.quantum.circuit", + "n_qubits": 2, + "n_ops": 2, + "circuit_hash": "894d9a9582b19f27b8d08d06938357237fc00f7f30bf4bfc2be0c04fa5e47a4e", + "measure": null + }, + "v1_param_on_h.wqc": { + "capability": "wai.quantum.circuit", + "n_qubits": 2, + "n_ops": 2, + "circuit_hash": "2eed24488205bf419b886feed0947cc63a21813f62f3a7a697b5785adcc72b08", + "measure": null + }, + "v1_repeated_control.wqc": { + "capability": "wai.quantum.circuit", + "n_qubits": 2, + "n_ops": 2, + "circuit_hash": "3aaa7132a1cc0f068c8f009f4e8d6137a825f6d22c2e5a32255a4a38c547755b", + "measure": null + }, + "v1_unknown_sections.wqc": { + "capability": "wai.quantum.circuit", + "n_qubits": 2, + "n_ops": 2, + "circuit_hash": "894d9a9582b19f27b8d08d06938357237fc00f7f30bf4bfc2be0c04fa5e47a4e", + "measure": null + }, + "v2_unknown_sections.wqc": { + "capability": "wai.quantum.circuit2", + "n_qubits": 2, + "n_ops": 2, + "circuit_hash": "6f6199943059fc4d90554d7c33f1ac69723603f5128962a6e3cac9c765484d2b", + "measure": null + }, + "v2_n_qubits_255.wqc": { + "capability": "wai.quantum.circuit2", + "n_qubits": 255, + "n_ops": 2, + "circuit_hash": "225f13952c14c6375adf7cc85080b1dd34af8f5378e1dac737517e0212d74c32", + "measure": null + }, + "v2_measure_u64_max.wqc": { + "capability": "wai.quantum.circuit2", + "n_qubits": 2, + "n_ops": 2, + "circuit_hash": "6f6199943059fc4d90554d7c33f1ac69723603f5128962a6e3cac9c765484d2b", + "measure": { + "seed": 18446744073709551615, + "shots": 18446744073709551615 + } + } +} diff --git a/wai/quantum-conformance/accept/v1_depth_127.wqc b/wai/quantum-conformance/accept/v1_depth_127.wqc new file mode 100644 index 0000000000000000000000000000000000000000..162b46dcd426838b957dd9ce17f4462e2480834c GIT binary patch literal 408 zcmWF!bT(vSU}OM-LLkWmBKU!vYNgbQ5+y68^2AKN!qUXNlG0qgq|B0HB`YH!r!+UUC^H$VJSDY6FD%WA_g5VTG1{fE6~?M6=9gRi0Y779HCQ)FuuwkS4{CF)}IoBf`yXcuPK$Lzqo zzn)Z8l!erfpvrV7*nrjm9einSZc$GmUYn8q7&+W4K$t#map15(*UAvDK4`W yzQy1pq1A37oT78MB|P^%!aRIJw}H+y$Zww#g1%^;^5)N5rBq3>ouDYo9QhZ<*+$g> literal 0 HcmV?d00001 diff --git a/wai/quantum-conformance/accept/v1_measure_lax.wqc b/wai/quantum-conformance/accept/v1_measure_lax.wqc new file mode 100644 index 0000000000000000000000000000000000000000..bcb8a92455eb7a00dfd9b1c9d8c6417b877ccce0 GIT binary patch literal 207 zcmXYr%L>9k3`P5J?=OUGEq$oC=++-l5M0W1W~u?(+Ie856o1{c7&sR$x#Z@0J{EBh z20Sk&nq|$zUzUs+Q4bDFd<#l@FIwlDRFaXNTvlL$?XC9}Ig90%jc>Fe`Hw5@(wYXX z4toyPM(|>nCwLc5TRk6@Ppa$AsLMuugd%yuDdNr*6NKUX6^wwDuw)TRzyTovw2zMa N*cbU?wN{IR34W}RG!6g& literal 0 HcmV?d00001 diff --git a/wai/quantum-conformance/accept/v1_measure_missing_shots.wqc b/wai/quantum-conformance/accept/v1_measure_missing_shots.wqc new file mode 100644 index 0000000000000000000000000000000000000000..6952eac5e8616fe8856e3fa39318ad6efc2bc1ef GIT binary patch literal 194 zcmXYr%?iRm41`JwW37HN~t_CQ?IZzF|VXFS1&oUD7iGV zMBh+JM=3A9urw*Nq*%$y2*@eTO)bhyhAK}#?P0qKMfW0N3v+ZvX%Q literal 0 HcmV?d00001 diff --git a/wai/quantum-conformance/accept/v1_measure_not_u64.wqc b/wai/quantum-conformance/accept/v1_measure_not_u64.wqc new file mode 100644 index 0000000000000000000000000000000000000000..95a06dd3eeaa2e2fa463a3977f965910a4f78574 GIT binary patch literal 208 zcmXYr%?iRW5QO{V-FMilwI()gwO3z2LGUOsKN_&DZ4#`M;;Xv_7rupMhncyY4m1wJ z0Oy57^I9VD7Z*WBP=kXC-rS^}=Z*7qDoiiDaT^K=G}q4GjI&T}@$j|kjrhx@a;Y|h zk_AJd?m0d(lE9riWz}pdo<{4om*9k3`P5J?>_`wS*oMrqFaAJL2xP4nT!Vdu=Bu5DgL?{FmNsjx#Z?{If*<; z6RsDNMOiZWmu1CFfTM>B-jd28$j*nhRAf~kzY$PCcOODc-a)nFA+%^o{p}ikX=p@! zJh0JN$&3BAfQR(xFdkJ68Pi*RUF(n3q|PvBoVj3uFm{lmte$l$I5xS(R7W Rt|&-JiUj(5J2(F`r!V?7Hhlm9 literal 0 HcmV?d00001 diff --git a/wai/quantum-conformance/accept/v1_members_reordered.wqc b/wai/quantum-conformance/accept/v1_members_reordered.wqc new file mode 100644 index 0000000000000000000000000000000000000000..36995bde5345404e5df87c4e1b5dc57b31ac17e8 GIT binary patch literal 151 zcmWlTK?(vf3`N`G-aBM#sg8<^ZoPnl;8JRvN?@isofb#LyPJM^9|Dgg*YmMcLJH1{ zQLUED7ap$Qgas`4o??9Fa9M?q3*=9rA!g^n?lu}y9WWCQ@OESZ787a4aM-hzZezVu hnZRx};M|rqX{;`lIv7vcS1Riceu{4{7$IeQ<`1M)C!hcT literal 0 HcmV?d00001 diff --git a/wai/quantum-conformance/accept/v1_n_qubits_255.wqc b/wai/quantum-conformance/accept/v1_n_qubits_255.wqc new file mode 100644 index 0000000000000000000000000000000000000000..702f3559b5ac169b1a7df3be4beb264696308a24 GIT binary patch literal 153 zcmWF!bT(vSU}OM-A|Pf05&S@0t(00(qGY91o|vguSelquQktumoLQ7ynpvW6sHCHm z7hhPKlvz@&WMyP(3gnjNrWR!;Lp7wNmguErR-~qwnE;ih6(s^Cj16>@(i2Nki&H_y iCg)_PrR5i;glMN!CZ=R22PoAt0c~XgVg^P=kaGa|J}3?V literal 0 HcmV?d00001 diff --git a/wai/quantum-conformance/accept/v1_numbers_largest_finite.wqc b/wai/quantum-conformance/accept/v1_numbers_largest_finite.wqc new file mode 100644 index 0000000000000000000000000000000000000000..0eb69a32e5115a4fc067f58860617bc8cf32208d GIT binary patch literal 835 zcmeHG%Wf1w4BTCE=Lb0Tm1VKpZa?~4s@|RB^c(>VZu-?4dY$qMzVl51L6Dcf9$f3Ek@k*fswc;Vx=*j$Wi$ORV&~7RQ zo!7ir%N%YxV24wvxs!7bZ%+$5YD?z8+yJV?qLK`bpbBnJP!hL)ltVueLhcLs&iL3F JJ4w^&i!b`-G`0W$ literal 0 HcmV?d00001 diff --git a/wai/quantum-conformance/accept/v1_oplog_trailing_bytes.wqc b/wai/quantum-conformance/accept/v1_oplog_trailing_bytes.wqc new file mode 100644 index 0000000000000000000000000000000000000000..dbb344b69c4194112876d0136b45190cf56ac35a GIT binary patch literal 154 zcmWF!bT(vSU}OM-0w87r5kf#*t(00(qGY91o|vguSelquQktumoLQ7ynpvW6sHCHm z7hhPKlvz@&WMu^8l;)-uWhO(Fr=*tXrDay6rkI%k6{Zy>0ws(Mbd=H)OHzwdL53#h iWTvI%7o~(~r&K1UWF`kF)iMF?WC3CZMn7?}VmcqgU+ literal 0 HcmV?d00001 diff --git a/wai/quantum-conformance/accept/v1_param_on_h.wqc b/wai/quantum-conformance/accept/v1_param_on_h.wqc new file mode 100644 index 0000000000000000000000000000000000000000..5d90861ca8d907584c64cb92403e300906f2e2f3 GIT binary patch literal 151 zcmWF!bT(vSU}OM-0w87r5&S@0t(00(qGY91o|vguSelquQktumoLQ7ynpvW6sHCHm z7hhPKlvz@&WMu^8l;)-uWhO(Fr=*tXrDay6rkI%k6{Zy>0ws(Mbd=H)OHzwdL53#h iWTvI%7o~(~r&K1UWF`kF)iMF?WMN=nXJBLmIR*f+D<`J_ literal 0 HcmV?d00001 diff --git a/wai/quantum-conformance/accept/v1_repeated_control.wqc b/wai/quantum-conformance/accept/v1_repeated_control.wqc new file mode 100644 index 0000000000000000000000000000000000000000..5db2ea7fef7dc1b3b31cbbfa2e4a3537502eae99 GIT binary patch literal 152 zcmWF!bT(vSU}OM-0w87r5duJ5t(00(qGY91o|vguSelquQktumoLQ7ynpvW6sHCHm z7hhPKlvz@&WMu^8l;)-uWhO(Fr=*tXrDay6rkI%k6{Zy>0ws(Mbd=H)OHzwdL53#h gWTvI%7o~(~r&K1UWF`kF)iMF?WC3CZMkcUh0NEWUq5uE@ literal 0 HcmV?d00001 diff --git a/wai/quantum-conformance/accept/v1_unknown_sections.wqc b/wai/quantum-conformance/accept/v1_unknown_sections.wqc new file mode 100644 index 0000000000000000000000000000000000000000..81527728a2aee2d36c9f8fbf3418797e6611d0f6 GIT binary patch literal 174 zcmXYq!3x4K5Ja02&;G++E!ArA(5pY7Ab6CRY-FLWHr-%F#ET#91{|0N%MLTA!>-cV zClk%YD&a29qhK!9UqMo97{d#2VrbxDs2rl{Id-L`U?cIef&%*U5HG|dG;29xhk@)r t?vP7Q6S{3JK_84fHmd@z(mP@*wJmvn3+}jZPvgk$;hoE{PU}i3;SZ2>D$oD` literal 0 HcmV?d00001 diff --git a/wai/quantum-conformance/accept/v1_whitespace.wqc b/wai/quantum-conformance/accept/v1_whitespace.wqc new file mode 100644 index 0000000000000000000000000000000000000000..a046def8c6c74f45caef283ad3f6d259edf9f069 GIT binary patch literal 179 zcmWF!bT(vSU}OM-9w24{5&S@`z{^#wlv+`uq+q3>RGyfrS6G^uS5lg*mz-IYT$)*; zZ>Xf8!^NqT7hhPKlvz>?QfCAaDa}nS%1nl9Oi3-#OUtZCO))cpXh|zd1j`y5fYqlb smZTP^LM%$o$xKVjFG>l~PN_^x$xIGVs^#TUU;?^_1&A3K89@#O0P#mEQUCw| literal 0 HcmV?d00001 diff --git a/wai/quantum-conformance/accept/v2_measure_u64_max.wqc b/wai/quantum-conformance/accept/v2_measure_u64_max.wqc new file mode 100644 index 0000000000000000000000000000000000000000..8f38a87ca751eea84bf425ba85225b80e45c16de GIT binary patch literal 244 zcmZvWO%8%U42415dxvf%2snVU^#W>4T$(yFXcGaQpQte|ytyTDX_K!>`(FCG->mW| z2m>y|jD|L0q7Sz%^8)>FP{SQ5xq0lIZxcyIdUE+JgBZHQ%^%5GsB<2Cs}0GYUulOsoEwKo)Z?E2=qrSrnyubPCKZ*bV literal 0 HcmV?d00001 diff --git a/wai/quantum-conformance/accept/v2_n_qubits_255.wqc b/wai/quantum-conformance/accept/v2_n_qubits_255.wqc new file mode 100644 index 0000000000000000000000000000000000000000..d1424da6cea0425a3f6f9a25653bee4af22b4c3d GIT binary patch literal 155 zcmWF!bT(pQU}OM-5+G&*5&S@0t(00(qGY91o|vguSelquQktumoLQ7ynpt9`Z>XfB zlowxEnv_{mtYl?mY6|3*=B5^9CPQ_kq?YI<<`$Tl02QVcB?5(w4Rn;!6H8KyQ$eOC k=VYd(06!QgfdBvi literal 0 HcmV?d00001 diff --git a/wai/quantum-conformance/accept/v2_unknown_sections.wqc b/wai/quantum-conformance/accept/v2_unknown_sections.wqc new file mode 100644 index 0000000000000000000000000000000000000000..f781fef670d25300b6f3f9ef5ff11fb946cdc780 GIT binary patch literal 176 zcmXYq%?iRW5QLjj&%VQ6Em3Rn(5o+?Ab6CRA6aOk-6m8K@#b^80SD%TU51&{Vb@f} zr(n(4B7@lF#F(?IFC(j0(b5a-BSHt0LOCQAV)9ZW>4dRauAzo-9?}Kl2wlTR@|IBl v^TtN0{C(SU%#q^vW>v$LYe$nd#gkb&v-YP3a literal 0 HcmV?d00001 diff --git a/wai/quantum-conformance/browser_verify.mjs b/wai/quantum-conformance/browser_verify.mjs index 11fee714d..f2a20f45f 100644 --- a/wai/quantum-conformance/browser_verify.mjs +++ b/wai/quantum-conformance/browser_verify.mjs @@ -16,6 +16,10 @@ // statevector hashes, and the histogram hash where the entry pins a measurement. // - QuantumSink refuses every file `reject/reject.json` lists, and its error names // the listed class in parentheses: ` (): `. +// - QuantumSink reads every file `accept/accept.json` lists as listed: capability, +// n_qubits, n_ops, circuit hash and measurement. A file too wide to simulate +// (n_qubits 255) is read and then refused at the `simulate` stage, never at +// `WQC parse`. // - QuantumIde.load_algorithm builds bell, ghz and qft in its default gateset to // `v2/expected_hashes.json`, and after `set_gateset(1)` to the circuit/1 manifest. // @@ -73,6 +77,23 @@ const REJECT = Object.entries(readJson(path.join(REJECT_DIR, "reject.json"))).ma kind, })); +// Every accept file, with its reading. seed and shots are u64, beyond what a JSON +// number keeps exactly in JS, so they are compared as decimal strings. +const ACCEPT_DIR = path.join(HERE, "accept"); +const acceptText = fs.readFileSync(path.join(ACCEPT_DIR, "accept.json"), "utf8") + .replace(/"(seed|shots)": (\d+)/g, '"$1": "$2"'); +const ACCEPT = Object.entries(JSON.parse(acceptText)).map(([file, e]) => ({ + label: "accept/" + file, + bytes: bytesOf(path.join(ACCEPT_DIR, file)), + want: { + capability: e.capability, + n_qubits: e.n_qubits, + n_ops: e.n_ops, + circuit: e.circuit_hash, + measure: e.measure ? `${e.measure.seed} ${e.measure.shots}` : null, + }, +})); + // The IDE's catalog: vector file -> (algorithm id, qubits), in each gateset against // its manifest. Gateset 2 (circuit2) is the IDE's default; 1 is set explicitly. const CATALOG = [ @@ -130,11 +151,11 @@ try { // but networkidle still stalls behind them on some hosts. await page.goto(`http://127.0.0.1:${PORT}/`, { waitUntil: "domcontentloaded", timeout: 30000 }); - const got = await page.evaluate(async ({ sink, reject, ide, port, module }) => { + const got = await page.evaluate(async ({ sink, reject, accept, ide, port, module }) => { const mod = await import(`http://127.0.0.1:${port}/${module}`); await mod.default(); const message = (e) => String((e && e.message) || e); - const out = { sink: {}, reject: {}, ide: {} }; + const out = { sink: {}, reject: {}, accept: {}, ide: {} }; for (const c of sink) { try { const s = new mod.QuantumSink(new Uint8Array(c.bytes)); @@ -159,6 +180,21 @@ try { out.reject[c.label] = { error: message(e) }; } } + for (const c of accept) { + try { + const s = new mod.QuantumSink(new Uint8Array(c.bytes)); + out.accept[c.label] = { + capability: s.capability(), + n_qubits: s.n_qubits(), + n_ops: s.n_ops(), + circuit: s.circuit_hash_hex(), + measure: s.has_measure() ? `${s.seed()} ${s.shots()}` : null, + }; + s.free(); + } catch (e) { + out.accept[c.label] = { error: message(e) }; + } + } for (const c of ide) { const q = new mod.QuantumIde(c.n); try { @@ -184,7 +220,7 @@ try { } } return out; - }, { sink: SINK, reject: REJECT, ide: IDE, port: PORT, module: MODULE }); + }, { sink: SINK, reject: REJECT, accept: ACCEPT, ide: IDE, port: PORT, module: MODULE }); let fail = 0; const report = (label, problem) => { @@ -213,6 +249,16 @@ try { const m = /^[^()]+ \(([a-z-]+)\): /.exec(g.error); report(`${c.label} refused as ${c.kind}`, m && m[1] === c.kind ? null : `refused as: ${g.error}`); } + for (const c of ACCEPT) { + const g = got.accept[c.label]; + if (g.error) { + const wide = c.want.n_qubits === 255 && /^simulate \(/.test(g.error); + report(wide ? `${c.label} read; too wide to simulate` : c.label, wide ? null : `refused: ${g.error}`); + continue; + } + const bad = differ(c.want, g); + report(c.label, bad.length ? bad.map((k) => `${k} browser=${g[k]} corpus=${c.want[k]}`).join("; ") : null); + } for (const c of IDE) { const g = got.ide[c.label]; if (g.error) { @@ -224,7 +270,7 @@ try { } console.log(errs.length ? `console errors: ${errs.length}\n ${errs.join("\n ")}` : "console errors: 0"); if (fail === 0 && errs.length === 0) { - console.log("all checks pass: statevector-equivalence holds in the browser for both capabilities, and every reject file is refused as listed"); + console.log("all checks pass: statevector-equivalence holds in the browser for both capabilities, every reject file is refused as listed, and every accept file reads as listed"); } process.exitCode = fail || errs.length ? 1 : 0; } catch (e) { diff --git a/wai/quantum-conformance/reject/reject.json b/wai/quantum-conformance/reject/reject.json index e85719011..31386b1dd 100644 --- a/wai/quantum-conformance/reject/reject.json +++ b/wai/quantum-conformance/reject/reject.json @@ -16,6 +16,14 @@ "v1_contract_lone_surrogate.wqc": "contract", "v1_contract_too_deep.wqc": "contract", "v1_n_qubits_minus0.wqc": "malformed", + "v1_contract_lone_low_surrogate.wqc": "contract", + "v1_contract_noncharacter.wqc": "contract", + "v1_contract_raw_noncharacter.wqc": "contract", + "v1_contract_integer_rounds_to_infinity.wqc": "contract", + "v1_contract_number_rounds_to_infinity.wqc": "contract", + "v1_contract_bom.wqc": "contract", + "v1_contract_nan.wqc": "contract", + "v1_contract_control_character.wqc": "contract", "v1_measure_duplicate_member.wqc": "malformed", "v1_contract_not_utf8.wqc": "contract", "v1_contract_number_out_of_range.wqc": "contract", @@ -44,6 +52,8 @@ "v2_contract1_in_wqc2.wqc": "contract", "v2_n_qubits_256.wqc": "malformed", "v2_contract_not_object.wqc": "contract", + "v2_n_qubits_rounds_to_infinity.wqc": "contract", + "v2_n_qubits_largest_finite.wqc": "malformed", "v2_phase_0.wqc": "invalid", "v2_measure_twice.wqc": "malformed", "v2_measure_seed_signed.wqc": "malformed", diff --git a/wai/quantum-conformance/reject/v1_contract_bom.wqc b/wai/quantum-conformance/reject/v1_contract_bom.wqc new file mode 100644 index 0000000000000000000000000000000000000000..172476497361f990b008bdde27aac033e144cad2 GIT binary patch literal 154 zcmWF!bT(vSU}OM-VjyM$5&S^>e)s-rrPPWNB`c-!#7w=y(!{)y(pqC|J@C^@D_#{hPhwQwNYBh zErKa;EqnoIh6dh_?3i88xwD><&&1mrD(D|$Ziy#o_9AkJiTpnvkZq(H!|5o&n4B1g nT?J3aIAJOEFN$$U;kF4M7sy{q;lL=C(=9AU2CM|7by@ZU47@8M literal 0 HcmV?d00001 diff --git a/wai/quantum-conformance/reject/v1_contract_integer_rounds_to_infinity.wqc b/wai/quantum-conformance/reject/v1_contract_integer_rounds_to_infinity.wqc new file mode 100644 index 0000000000000000000000000000000000000000..73397baa5feac0dee3e3b14d5c3176a519487c3e GIT binary patch literal 465 zcmXX@%WB*}5cDp-=O1*x|biX>(6<4>rR4V zeV#7!@E<--%key4r_l|-^QFgw(xH3mm+5WVNQYnZ`TYAbe)~Fp^y54{0C(+992H*5 zYU!fflA<&uE7+(hsc}QAaH<3oX{2DlIW!_G$;`@AO2RD~mAPBd#v3g|D5zYOB;DA( zBr}RPSVLH3<1#W5SMnB?=>f@>LY`cdMM4CY)Km|b&}^30k|UU#GISvvkO(5L1Z}WF zs+EhRNwI+`9TW!V0B>%=9dMNJukgZ<2O-c9QZ13^0~*hZnypI IcH0{N0c`VV(EtDd literal 0 HcmV?d00001 diff --git a/wai/quantum-conformance/reject/v1_contract_lone_low_surrogate.wqc b/wai/quantum-conformance/reject/v1_contract_lone_low_surrogate.wqc new file mode 100644 index 0000000000000000000000000000000000000000..02327527e0295a82ca269596825d282b6cc07985 GIT binary patch literal 167 zcmXYr%?iRW5QLlH-FMilrKTz#di4bq1TTWbWFrgxq3H%IBKYWTz=dyNm|>X9=}_xJ z6>?ue=eHEzfHOk_PfOL9Y|p8y97UIi*EN*T-^O$&o}k%?NF7FU|F}o40?p|6I|+tp r#n_q>9+q*!T$HqyrW7hdqlSS|02+ l4LoeZ8S7HlQwY!KFDw7Hp4^GO0;aRwSMluyBc!ZP{Qw=4DbD}^ literal 0 HcmV?d00001 diff --git a/wai/quantum-conformance/reject/v1_contract_noncharacter.wqc b/wai/quantum-conformance/reject/v1_contract_noncharacter.wqc new file mode 100644 index 0000000000000000000000000000000000000000..369b6c682ecd53c421e1acb1539a8673a5fba33d GIT binary patch literal 167 zcmXYr%?iRm41~MIyYG;zrM4;_di4bq1TTWDo2&%-L$_J1h~T4}1p{ATm|>X9=}_mH z$@IQTp59V<1I+>rJS~@Fw7tZxbR1k{S=UfNe;ebSWrAj-B6bwm{o_bddY%dQI}Q3^ r)!0@AJgm?}snoUX{SffH!k0yM$5c27sdbGWv4T&o8x~4tOd9tCtT-%_ literal 0 HcmV?d00001 diff --git a/wai/quantum-conformance/reject/v1_contract_number_rounds_to_infinity.wqc b/wai/quantum-conformance/reject/v1_contract_number_rounds_to_infinity.wqc new file mode 100644 index 0000000000000000000000000000000000000000..45a9943c8fec403c86efef42047d9d2f0ca68123 GIT binary patch literal 178 zcmXYr%L>9U5JlVK-hY^_B{oe>A6tJwL2xNC4;iqPrU_QWFYu?Gf&*vZGKb-wk9(da zX+rZQvSqD_H`qBq2Tz4;jA{t6XBxeZ7`7a87%pSHVwj*SDa77-)PLRBz#Puj$cjYg z6&*y8!%YP{+2yIeqjL{$P8;)52K9lw89L7DwwA5nqH1I*_^!30Xg(R$SrJK^E0ws(Mbd=H)OHzwdL53#h rWTvI%7o~(~r&K1UWF`lI49qV{1xmf&zh9}A3FrV8AZB1>1i1_VEO;!- literal 0 HcmV?d00001 diff --git a/wai/quantum-conformance/reject/v2_n_qubits_largest_finite.wqc b/wai/quantum-conformance/reject/v2_n_qubits_largest_finite.wqc new file mode 100644 index 0000000000000000000000000000000000000000..d3d3d2bb975358ee637353d745eece3496eb8e82 GIT binary patch literal 174 zcmXYr%?iRW5QLlJ-FMilB{og%-_;jT5IjmuQdzLorU@$I+1GXhE_{0!c4y|e-xg`Y z5}GfO#r(KIw4!L~ zA`I4{{+k%K$_ 30: # beyond any length a section can hold + return not exponent.startswith("-") + k = len(whole) - (len(digits) - len(digits.lstrip("0"))) + int(exponent or "0") + if k != len(RANGE_LIMIT): + return k > len(RANGE_LIMIT) + n = max(len(significant), len(RANGE_LIMIT)) + return significant.ljust(n, "0") >= RANGE_LIMIT.ljust(n, "0") def strict_json(raw): - """`raw` read as I-JSON (RFC 7493, §2), or _NOT_JSON: UTF-8 text, no NaN or - Infinity, every number within an IEEE double, no unpaired surrogate escape, at most - MAX_DEPTH nested arrays and objects, and no member name twice in one object. `-0` reads - as the float -0.0, so it is never an unsigned integer.""" + """`raw` read as I-JSON (§2), or NotIJson naming the rule it breaks. An integer reads + as a Python int, so `-0` and any number with a fraction or exponent read as + ("number", literal): never an unsigned integer, and never a float.""" - def no_constant(name): - raise ValueError(name) - - def finite(text): - v = float(text) - if v != v or v in (float("inf"), float("-inf")): - raise ValueError("number out of range") - return v + def number(text): + if out_of_range(text): + raise NotIJson("number range") + return ("number", text) def integer(text): - if text == "-0": - return -0.0 - if float(text) in (float("inf"), float("-inf")): - raise ValueError("number out of range") - return int(text) + if out_of_range(text): + raise NotIJson("number range") + return ("number", text) if text == "-0" else int(text) + + def no_constant(name): + raise NotIJson("syntax") def unique(pairs): names = [k for k, _ in pairs] if len(set(names)) != len(names): - raise ValueError("a member name appears twice") + raise NotIJson("duplicate member") return dict(pairs) + def text(s): + for ch in s: + o = ord(ch) + if 0xD800 <= o <= 0xDFFF: + raise NotIJson("surrogate") + if 0xFDD0 <= o <= 0xFDEF or o & 0xFFFE == 0xFFFE: + raise NotIJson("noncharacter") + def walk(v, depth): if isinstance(v, (dict, list)): if depth + 1 > MAX_DEPTH: - raise ValueError("nested too deeply") + raise NotIJson("depth") items = v.items() if isinstance(v, dict) else enumerate(v) for k, x in items: if isinstance(k, str): - k.encode("utf-8") + text(k) walk(x, depth + 1) elif isinstance(v, str): - v.encode("utf-8") + text(v) try: - v = json.loads(raw.decode("utf-8"), parse_constant=no_constant, parse_float=finite, + decoded = raw.decode("utf-8") + except UnicodeDecodeError: + raise NotIJson("utf-8") + try: + v = json.loads(decoded, parse_constant=no_constant, parse_float=number, parse_int=integer, object_pairs_hook=unique) - walk(v, 0) - return v - except (UnicodeDecodeError, UnicodeEncodeError, ValueError, RecursionError): - return _NOT_JSON + except RecursionError: + raise NotIJson("depth") + except ValueError: # json.JSONDecodeError among them + raise NotIJson("syntax") + walk(v, 0) + return v def unsigned(v, below): @@ -356,9 +398,10 @@ def unsigned(v, below): def read_contract(gs, raw): """The contract's n_qubits, under the capability the magic names.""" - cv = strict_json(raw) - if cv is _NOT_JSON: - raise Refused("contract", "not JSON") + try: + cv = strict_json(raw) + except NotIJson as e: + raise Refused("contract", "not JSON: " + e.rule) if not isinstance(cv, dict): raise Refused("contract", "not an object") @@ -430,9 +473,10 @@ def check_valid(gs, n_qubits, ops): def read_measure(gs, raw): """circuit/1: JSON, a missing or non-u64 seed or shots reads as 0. circuit2: u64 seed and shots, and exactly the canonical bytes.""" - mv = strict_json(raw) - if mv is _NOT_JSON: - raise Refused("malformed", "measure JSON") + try: + mv = strict_json(raw) + except NotIJson as e: + raise Refused("malformed", "measure JSON: " + e.rule) if gs is V1: def field(key): v = mv.get(key) if isinstance(mv, dict) else None @@ -647,16 +691,24 @@ REJECTS = { "v1_n_qubits_258": ("malformed", "n_qubits"), "v1_contract_twice": ("malformed", "duplicate section"), "v1_oplog_twice": ("malformed", "duplicate section"), - "v1_contract_duplicate_member": ("contract", "not JSON"), - "v1_contract_lone_surrogate": ("contract", "not JSON"), - "v1_contract_too_deep": ("contract", "not JSON"), + "v1_contract_duplicate_member": ("contract", "not JSON: duplicate member"), + "v1_contract_lone_surrogate": ("contract", "not JSON: surrogate"), + "v1_contract_too_deep": ("contract", "not JSON: depth"), "v1_n_qubits_minus0": ("malformed", "n_qubits"), - "v1_measure_duplicate_member": ("malformed", "measure JSON"), - "v1_contract_not_utf8": ("contract", "not JSON"), - "v1_contract_number_out_of_range": ("contract", "not JSON"), + "v1_contract_lone_low_surrogate": ("contract", "not JSON: surrogate"), + "v1_contract_noncharacter": ("contract", "not JSON: noncharacter"), + "v1_contract_raw_noncharacter": ("contract", "not JSON: noncharacter"), + "v1_contract_integer_rounds_to_infinity": ("contract", "not JSON: number range"), + "v1_contract_number_rounds_to_infinity": ("contract", "not JSON: number range"), + "v1_contract_bom": ("contract", "not JSON: syntax"), + "v1_contract_nan": ("contract", "not JSON: syntax"), + "v1_contract_control_character": ("contract", "not JSON: syntax"), + "v1_measure_duplicate_member": ("malformed", "measure JSON: duplicate member"), + "v1_contract_not_utf8": ("contract", "not JSON: utf-8"), + "v1_contract_number_out_of_range": ("contract", "not JSON: number range"), "v1_n_qubits_fraction": ("malformed", "n_qubits"), "v1_n_qubits_missing": ("malformed", "n_qubits"), - "v1_measure_not_json": ("malformed", "measure JSON"), + "v1_measure_not_json": ("malformed", "measure JSON: syntax"), "short_payload": ("malformed", "magic"), "table_past_end": ("malformed", "section table"), "section_past_end": ("malformed", "section bounds"), @@ -679,6 +731,8 @@ REJECTS = { "v2_contract1_in_wqc2": ("contract", "canonical contract"), "v2_n_qubits_256": ("malformed", "n_qubits"), "v2_contract_not_object": ("contract", "not an object"), + "v2_n_qubits_rounds_to_infinity": ("contract", "not JSON: number range"), + "v2_n_qubits_largest_finite": ("malformed", "n_qubits"), "v2_phase_0": ("invalid", "phase index"), "v2_measure_twice": ("malformed", "duplicate section"), "v2_measure_seed_signed": ("malformed", "measure seed"), @@ -894,6 +948,51 @@ def check_rejects(rep, root): None if (r.kind, r.rule) == (kind, rule) else "refused as %s (%s)" % (r.kind, r.rule)) +# The accept corpus: the file stems `accept.json` must list. Each file's reading is +# recomputed here and compared with the listing. +ACCEPTS = [ + "v1_members_reordered", "v1_whitespace", "v1_further_members", "v1_escapes_and_characters", + "v1_depth_127", "v1_numbers_largest_finite", "v1_numbers_toward_zero", "v1_n_qubits_255", + "v1_measure_lax", "v1_measure_missing_shots", "v1_measure_u64_max", "v1_measure_not_u64", + "v1_measure_other_types", "v1_measure_not_object", "v1_oplog_trailing_bytes", "v1_param_on_h", + "v1_repeated_control", "v1_unknown_sections", "v2_unknown_sections", "v2_n_qubits_255", + "v2_measure_u64_max", +] + + +def check_accepts(rep, root): + tree = os.path.join(root, "accept") + listing, why = read_object(os.path.join(tree, "accept.json")) + if listing is None: + rep.check("accept/accept.json", why) + return + want = sorted("%s.wqc" % stem for stem in ACCEPTS) + rep.check("accept/accept.json lists every accept case", + None if sorted(listing) == want else "differs in %s" % sorted(set(listing) ^ set(want))) + rep.check("accept/ holds exactly the listed files", listed_and_present(listing, tree)) + for file in want: + label = "accept/" + file + try: + with open(os.path.join(tree, file), "rb") as f: + data = f.read() + gs, n_qubits, ops, measure = read_wqc(data) + except OSError as e: + rep.check(label, str(e)) + continue + except Refused as r: + rep.check(label, "refused as %s (%s); it must be accepted" % (r.kind, r.rule)) + continue + got = { + "capability": gs.capability, + "n_qubits": n_qubits, + "n_ops": len(ops), + "circuit_hash": circuit_hash(gs, n_qubits, ops), + "measure": None if measure is None else {"seed": measure[0], "shots": measure[1]}, + } + rep.check(label + " accepted and read as listed", + None if got == listing.get(file) else "read as %s, listed as %s" % (got, listing.get(file))) + + def check_mutations(rep): n, ops, _ = VECTORS["dyadic_ladder"] for gs in (V1, V2): @@ -922,6 +1021,7 @@ def main(): check_tree(rep, root, "", V1, False) check_tree(rep, root, "v2", V2, True) check_rejects(rep, root) + check_accepts(rep, root) check_mutations(rep) if rep.failures: print("%d failed — NOT conformant" % rep.failures) diff --git a/wai/wai-pack/Cargo.lock b/wai/wai-pack/Cargo.lock index b69d57caa..11894f19c 100644 --- a/wai/wai-pack/Cargo.lock +++ b/wai/wai-pack/Cargo.lock @@ -686,7 +686,6 @@ version = "0.4.0" dependencies = [ "blake3", "ed25519-dalek", - "serde", "serde_json", ] diff --git a/wai/wai-quantum-serve/Cargo.lock b/wai/wai-quantum-serve/Cargo.lock index ec3a4d98f..b196c468c 100644 --- a/wai/wai-quantum-serve/Cargo.lock +++ b/wai/wai-quantum-serve/Cargo.lock @@ -593,7 +593,6 @@ version = "0.4.0" dependencies = [ "blake3", "ed25519-dalek", - "serde", "serde_json", ] diff --git a/wai/wai-quantum/Cargo.lock b/wai/wai-quantum/Cargo.lock index d8d34a1da..51a8fe778 100644 --- a/wai/wai-quantum/Cargo.lock +++ b/wai/wai-quantum/Cargo.lock @@ -419,7 +419,6 @@ version = "0.4.0" dependencies = [ "blake3", "ed25519-dalek", - "serde", "serde_json", ] diff --git a/wai/wai-quantum/Cargo.toml b/wai/wai-quantum/Cargo.toml index d2db8c404..9561e8e23 100644 --- a/wai/wai-quantum/Cargo.toml +++ b/wai/wai-quantum/Cargo.toml @@ -23,9 +23,6 @@ all-features = true blake3 = "1" # Canonical receipt JSON (sorted keys, byte-stable round-trip). serde_json = { version = "1", features = ["preserve_order"] } -# Reading a container's JSON sections as I-JSON: a visitor refuses a member -# name repeated within an object. -serde = "1" # Receipt signatures. Optional — only the sealing features need it. ed25519-dalek = { version = "2", optional = true } diff --git a/wai/wai-quantum/src/ijson.rs b/wai/wai-quantum/src/ijson.rs new file mode 100644 index 000000000..0d0eeb8ca --- /dev/null +++ b/wai/wai-quantum/src/ijson.rs @@ -0,0 +1,437 @@ +//! The JSON a WQC container's contract and measurement sections are read as +//! (extensions/quantum-sim §2): I-JSON (RFC 7493), with every rule decided here +//! from the section's text. +//! +//! A general JSON parser leaves some of these rules to its build: whether a +//! number near the largest double is in range, for one, can depend on which +//! float parser was compiled in. A conformance verdict must not, so this reader +//! keeps each number's literal text and decides each rule itself. + +use std::collections::BTreeSet; +use std::fmt; + +/// The deepest nesting of arrays and objects a section may hold, the outermost +/// counted as the first. +pub(crate) const MAX_DEPTH: usize = 127; + +/// A JSON value as the sections need it. A number keeps its literal text: the +/// sections read only unsigned integers, and the range rule is decided on the +/// text. +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) enum Json { + Null, + Bool(bool), + Number(String), + String(String), + Array(Vec), + Object(Vec<(String, Json)>), +} + +impl Json { + /// The member `name` of an object; `None` for a missing member or a value + /// that is not an object. + pub(crate) fn get(&self, name: &str) -> Option<&Json> { + match self { + Json::Object(members) => members.iter().find(|(k, _)| k == name).map(|(_, v)| v), + _ => None, + } + } + + pub(crate) fn as_str(&self) -> Option<&str> { + match self { + Json::String(s) => Some(s), + _ => None, + } + } + + /// The value of an unsigned JSON integer that fits a `u64`: a number written + /// with no sign, fraction or exponent. `-0`, `2.0` and `2e0` are not one. + pub(crate) fn as_u64(&self) -> Option { + match self { + Json::Number(t) if t.bytes().all(|b| b.is_ascii_digit()) => t.parse().ok(), + _ => None, + } + } +} + +impl fmt::Display for Json { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + Json::Null => f.write_str("null"), + Json::Bool(b) => write!(f, "{b}"), + Json::Number(t) => f.write_str(t), + Json::String(s) => write!(f, "{s:?}"), + Json::Array(items) => { + f.write_str("[")?; + for (i, v) in items.iter().enumerate() { + if i > 0 { + f.write_str(",")?; + } + write!(f, "{v}")?; + } + f.write_str("]") + } + Json::Object(members) => { + f.write_str("{")?; + for (i, (k, v)) in members.iter().enumerate() { + if i > 0 { + f.write_str(",")?; + } + write!(f, "{k:?}:{v}")?; + } + f.write_str("}") + } + } + } +} + +/// Why a section is not I-JSON: the rule it breaks, and the byte offset where +/// the reader found it. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) struct NotIJson { + /// One of `utf-8`, `syntax`, `depth`, `duplicate member`, `surrogate`, + /// `noncharacter` and `number range`. + pub(crate) rule: &'static str, + pub(crate) at: usize, +} + +impl fmt::Display for NotIJson { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{} at byte {}", self.rule, self.at) + } +} + +/// `raw` read as one I-JSON value, with whitespace allowed around it. +pub(crate) fn parse(raw: &[u8]) -> Result { + let text = std::str::from_utf8(raw).map_err(|e| NotIJson { rule: "utf-8", at: e.valid_up_to() })?; + let mut p = Parser { s: text.as_bytes(), text, i: 0 }; + p.ws(); + let v = p.value(0)?; + p.ws(); + if p.i != p.s.len() { + return Err(p.err("syntax")); + } + Ok(v) +} + +/// Whether `c` is a Unicode noncharacter: U+FDD0 to U+FDEF, and the last two +/// code points of every plane. +pub(crate) fn is_noncharacter(c: char) -> bool { + let c = u32::from(c); + (0xFDD0..=0xFDEF).contains(&c) || c & 0xFFFE == 0xFFFE +} + +/// Whether a JSON number's literal rounds to an infinite IEEE 754 double: +/// whether its magnitude is at least 2^1024 − 2^970. The standard library's +/// parse rounds correctly to nearest, ties to even, at any length. +fn rounds_to_infinity(literal: &str) -> bool { + literal.parse::().map_or(true, f64::is_infinite) +} + +struct Parser<'a> { + s: &'a [u8], + text: &'a str, + i: usize, +} + +impl Parser<'_> { + fn err(&self, rule: &'static str) -> NotIJson { + NotIJson { rule, at: self.i } + } + + fn peek(&self) -> Option { + self.s.get(self.i).copied() + } + + fn ws(&mut self) { + while matches!(self.peek(), Some(b' ' | b'\t' | b'\n' | b'\r')) { + self.i += 1; + } + } + + fn eat(&mut self, b: u8) -> Result<(), NotIJson> { + if self.peek() == Some(b) { + self.i += 1; + Ok(()) + } else { + Err(self.err("syntax")) + } + } + + /// A value inside `depth` arrays and objects. + fn value(&mut self, depth: usize) -> Result { + match self.peek() { + Some(b'{') => self.object(depth + 1), + Some(b'[') => self.array(depth + 1), + Some(b'"') => self.string().map(Json::String), + Some(b'-' | b'0'..=b'9') => self.number(), + Some(b't') => self.literal("true", Json::Bool(true)), + Some(b'f') => self.literal("false", Json::Bool(false)), + Some(b'n') => self.literal("null", Json::Null), + _ => Err(self.err("syntax")), + } + } + + fn literal(&mut self, word: &str, v: Json) -> Result { + if self.s[self.i..].starts_with(word.as_bytes()) { + self.i += word.len(); + Ok(v) + } else { + Err(self.err("syntax")) + } + } + + fn object(&mut self, depth: usize) -> Result { + if depth > MAX_DEPTH { + return Err(self.err("depth")); + } + self.i += 1; + let (mut members, mut seen) = (Vec::new(), BTreeSet::new()); + self.ws(); + if self.peek() == Some(b'}') { + self.i += 1; + return Ok(Json::Object(members)); + } + loop { + self.ws(); + let at = self.i; + if self.peek() != Some(b'"') { + return Err(self.err("syntax")); + } + let name = self.string()?; + if !seen.insert(name.clone()) { + return Err(NotIJson { rule: "duplicate member", at }); + } + self.ws(); + self.eat(b':')?; + self.ws(); + let v = self.value(depth)?; + members.push((name, v)); + self.ws(); + match self.peek() { + Some(b',') => self.i += 1, + Some(b'}') => { + self.i += 1; + return Ok(Json::Object(members)); + } + _ => return Err(self.err("syntax")), + } + } + } + + fn array(&mut self, depth: usize) -> Result { + if depth > MAX_DEPTH { + return Err(self.err("depth")); + } + self.i += 1; + let mut items = Vec::new(); + self.ws(); + if self.peek() == Some(b']') { + self.i += 1; + return Ok(Json::Array(items)); + } + loop { + self.ws(); + items.push(self.value(depth)?); + self.ws(); + match self.peek() { + Some(b',') => self.i += 1, + Some(b']') => { + self.i += 1; + return Ok(Json::Array(items)); + } + _ => return Err(self.err("syntax")), + } + } + } + + /// Four hex digits after `\u`. + fn hex4(&mut self) -> Result { + let digits = self.s.get(self.i..self.i + 4).ok_or_else(|| self.err("syntax"))?; + let mut v = 0; + for &d in digits { + v = v * 16 + char::from(d).to_digit(16).ok_or_else(|| self.err("syntax"))?; + } + self.i += 4; + Ok(v) + } + + /// A string, unescaped. No surrogate and no noncharacter, raw or escaped; + /// an escaped surrogate pair is the character it encodes. + fn string(&mut self) -> Result { + self.i += 1; + let mut out = String::new(); + loop { + let at = self.i; + let c = match self.peek() { + None => return Err(self.err("syntax")), + Some(b'"') => { + self.i += 1; + return Ok(out); + } + Some(0..=0x1F) => return Err(self.err("syntax")), + Some(b'\\') => { + self.i += 1; + let e = self.peek().ok_or_else(|| self.err("syntax"))?; + self.i += 1; + match e { + b'"' => '"', + b'\\' => '\\', + b'/' => '/', + b'b' => '\u{8}', + b'f' => '\u{c}', + b'n' => '\n', + b'r' => '\r', + b't' => '\t', + b'u' => { + let hi = self.hex4()?; + let code = match hi { + 0xD800..=0xDBFF => { + if !self.s[self.i..].starts_with(b"\\u") { + return Err(NotIJson { rule: "surrogate", at }); + } + self.i += 2; + let lo = self.hex4()?; + if !(0xDC00..=0xDFFF).contains(&lo) { + return Err(NotIJson { rule: "surrogate", at }); + } + 0x10000 + ((hi - 0xD800) << 10) + (lo - 0xDC00) + } + 0xDC00..=0xDFFF => return Err(NotIJson { rule: "surrogate", at }), + _ => hi, + }; + char::from_u32(code).ok_or(NotIJson { rule: "surrogate", at })? + } + _ => return Err(NotIJson { rule: "syntax", at }), + } + } + Some(_) => { + let c = self.text[self.i..].chars().next().ok_or_else(|| self.err("syntax"))?; + self.i += c.len_utf8(); + c + } + }; + if is_noncharacter(c) { + return Err(NotIJson { rule: "noncharacter", at }); + } + out.push(c); + } + } + + /// `-? (0 | [1-9][0-9]*) (. [0-9]+)? ([eE] [+-]? [0-9]+)?`, in range. + fn number(&mut self) -> Result { + let start = self.i; + let digits = |p: &mut Self| { + let from = p.i; + while matches!(p.peek(), Some(b'0'..=b'9')) { + p.i += 1; + } + p.i - from + }; + if self.peek() == Some(b'-') { + self.i += 1; + } + match self.peek() { + Some(b'0') => self.i += 1, + Some(b'1'..=b'9') => { + digits(self); + } + _ => return Err(self.err("syntax")), + } + if self.peek() == Some(b'.') { + self.i += 1; + if digits(self) == 0 { + return Err(self.err("syntax")); + } + } + if matches!(self.peek(), Some(b'e' | b'E')) { + self.i += 1; + if matches!(self.peek(), Some(b'+' | b'-')) { + self.i += 1; + } + if digits(self) == 0 { + return Err(self.err("syntax")); + } + } + let literal = &self.text[start..self.i]; + if rounds_to_infinity(literal) { + return Err(NotIJson { rule: "number range", at: start }); + } + Ok(Json::Number(literal.to_string())) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + /// 2^1024 − 2^970: the least magnitude that rounds to an infinite double. + const THRESHOLD: &str = "179769313486231580793728971405303415079934132710037826936173778980444968292764750946649017977587207096330286416692887910946555547851940402630657488671505820681908902000708383676273854845817711531764475730270069855571366959622842914819860834936475292719074168444365510704342711559699508093042880177904174497792"; + + fn rule(text: &str) -> Option<&'static str> { + parse(text.as_bytes()).err().map(|e| e.rule) + } + + /// The range rule falls exactly at 2^1024 − 2^970, written as an integer or + /// with a fraction or exponent, and a number that underflows is in range. + #[test] + fn number_range_is_exact() { + assert_eq!(THRESHOLD.len(), 309); + let below = format!("{}1", &THRESHOLD[..308]); // THRESHOLD − 1, its last digit 2 + assert_eq!(below.as_bytes()[308], b'1'); + assert_eq!(rule(THRESHOLD), Some("number range")); + assert_eq!(rule(&format!("-{THRESHOLD}")), Some("number range")); + assert_eq!(rule(&below), None); + assert_eq!(rule(&format!("{}.{}e308", &THRESHOLD[..1], &THRESHOLD[1..])), Some("number range")); + assert_eq!(rule(&format!("{}.{}e308", &below[..1], &below[1..])), None); + assert_eq!(rule("1.7976931348623158e308"), None); + assert_eq!(rule("1.7976931348623159e308"), Some("number range")); + assert_eq!(rule("179769313486231570814527423731704356798070567525844996598917476803157260780028538760589558632766878171540458953514382464234321326889464182768467546703537516986049910576551282076245490090389328944075868508455133942304583236903222948165808559332123348274797826204144723168738177180919299881250404026184124858368"), None); + assert_eq!(rule("1e400"), Some("number range")); + assert_eq!(rule("1e99999999999999999999999"), Some("number range")); + assert_eq!(rule("1e-400"), None); + assert_eq!(rule("0e99999999999999999999999"), None); + assert_eq!(rule("-0"), None); + } + + /// Only a number with no sign, fraction or exponent is an unsigned integer, + /// and only one that fits a `u64` reads as one. + #[test] + fn unsigned_integers() { + let n = |t: &str| parse(t.as_bytes()).unwrap().as_u64(); + assert_eq!(n("0"), Some(0)); + assert_eq!(n("18446744073709551615"), Some(u64::MAX)); + assert_eq!(n("18446744073709551616"), None); + for t in ["-0", "-1", "2.0", "2e0", "2E0"] { + assert_eq!(n(t), None, "{t}"); + } + } + + /// Each rule, refused by name; and what the rules allow. + #[test] + fn rules() { + let deep = |n: usize| format!("{}{}", "[".repeat(n), "]".repeat(n)); + assert_eq!(rule(&deep(MAX_DEPTH)), None); + assert_eq!(rule(&deep(MAX_DEPTH + 1)), Some("depth")); + assert_eq!(rule(&format!("{{\"a\":{}}}", deep(MAX_DEPTH - 1))), None); + assert_eq!(rule(&format!("{{\"a\":{}}}", deep(MAX_DEPTH))), Some("depth")); + assert_eq!(rule(r#"{"a":1,"a":1}"#), Some("duplicate member")); + assert_eq!(rule(r#"{"a":1,"\u0061":2}"#), Some("duplicate member")); + assert_eq!(rule(r#"[{"a":1},{"a":1}]"#), None); + assert_eq!(rule(r#"{"a":{"b":1,"b":2}}"#), Some("duplicate member")); + assert_eq!(rule(r#""\ud800""#), Some("surrogate")); + assert_eq!(rule(r#""\udc00""#), Some("surrogate")); + assert_eq!(rule(r#""\ud800\u0041""#), Some("surrogate")); + assert_eq!(rule(r#""\ud83d\ude00""#), None); + assert_eq!(rule(r#""\ufdd0""#), Some("noncharacter")); + assert_eq!(rule(r#""\ud83f\udffe""#), Some("noncharacter")); + assert_eq!(rule("\"\u{ffff}\""), Some("noncharacter")); + assert_eq!(rule(r#"{"\uffff":1}"#), Some("noncharacter")); + assert_eq!(rule("\"\u{fffd}\u{fdcf}\u{fdf0}\""), None); + assert_eq!(parse(b"\"\xff\"").unwrap_err(), NotIJson { rule: "utf-8", at: 1 }); + for bad in ["", "\u{feff}{}", "{} x", "NaN", "Infinity", "01", "1.", ".5", "+1", "1e", "[1,]", "{\"a\"}", "\"\x01\"", "\"\\x\"", "tru", "'a'"] { + assert_eq!(rule(bad), Some("syntax"), "{bad:?}"); + } + assert_eq!(parse(b" \t\n\r{ \"a\" : [ 1 , -2.5e-3 , true , false , null ] }\r\n").unwrap().get("a").map(Json::to_string).as_deref(), Some("[1,-2.5e-3,true,false,null]")); + } +} diff --git a/wai/wai-quantum/src/lib.rs b/wai/wai-quantum/src/lib.rs index 2a9e5fe81..52f1fc996 100644 --- a/wai/wai-quantum/src/lib.rs +++ b/wai/wai-quantum/src/lib.rs @@ -51,6 +51,10 @@ mod repro; pub use ed25519_dalek; +// The strict JSON reader for a container's contract and measurement sections. +#[cfg(feature = "quantum")] +mod ijson; + #[cfg(feature = "quantum")] pub mod quantum; diff --git a/wai/wai-quantum/src/quantum.rs b/wai/wai-quantum/src/quantum.rs index d20db5f14..6dcde59a1 100644 --- a/wai/wai-quantum/src/quantum.rs +++ b/wai/wai-quantum/src/quantum.rs @@ -54,6 +54,8 @@ //! this exact-reconstruction extension. [`StateVector::fidelity_fx`] is provided as the //! quantum-information-theoretic conformance metric such a future path would use. +use crate::ijson::{self, Json}; + /// Fractional bits of the fixed-point amplitude floor. Amplitudes live in /// `[-1, 1]`, so `2^FRAC` (≈ `1.07e9` at 30) is the unit and the i128 products in /// [`Amp::mul`] stay far inside range. @@ -485,7 +487,7 @@ pub struct Circuit { pub enum QuantumError { /// A container whose structure is broken: a bad magic, a section out of /// bounds, repeated or missing, an `n_qubits` that is not an integer in - /// `0..=255`, a malformed op-log, or a measurement that is not JSON. In + /// `0..=255`, a malformed op-log, or a measurement that is not I-JSON. In /// `WQC2`, also bytes after the last op or a measurement that is not the /// canonical bytes. Malformed(String), @@ -1011,7 +1013,7 @@ fn section_range(blob_start: usize, off: u32, len: u32, total: usize) -> Option< /// How a member of a contract or measurement object reads in an error: its /// JSON, cut at 64 characters, since it comes from an untrusted file. -fn shown(v: Option<&serde_json::Value>) -> String { +fn shown(v: Option<&Json>) -> String { let Some(v) = v else { return "missing".to_string(); }; @@ -1022,81 +1024,22 @@ fn shown(v: Option<&serde_json::Value>) -> String { } } -/// A JSON text in which no object repeats a member name: the visitor walks -/// every value and refuses the second occurrence of a name. -struct Unique; - -impl<'de> serde::Deserialize<'de> for Unique { - fn deserialize>(d: D) -> Result { - struct Walk; - impl<'de> serde::de::Visitor<'de> for Walk { - type Value = Unique; - fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - f.write_str("a JSON value") - } - fn visit_bool(self, _: bool) -> Result { - Ok(Unique) - } - fn visit_i64(self, _: i64) -> Result { - Ok(Unique) - } - fn visit_u64(self, _: u64) -> Result { - Ok(Unique) - } - fn visit_f64(self, _: f64) -> Result { - Ok(Unique) - } - fn visit_str(self, _: &str) -> Result { - Ok(Unique) - } - fn visit_unit(self) -> Result { - Ok(Unique) - } - fn visit_seq>(self, mut seq: A) -> Result { - while seq.next_element::()?.is_some() {} - Ok(Unique) - } - fn visit_map>(self, mut map: A) -> Result { - let mut seen = std::collections::BTreeSet::new(); - while let Some(name) = map.next_key::()? { - if !seen.insert(name.clone()) { - return Err(serde::de::Error::custom(format!("member {name:?} appears twice"))); - } - map.next_value::()?; - } - Ok(Unique) - } - } - d.deserialize_any(Walk) - } -} - -/// A section read as I-JSON (RFC 7493), as §2 of the extension requires: -/// UTF-8, no unpaired surrogate escape, no number that rounds to an infinite -/// double, at most 127 nested arrays and objects, and no member name twice in -/// one object. -fn i_json(raw: &[u8]) -> Result { - serde_json::from_slice::(raw).map_err(|e| e.to_string())?; - serde_json::from_slice(raw).map_err(|e: serde_json::Error| e.to_string()) -} - /// Read a contract section under the gateset its container's magic names, and /// return its `n_qubits`. /// -/// The section must be an I-JSON object ([`i_json`]), or it is a contract -/// mismatch, and -/// `n_qubits` must be a JSON unsigned integer of at most 255, or the container -/// is malformed. circuit/1 requires `ext`, `numeric`, `frac` (the integer 30) +/// The section must be an I-JSON object (extensions/quantum-sim §2, read by +/// [`ijson::parse`]), or it is a contract mismatch, and `n_qubits` must be an +/// unsigned JSON integer of at most 255, or the container is malformed. circuit/1 requires `ext`, `numeric`, `frac` (the integer 30) /// and `gateset` to carry its values, and leaves member order, whitespace and /// other members free. circuit2 requires the canonical bytes exactly. fn read_contract(gateset: Gateset, raw: &[u8]) -> Result { - let cv = i_json(raw).map_err(|e| QuantumError::Contract(format!("the contract is not I-JSON: {e}")))?; - let Some(obj) = cv.as_object() else { - return Err(QuantumError::Contract(format!("the contract is not a JSON object: {}", shown(Some(&cv))))); - }; + let obj = ijson::parse(raw).map_err(|e| QuantumError::Contract(format!("the contract is not I-JSON: {e}")))?; + if !matches!(obj, Json::Object(_)) { + return Err(QuantumError::Contract(format!("the contract is not a JSON object: {}", shown(Some(&obj))))); + } let n_qubits = || { obj.get("n_qubits") - .and_then(serde_json::Value::as_u64) + .and_then(Json::as_u64) .and_then(|n| u8::try_from(n).ok()) .ok_or_else(|| { QuantumError::Malformed(format!("n_qubits {} is not an integer in 0..=255", shown(obj.get("n_qubits")))) @@ -1105,7 +1048,7 @@ fn read_contract(gateset: Gateset, raw: &[u8]) -> Result { match gateset { Gateset::V1 => { let text = |key: &str, value: &str| { - if obj.get(key).and_then(serde_json::Value::as_str) == Some(value) { + if obj.get(key).and_then(Json::as_str) == Some(value) { Ok(()) } else { Err(QuantumError::Contract(format!("{key} is {}, not {value:?}", shown(obj.get(key))))) @@ -1113,7 +1056,7 @@ fn read_contract(gateset: Gateset, raw: &[u8]) -> Result { }; text("ext", gateset.ext())?; text("numeric", gateset.numeric())?; - if obj.get("frac").and_then(serde_json::Value::as_u64) != Some(u64::from(FRAC)) { + if obj.get("frac").and_then(Json::as_u64) != Some(u64::from(FRAC)) { return Err(QuantumError::Contract(format!("frac is {}, not {FRAC}", shown(obj.get("frac"))))); } text("gateset", gateset.name())?; @@ -1132,22 +1075,21 @@ fn read_contract(gateset: Gateset, raw: &[u8]) -> Result { } } -/// Read a measurement section. It must be I-JSON ([`i_json`]). circuit/1 then +/// Read a measurement section. It must be I-JSON ([`ijson::parse`]). circuit/1 then /// reads it as it always has: a missing or non-integer `seed` or `shots` reads /// as 0. /// circuit2 requires a `u64` `seed` and `shots` and the canonical bytes for /// them, which fixes the basis as `computational`. fn read_measure(gateset: Gateset, raw: &[u8]) -> Result { - let mv = i_json(raw).map_err(|e| QuantumError::Malformed(format!("the measurement is not I-JSON: {e}")))?; + let mv = ijson::parse(raw).map_err(|e| QuantumError::Malformed(format!("the measurement is not I-JSON: {e}")))?; + let field = |key: &str| mv.get(key).and_then(Json::as_u64); match gateset { - Gateset::V1 => Ok(Measure { seed: mv["seed"].as_u64().unwrap_or(0), shots: mv["shots"].as_u64().unwrap_or(0) }), + Gateset::V1 => Ok(Measure { seed: field("seed").unwrap_or(0), shots: field("shots").unwrap_or(0) }), Gateset::V2 => { - let field = |key: &str| { - mv.get(key) - .and_then(serde_json::Value::as_u64) - .ok_or_else(|| QuantumError::Malformed(format!("measure {key} is {}, not a u64", shown(mv.get(key))))) + let u64_field = |key: &str| { + field(key).ok_or_else(|| QuantumError::Malformed(format!("measure {key} is {}, not a u64", shown(mv.get(key))))) }; - let m = Measure { seed: field("seed")?, shots: field("shots")? }; + let m = Measure { seed: u64_field("seed")?, shots: u64_field("shots")? }; if raw != measure_bytes(m).as_slice() { return Err(QuantumError::Malformed("not the canonical measure bytes".into())); } @@ -1220,9 +1162,11 @@ pub fn to_wqc(c: &Circuit, measure: Option) -> Result, QuantumE /// canonical bytes. Else malformed. /// /// I-JSON is extensions/quantum-sim §2's reading of a section: UTF-8, no -/// member name twice in one object, no unpaired surrogate escape, no number -/// beyond the double range, and at most 127 nested arrays and objects. -/// [`reject_corpus`] holds a payload for each refusal. +/// member name twice in one object, no surrogate or noncharacter, no number of +/// magnitude 2^1024 − 2^970 or more, and at most 127 nested arrays and +/// objects, each decided from the section's text. [`reject_corpus`] holds a +/// payload for each refusal, and [`accept_corpus`] payloads at the edges of +/// the rules that must be read. pub fn from_wqc(bytes: &[u8]) -> Result<(Circuit, Option), QuantumError> { if bytes.len() < 6 { return Err(QuantumError::Malformed("magic".into())); @@ -1339,6 +1283,17 @@ fn oplog_of(ops: &[Vec]) -> Vec { out } +/// 2^1024 − 2^970, the least magnitude that rounds to an infinite double: +/// the I-JSON range rule of extensions/quantum-sim §2 falls here. +const ROUNDS_TO_INFINITY: &str = "179769313486231580793728971405303415079934132710037826936173778980444968292764750946649017977587207096330286416692887910946555547851940402630657488671505820681908902000708383676273854845817711531764475730270069855571366959622842914819860834936475292719074168444365510704342711559699508093042880177904174497792"; + +/// `contract`, an object, with `members` added at its end. +fn with_members(contract: &[u8], members: &[u8]) -> Vec { + let (body, close) = contract.split_at(contract.len() - 1); + assert_eq!(close, b"}", "a contract is an object"); + [body, b",", members, b"}"].concat() +} + /// `bytes` with the first `from` replaced by `to`; `from` must be there. fn swap(bytes: &[u8], from: &str, to: &str) -> Vec { let s = std::str::from_utf8(bytes).expect("a contract is text"); @@ -1348,9 +1303,10 @@ fn swap(bytes: &[u8], from: &str, to: &str) -> Vec { /// The reject corpus of extensions/quantum-sim §2, as `(name, payload, class)`: /// payloads a reader must refuse, each with the class [`QuantumError::kind`] -/// gives it. Each is a two-qubit Bell container with one rule broken and every -/// rule checked before it kept, so the check order of [`from_wqc`] decides the -/// class. The conformance corpus's `reject/` tree is written from this list. +/// gives it. Each breaks one rule and keeps every rule checked before it, so +/// the check order of [`from_wqc`] decides the class; most are a two-qubit Bell +/// container with that one thing broken. The conformance corpus's `reject/` +/// tree is written from this list. pub fn reject_corpus() -> Vec<(&'static str, Vec, &'static str)> { let bell = oplog_of(&[op(4, &[], 0, 0), op(1, &[0], 1, 0)]); let canon1 = Gateset::V1.contract_bytes(2); @@ -1387,7 +1343,7 @@ pub fn reject_corpus() -> Vec<(&'static str, Vec, &'static str)> { let truncated_controls = [1u32.to_le_bytes().as_slice(), &[1, 2, 1, 0, 0, 0]].concat(); let measure = b"{\"seed\":1,\"shots\":10,\"basis\":\"computational\"}"; // A circuit/1 contract nesting 127 arrays inside its object: 128 levels, - // one past what §2.1 allows. + // one past what §2 allows. let deep = [&canon1[..canon1.len() - 1], b",\"x\":", &b"[".repeat(127), &b"]".repeat(127), b"}"].concat(); let reordered2 = format!( r#"{{"n_qubits":2,"ext":"{}","numeric":"{}","frac":30,"gateset":"{}"}}"#, @@ -1422,6 +1378,18 @@ pub fn reject_corpus() -> Vec<(&'static str, Vec, &'static str)> { ("v1_contract_lone_surrogate", v1(&swap(&canon1, "}", ",\"note\":\"\\ud800\"}")), "contract"), ("v1_contract_too_deep", v1(&deep), "contract"), ("v1_n_qubits_minus0", v1(&swap(&canon1, "\"n_qubits\":2", "\"n_qubits\":-0")), "malformed"), + ("v1_contract_lone_low_surrogate", v1(&with_members(&canon1, br#""note":"\udc00""#)), "contract"), + ("v1_contract_noncharacter", v1(&with_members(&canon1, br#""note":"\ufdd0""#)), "contract"), + ("v1_contract_raw_noncharacter", v1(&with_members(&canon1, "\"note\":\"\u{ffff}\"".as_bytes())), "contract"), + ( + "v1_contract_integer_rounds_to_infinity", + v1(&with_members(&canon1, format!("\"x\":{ROUNDS_TO_INFINITY}").as_bytes())), + "contract", + ), + ("v1_contract_number_rounds_to_infinity", v1(&with_members(&canon1, br#""x":1.7976931348623159e308"#)), "contract"), + ("v1_contract_bom", v1(&[b"\xef\xbb\xbf".as_slice(), &canon1].concat()), "contract"), + ("v1_contract_nan", v1(&with_members(&canon1, br#""x":NaN"#)), "contract"), + ("v1_contract_control_character", v1(&with_members(&canon1, b"\"note\":\"a\x01b\"")), "contract"), // §2.3: so is the measurement. ( "v1_measure_duplicate_member", @@ -1465,6 +1433,18 @@ pub fn reject_corpus() -> Vec<(&'static str, Vec, &'static str)> { ("v2_contract1_in_wqc2", v2_contract(&canon1), "contract"), ("v2_n_qubits_256", v2_contract(&swap(&canon2, "\"n_qubits\":2", "\"n_qubits\":256")), "malformed"), ("v2_contract_not_object", v2_contract(&[b"[".as_slice(), &canon2, b"]"].concat()), "contract"), + // The I-JSON rules come before n_qubits: out of range is a contract + // refusal, in range but not an integer a malformed one. + ( + "v2_n_qubits_rounds_to_infinity", + v2_contract(&swap(&canon2, "\"n_qubits\":2", "\"n_qubits\":1.7976931348623159e308")), + "contract", + ), + ( + "v2_n_qubits_largest_finite", + v2_contract(&swap(&canon2, "\"n_qubits\":2", "\"n_qubits\":1.7976931348623158e308")), + "malformed", + ), ("v2_phase_0", v2_oplog(&oplog_of(&[op(9, &[], 0, 0)])), "invalid"), ( "v2_measure_twice", @@ -1486,6 +1466,134 @@ pub fn reject_corpus() -> Vec<(&'static str, Vec, &'static str)> { ] } +/// The accept corpus of extensions/quantum-sim §2, as `(name, payload, circuit, +/// measurement)`: payloads a reader must accept, each with the circuit and the +/// measurement it reads as. They sit at the edges the reject corpus does not +/// reach from the other side: circuit/1's free formatting and further members, +/// every I-JSON rule at its limit, circuit/1's lax measurement, op-log bytes +/// after the last op, unknown sections, and the widest `n_qubits` and `u64` +/// values. The conformance corpus's `accept/` tree is written from this list. +pub fn accept_corpus() -> Vec<(&'static str, Vec, Circuit, Option)> { + let bell_ops = |gateset| Circuit { + n_qubits: 2, + ops: vec![ + Gate { base: BaseGate::H, controls: vec![], target: 0, param: 0 }, + Gate { base: BaseGate::X, controls: vec![0], target: 1, param: 0 }, + ], + gateset, + }; + let (bell1, bell2) = (bell_ops(Gateset::V1), bell_ops(Gateset::V2)); + let wide = |c: &Circuit| Circuit { n_qubits: 255, ..c.clone() }; + let bell = oplog_of(&[op(4, &[], 0, 0), op(1, &[0], 1, 0)]); + let canon1 = Gateset::V1.contract_bytes(2); + let canon2 = Gateset::V2.contract_bytes(2); + let v1 = |contract: &[u8]| wqc_raw(b"WQC1", &[(SECT_CONTRACT, contract), (SECT_OPLOG, &bell)]); + let v1_measure = |measure: &[u8]| wqc_raw(b"WQC1", &[(SECT_CONTRACT, &canon1), (SECT_OPLOG, &bell), (SECT_MEASURE, measure)]); + let m = |seed, shots| Some(Measure { seed, shots }); + let reordered = format!( + r#"{{"gateset":"{}","frac":30,"numeric":"{}","n_qubits":2,"ext":"{}"}}"#, + Gateset::V1.name(), + Gateset::V1.numeric(), + Gateset::V1.ext() + ); + let spaced = String::from_utf8(canon1.clone()).expect("a contract is text").replace(':', " : ").replace(',', " ,\n\t"); + let spaced = format!(" \r\n{spaced}\r\n "); + let depth = format!(r#""x":{}{}"#, "[".repeat(126), "]".repeat(126)); + let largest_integer = format!("{}1", &ROUNDS_TO_INFINITY[..ROUNDS_TO_INFINITY.len() - 1]); + let unknown = |magic: &[u8; 4], contract: &[u8]| { + wqc_raw(magic, &[(0x7F, b"later"), (SECT_CONTRACT, contract), (SECT_OPLOG, &bell), (0x04, b"")]) + }; + let mut param_on_h = bell1.clone(); + param_on_h.ops[0].param = 7; + let mut repeated_control = bell1.clone(); + repeated_control.ops[1].controls = vec![0, 0]; + vec![ + ("v1_members_reordered", v1(reordered.as_bytes()), bell1.clone(), None), + ("v1_whitespace", v1(spaced.as_bytes()), bell1.clone(), None), + ( + "v1_further_members", + v1(&with_members(&canon1, br#""note":"x","list":[0,-1,2.5,-3e-2,1E+2,true,false,null,{},[]],"obj":{"a":{"b":[]}},"again":{"note":1}"#)), + bell1.clone(), + None, + ), + ( + "v1_escapes_and_characters", + v1(&with_members(&canon1, "\"note\":\"\\\"\\\\\\/\\b\\f\\n\\r\\t\\u00e9\\ud83d\\ude00\\udbff\\udffd\u{e9}\u{fdcf}\u{fdf0}\u{fffd}\u{10fffd}\"".as_bytes())), + bell1.clone(), + None, + ), + ("v1_depth_127", v1(&with_members(&canon1, depth.as_bytes())), bell1.clone(), None), + ( + "v1_numbers_largest_finite", + v1(&with_members(&canon1, format!(r#""x":1.7976931348623158e308,"y":-1.7976931348623157e308,"z":{largest_integer},"w":-{largest_integer}"#).as_bytes())), + bell1.clone(), + None, + ), + ( + "v1_numbers_toward_zero", + v1(&with_members(&canon1, br#""x":1e-400,"y":-0,"z":0e99999999999999999999,"w":-0.0E-0"#)), + bell1.clone(), + None, + ), + ("v1_n_qubits_255", v1(&Gateset::V1.contract_bytes(255)), wide(&bell1), None), + ("v1_measure_lax", v1_measure(br#" { "basis" : "x" , "shots" : 10 , "seed" : 1 } "#), bell1.clone(), m(1, 10)), + ("v1_measure_missing_shots", v1_measure(br#"{"seed":1,"basis":"computational"}"#), bell1.clone(), m(1, 0)), + ( + "v1_measure_u64_max", + v1_measure(br#"{"seed":18446744073709551615,"shots":18446744073709551615}"#), + bell1.clone(), + m(u64::MAX, u64::MAX), + ), + ( + "v1_measure_not_u64", + v1_measure(br#"{"seed":18446744073709551616,"shots":-0,"x":2.5}"#), + bell1.clone(), + m(0, 0), + ), + ("v1_measure_other_types", v1_measure(br#"{"seed":"1","shots":1e1,"basis":null}"#), bell1.clone(), m(0, 0)), + ("v1_measure_not_object", v1_measure(b"[1,2]"), bell1.clone(), m(0, 0)), + ( + "v1_oplog_trailing_bytes", + wqc_raw(b"WQC1", &[(SECT_CONTRACT, &canon1), (SECT_OPLOG, &[bell.as_slice(), &[0, 1, 2]].concat())]), + bell1.clone(), + None, + ), + ( + "v1_param_on_h", + wqc_raw(b"WQC1", &[(SECT_CONTRACT, &canon1), (SECT_OPLOG, &oplog_of(&[op(4, &[], 0, 7), op(1, &[0], 1, 0)]))]), + param_on_h, + None, + ), + ( + "v1_repeated_control", + wqc_raw(b"WQC1", &[(SECT_CONTRACT, &canon1), (SECT_OPLOG, &oplog_of(&[op(4, &[], 0, 0), op(1, &[0, 0], 1, 0)]))]), + repeated_control, + None, + ), + ("v1_unknown_sections", unknown(b"WQC1", &canon1), bell1.clone(), None), + ("v2_unknown_sections", unknown(b"WQC2", &canon2), bell2.clone(), None), + ( + "v2_n_qubits_255", + wqc_raw(b"WQC2", &[(SECT_CONTRACT, &Gateset::V2.contract_bytes(255)), (SECT_OPLOG, &bell)]), + wide(&bell2), + None, + ), + ( + "v2_measure_u64_max", + wqc_raw( + b"WQC2", + &[ + (SECT_CONTRACT, &canon2), + (SECT_OPLOG, &bell), + (SECT_MEASURE, br#"{"seed":18446744073709551615,"shots":18446744073709551615,"basis":"computational"}"#), + ], + ), + bell2, + m(u64::MAX, u64::MAX), + ), + ] +} + #[cfg(test)] mod tests { use super::*; @@ -2100,39 +2208,16 @@ mod tests { assert_eq!(hx(&circuit_hash(&c).unwrap()), hash, "circuit2 {name}"); } + // The accept corpus reads as it says, and its Bell circuit is the + // vectors' bell. let [(_, bell, _), ..] = corpus_circuits(Gateset::V1); - let oplog = oplog_bytes(&bell); - let lax: [&[u8]; 3] = [ - br#"{"n_qubits":2,"gateset":"cliffordT+dyadicP","frac":30,"numeric":"wai.det.fixed64","ext":"wai.quantum.circuit/1"}"#, - b"{ \"ext\" : \"wai.quantum.circuit/1\",\n \"n_qubits\" : 2,\t\"numeric\":\"wai.det.fixed64\", \"frac\":30, \"gateset\":\"cliffordT+dyadicP\" }\n", - br#"{"ext":"wai.quantum.circuit/1","n_qubits":2,"numeric":"wai.det.fixed64","frac":30,"gateset":"cliffordT+dyadicP","note":"x"}"#, - ]; - for contract in lax { - let (back, m) = from_wqc(&wqc_raw(b"WQC1", &[(SECT_CONTRACT, contract), (SECT_OPLOG, &oplog)])).unwrap(); - assert_eq!((&back, m), (&bell, None), "{}", String::from_utf8_lossy(contract)); - assert_eq!(circuit_hash(&back).unwrap(), circuit_hash(&bell).unwrap()); - } - let canon = Gateset::V1.contract_bytes(2); - let trailing = [oplog.as_slice(), &[0, 1, 2]].concat(); - let (back, _) = from_wqc(&wqc_raw(b"WQC1", &[(SECT_CONTRACT, &canon), (SECT_OPLOG, &trailing)])).unwrap(); - assert_eq!(back, bell); - let lax_measures: [(&[u8], u64, u64); 3] = [ - (br#"{"seed": 1, "shots": 10, "basis": "computational"}"#, 1, 10), - (br#"{"seed":1,"basis":"computational"}"#, 1, 0), - (br#"{"seed":1,"shots":10,"basis":"x"}"#, 1, 10), - ]; - for (measure, seed, shots) in lax_measures { - let file = wqc_raw(b"WQC1", &[(SECT_CONTRACT, &canon), (SECT_OPLOG, &oplog), (SECT_MEASURE, measure)]); - assert_eq!(from_wqc(&file).unwrap(), (bell.clone(), Some(Measure { seed, shots }))); - } - for gs in Gateset::ALL { - let mut c = bell.clone(); - c.gateset = gs; - let file = wqc_raw( - &gs.magic(), - &[(0x7f, b"later"), (SECT_CONTRACT, &gs.contract_bytes(2)), (SECT_OPLOG, &oplog), (0x04, b"")], - ); - assert_eq!(from_wqc(&file).unwrap(), (c, None), "{gs:?}"); + let accepted = accept_corpus(); + assert_eq!(accepted[0].2, bell); + let names: std::collections::BTreeSet<_> = accepted.iter().map(|c| c.0).collect(); + assert_eq!(names.len(), accepted.len(), "case names repeat"); + for (name, file, circuit, measure) in accepted { + let (back, m) = from_wqc(&file).unwrap_or_else(|e| panic!("{name}: {e}")); + assert_eq!((&back, m), (&circuit, measure), "{name}"); } } @@ -2167,6 +2252,29 @@ mod tests { Err(e) => assert_eq!(e.kind(), *kind, "{name}: {e}"), } } + // The I-JSON cases are refused by the rule their names give. + for (name, rule) in [ + ("v1_contract_duplicate_member", "duplicate member"), + ("v1_contract_lone_surrogate", "surrogate"), + ("v1_contract_lone_low_surrogate", "surrogate"), + ("v1_contract_too_deep", "depth"), + ("v1_contract_not_utf8", "utf-8"), + ("v1_contract_number_out_of_range", "number range"), + ("v1_contract_integer_rounds_to_infinity", "number range"), + ("v1_contract_number_rounds_to_infinity", "number range"), + ("v1_contract_noncharacter", "noncharacter"), + ("v1_contract_raw_noncharacter", "noncharacter"), + ("v1_contract_bom", "syntax"), + ("v1_contract_nan", "syntax"), + ("v1_contract_control_character", "syntax"), + ("v2_n_qubits_rounds_to_infinity", "number range"), + ("v1_measure_duplicate_member", "duplicate member"), + ("v1_measure_not_json", "syntax"), + ] { + let (_, file, _) = cases.iter().find(|c| c.0 == name).unwrap_or_else(|| panic!("no case {name}")); + let e = from_wqc(file).unwrap_err().to_string(); + assert!(e.contains(&format!("I-JSON: {rule} at byte")), "{name}: {e}"); + } // Structure, beyond the corpus. let structural: [(&str, Vec, &str); 9] = [ diff --git a/wai/wai-rs/Cargo.lock b/wai/wai-rs/Cargo.lock index da53a5114..f43953910 100644 --- a/wai/wai-rs/Cargo.lock +++ b/wai/wai-rs/Cargo.lock @@ -3904,7 +3904,6 @@ version = "0.4.0" dependencies = [ "blake3", "ed25519-dalek", - "serde", "serde_json", ] diff --git a/wai/wai-rs/src/bin/wai_quantum_vectors.rs b/wai/wai-rs/src/bin/wai_quantum_vectors.rs index 8317b03f1..9a62f6cc4 100644 --- a/wai/wai-rs/src/bin/wai_quantum_vectors.rs +++ b/wai/wai-rs/src/bin/wai_quantum_vectors.rs @@ -5,7 +5,7 @@ //! hash and statevector hash (and, where a measurement is pinned, a shot-histogram //! hash — shot-histogram-equivalence). //! -//! The corpus has three trees: +//! The corpus has four trees: //! //! - `/`: `wai.quantum.circuit` (`WQC1`), `vectors/*.wqc` and //! `expected_hashes.json`. The four original vectors are frozen; three @@ -16,9 +16,13 @@ //! - `/reject/`: containers a conforming reader MUST refuse, and //! `reject.json`, which maps each file to the class it is refused as //! (`malformed`, `invalid` or `contract`). +//! - `/accept/`: containers a conforming reader MUST accept, at the edges +//! of the reader's rules, and `accept.json`, which gives each one's reading: +//! its capability, `n_qubits`, op count, `circuit_hash` and measurement. //! -//! `verify` checks all three: each vector decodes to the capability its entry -//! names and reproduces its hashes, and each reject file fails with its class. +//! `verify` checks all four: each vector decodes to the capability its entry +//! names and reproduces its hashes, each reject file fails with its class, and +//! each accept file reads as listed. //! //! `appendix` writes the phase tables of the extension's Appendix B, rendered //! from the reference constants, into the generated part of the file it is @@ -28,7 +32,7 @@ use std::collections::BTreeSet; use std::path::Path; -use wai::quantum::{circuit_hash, from_wqc, reject_corpus, to_wqc, BaseGate, Circuit, Gate, Gateset, Measure, DYADIC_MAX}; +use wai::quantum::{accept_corpus, circuit_hash, from_wqc, reject_corpus, to_wqc, BaseGate, Circuit, Gate, Gateset, Measure, DYADIC_MAX}; /// The circuit2 tree, under the corpus directory. const V2_DIR: &str = "v2"; @@ -36,6 +40,10 @@ const V2_DIR: &str = "v2"; const REJECT_DIR: &str = "reject"; /// The reject tree's listing: file → class. const REJECT_LISTING: &str = "reject.json"; +/// The accept tree, under the corpus directory. +const ACCEPT_DIR: &str = "accept"; +/// The accept tree's listing: file → reading. +const ACCEPT_LISTING: &str = "accept.json"; fn main() { let a: Vec = std::env::args().collect(); @@ -47,7 +55,7 @@ fn main() { eprintln!("{fails} failed — NOT conformant"); std::process::exit(1); } - println!("all vectors pass: statevector-equivalence holds, and every reject file is refused as listed"); + println!("all vectors pass: statevector-equivalence holds, every reject file is refused as listed, and every accept file reads as listed"); } (Some("appendix"), Some(f)) => write_appendix(Path::new(f)), _ => { @@ -173,11 +181,43 @@ fn reject_files(prefix: &str) -> Vec<(String, Vec)> { files } +/// How a container reads, as an accept listing states it. +fn reading(c: &Circuit, m: Option) -> serde_json::Value { + serde_json::json!({ + "capability": c.gateset.capability(), + "n_qubits": c.n_qubits, + "n_ops": c.ops.len(), + "circuit_hash": hex(&circuit_hash(c).expect("an accepted circuit hashes")), + "measure": m.map(|m| serde_json::json!({ "seed": m.seed, "shots": m.shots })), + }) +} + +/// The accept tree: `.wqc` for each case and the listing of its reading, +/// as (path under `prefix`, bytes). The listing is the reading the case +/// states, and each case is checked here to read so, so `gen` never writes an +/// accept file the reader refuses or misreads. +fn accept_files(prefix: &str) -> Vec<(String, Vec)> { + let mut files = Vec::new(); + let mut listing = serde_json::Map::new(); + for (name, bytes, circuit, measure) in accept_corpus() { + match from_wqc(&bytes) { + Ok(got) => assert_eq!(got, (circuit.clone(), measure), "accept case {name}"), + Err(e) => panic!("accept case {name} is refused: {e}"), + } + listing.insert(format!("{name}.wqc"), reading(&circuit, measure)); + files.push((format!("{prefix}{name}.wqc"), bytes)); + } + let text = serde_json::to_string_pretty(&serde_json::Value::Object(listing)).unwrap() + "\n"; + files.push((format!("{prefix}{ACCEPT_LISTING}"), text.into_bytes())); + files +} + /// Every file `gen` writes, as (path under the corpus directory, bytes). fn corpus_files() -> Vec<(String, Vec)> { let mut files = tree_files("", Gateset::V1, false); files.extend(tree_files(&format!("{V2_DIR}/"), Gateset::V2, true)); files.extend(reject_files(&format!("{REJECT_DIR}/"))); + files.extend(accept_files(&format!("{ACCEPT_DIR}/"))); files } @@ -240,9 +280,12 @@ fn write_appendix(path: &Path) { // ---- Verification ---------------------------------------------------------------- -/// Verify all three trees under `dir`; the number of failures. +/// Verify all four trees under `dir`; the number of failures. fn verify(dir: &Path) -> usize { - verify_tree(dir, "") + verify_tree(&dir.join(V2_DIR), &format!("{V2_DIR}/")) + verify_rejects(&dir.join(REJECT_DIR), &format!("{REJECT_DIR}/")) + verify_tree(dir, "") + + verify_tree(&dir.join(V2_DIR), &format!("{V2_DIR}/")) + + verify_rejects(&dir.join(REJECT_DIR), &format!("{REJECT_DIR}/")) + + verify_accepts(&dir.join(ACCEPT_DIR), &format!("{ACCEPT_DIR}/")) } /// The `.wqc` file names in `dir`. @@ -372,6 +415,25 @@ fn verify_rejects(dir: &Path, label: &str) -> usize { fails } +/// The accept tree: every listed file is accepted and reads as listed. +fn verify_accepts(dir: &Path, label: &str) -> usize { + let m = match read_object(&dir.join(ACCEPT_LISTING)) { + Ok(m) => m, + Err(why) => return report(&format!("{label}{ACCEPT_LISTING}"), Err(why)), + }; + let mut fails = report(&format!("{label} holds the listed files"), same_files(m.keys().cloned().collect(), wqc_names(dir))); + for (file, want) in &m { + let r = (|| -> Result { + let bytes = std::fs::read(dir.join(file)).map_err(|e| e.to_string())?; + let (c, measure) = from_wqc(&bytes).map_err(|e| format!("refused as {}: {e}", e.kind()))?; + let got = reading(&c, measure); + if &got == want { Ok(" (accepted, read as listed)".into()) } else { Err(format!("read as {got}, the listing says {want}")) } + })(); + fails += report(&format!("{label}{file}"), r); + } + fails +} + #[cfg(test)] mod tests { use super::*; @@ -403,9 +465,9 @@ mod tests { fn the_committed_corpus_is_what_gen_writes() { let dir = corpus(); let files = corpus_files(); - // Two trees of seven vectors and a manifest, and the reject tree: a - // file per case and the listing. - assert_eq!(files.len(), 2 * 8 + reject_corpus().len() + 1); + // Two trees of seven vectors and a manifest, and the reject and accept + // trees: a file per case and the listing. + assert_eq!(files.len(), 2 * 8 + reject_corpus().len() + 1 + accept_corpus().len() + 1); for (rel, want) in files { let mut got = std::fs::read(dir.join(&rel)).unwrap_or_else(|e| panic!("{rel}: {e}")); if rel.ends_with(".json") { @@ -445,7 +507,8 @@ mod tests { } /// `verify` fails a corpus whose v2 entry names the other capability, whose - /// reject listing names an accepted file or the wrong class, or that holds a + /// reject listing names an accepted file or the wrong class, whose accept + /// listing gives another reading or names a refused file, or that holds a /// file its listing does not. #[test] fn verify_fails_what_it_should() { @@ -465,6 +528,18 @@ mod tests { edit("reject/reject.json", "\"v2_param_on_h.wqc\": \"invalid\"", "\"v2_param_on_h.wqc\": \"malformed\""); assert_eq!(verify(&s.0), 1); edit("reject/reject.json", "\"v2_param_on_h.wqc\": \"malformed\"", "\"v2_param_on_h.wqc\": \"invalid\""); + // An accept entry with another reading. + edit("accept/accept.json", "\"seed\": 1,", "\"seed\": 2,"); + assert_eq!(verify(&s.0), 1); + edit("accept/accept.json", "\"seed\": 2,", "\"seed\": 1,"); + // A refused file in the accept tree. + std::fs::copy(s.0.join("reject/v2_param_on_h.wqc"), s.0.join("accept/v1_whitespace.wqc")).unwrap(); + assert_eq!(verify(&s.0), 1); + std::fs::copy(s.0.join("vectors/bell.wqc"), s.0.join("accept/v1_whitespace.wqc")).unwrap(); + assert_eq!(verify(&s.0), 1, "bell has a measurement; v1_whitespace has none"); + std::fs::remove_file(s.0.join("accept/v1_whitespace.wqc")).unwrap(); + generate(&s.0); + assert_eq!(verify(&s.0), 0); // An accepted file in the reject tree. std::fs::copy(s.0.join("v2/vectors/bell.wqc"), s.0.join("reject/v2_param_on_h.wqc")).unwrap(); assert_eq!(verify(&s.0), 1); diff --git a/wai/wai-web/Cargo.lock b/wai/wai-web/Cargo.lock index a448303a5..cf84498df 100644 --- a/wai/wai-web/Cargo.lock +++ b/wai/wai-web/Cargo.lock @@ -759,7 +759,6 @@ version = "0.4.0" dependencies = [ "blake3", "ed25519-dalek", - "serde", "serde_json", ] -- 2.54.0 (Apple Git-157) From e1f3b6faa0df87efe796a6d199130dbe0f391fd8 Mon Sep 17 00:00:00 2001 From: Transaction Science Date: Fri, 9 Oct 2026 18:50:35 -0400 Subject: [PATCH 13/13] wai-quantum circuit2: the number range decided on the literal's digits in both readers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A third review upheld six findings. - std's f64 parse stops adding exponent digits once the exponent reaches 0x10000, so a literal whose exponent offsets about 655,000 zeros was judged wrong both ways: exactly 1 refused, 10^309 accepted. The Rust reader now decides the range rule on the digits, as the referee does: the significant digits and the decimal exponent, with saturating exponent arithmetic, against the 309 digits of 2^1024 − 2^970. The limit is one constant, RANGE_LIMIT, and a test derives it in decimal from 2^1024 and 2^970 and checks it against std's parse where that parse is exact. - The referee passed a zero-padded exponent of over 4300 digits to int(), which Python refuses; it now converts only the significant exponent digits. - quantum-sim §8, the CHANGELOG and SPEC §5 now list both directions of the range change: numbers at or above the limit that earlier builds let through are refused, numbers below it that some builds refused are accepted. §2 says the rule is decided on the digits, at any length. - The corpus README and the wasip2 job comment were stale. The corpus holds both long literals and the padded exponent: an accept file (exactly 1 and exactly 10, written long) and a reject file (10^309). Co-Authored-By: Claude Opus 5.5 --- .github/workflows/wai-conformance.yml | 10 +- wai/CHANGELOG.md | 7 +- wai/SPEC.md | 4 +- wai/extensions/quantum-sim.md | 20 +-- wai/quantum-conformance/README.md | 9 +- wai/quantum-conformance/accept/accept.json | 7 ++ .../accept/v1_numbers_long_literals.wqc | Bin 0 -> 659833 bytes wai/quantum-conformance/reject/reject.json | 1 + .../reject/v1_contract_long_literal.wqc | Bin 0 -> 655525 bytes wai/quantum-conformance/tools/referee.py | 10 +- wai/wai-quantum/src/ijson.rs | 117 +++++++++++++++--- wai/wai-quantum/src/quantum.rs | 24 +++- 12 files changed, 167 insertions(+), 42 deletions(-) create mode 100644 wai/quantum-conformance/accept/v1_numbers_long_literals.wqc create mode 100644 wai/quantum-conformance/reject/v1_contract_long_literal.wqc diff --git a/.github/workflows/wai-conformance.yml b/.github/workflows/wai-conformance.yml index c291e4f7b..89f98ba79 100644 --- a/.github/workflows/wai-conformance.yml +++ b/.github/workflows/wai-conformance.yml @@ -469,10 +469,12 @@ jobs: # 1. tests/bit_identity.rs, which pins one result of each f64 layer, so the wasm32 # build must give the bits every native leg gives; # 2. the library's unit tests in the modules that hold the circuit core, its - # container and its pins (quantum, receipt_pins, quantum_fabric, quantum_source, - # quantum_channel, quantum_qudit, quantum_receipt), each required to have run; - # 3. wai_quantum_vectors verify over all three trees of quantum-conformance/ (both - # capabilities and the reject corpus), the same check the matrix above runs natively. + # container, its JSON reader and its pins (quantum, ijson, receipt_pins, + # quantum_fabric, quantum_source, quantum_channel, quantum_qudit, quantum_receipt), + # each required to have run; + # 3. wai_quantum_vectors verify over all four trees of quantum-conformance/ (both + # capabilities, the reject corpus and the accept corpus), the same check the matrix + # above runs natively. # One leg: wasm32 is the same machine on every host. Linux, so it does not queue on the # single macOS runner. quantum-wasip2: diff --git a/wai/CHANGELOG.md b/wai/CHANGELOG.md index 4cd85fe96..724400972 100644 --- a/wai/CHANGELOG.md +++ b/wai/CHANGELOG.md @@ -39,7 +39,9 @@ wire format may change and such changes are recorded here. `ext`, `numeric`, `frac` or `gateset` is missing or carries another value; an `n_qubits` that is not an unsigned integer of at most 255; a known section kind more than once; section bounds that overflow; a contract or measurement that repeats a - member name within one object or holds a noncharacter. No file a reference writer + member name within one object, holds a noncharacter, or holds a number of magnitude + 2^1024 − 2^970 or more that an earlier build's float parser let through (some builds + also refused numbers just below it, which are now accepted). No file a reference writer wrote is among them. - **Corpus.** `quantum-conformance/` keeps its four original vectors and manifest entries byte for byte and adds qft5_x22, dyadic_ladder and qft8_ones, which see the phase table: @@ -62,7 +64,8 @@ wire format may change and such changes are recorded here. lanes and the toolchain follow the circuit's gateset. `from_wqc` reads both magics, checks a circuit/1 contract's values and a circuit2 contract's bytes, reads the contract and the measurement as I-JSON with a strict reader of its own that decides every rule - from the text (so no build feature changes a verdict), refuses repeated sections and + from the text, the number range on the literal's digits (so neither a build feature nor + a float parser's limits changes a verdict), refuses repeated sections and wrapping bounds, and no longer sizes an allocation from a claimed op count. `reject_corpus()` and `accept_corpus()` return the payloads a reader must refuse, each with its class, and must accept, each with its reading; the corpus's `reject/` and diff --git a/wai/SPEC.md b/wai/SPEC.md index 2bd2b5703..fb69a7615 100644 --- a/wai/SPEC.md +++ b/wai/SPEC.md @@ -746,7 +746,9 @@ the registered `ext`, `numeric`, `frac` and `gateset`, a `wai.quantum.circuit2` contract among them; whose `n_qubits` is not an unsigned integer of at most 255; that carries a known section kind more than once; that has a section whose bounds overflow; or whose contract or measurement repeats a member name within one -object or holds a noncharacter. No payload a reference writer wrote is among them; +object, holds a noncharacter, or holds a number of magnitude 2^1024 − 2^970 or more +that an earlier reader's float parser let through. No payload a reference writer +wrote is among them; [quantum-sim](extensions/quantum-sim.md) §8 lists them with the rules. And the verify half — a signed, energy-accounted lineage for opaque diff --git a/wai/extensions/quantum-sim.md b/wai/extensions/quantum-sim.md index 63a368788..245767eb5 100644 --- a/wai/extensions/quantum-sim.md +++ b/wai/extensions/quantum-sim.md @@ -107,9 +107,11 @@ of these rules from the section's bytes: escape. An escaped surrogate pair stands for the one character it encodes. - No number has a magnitude of 2^1024 − 2^970 or more. These are exactly the numbers that round to an infinite IEEE 754 double, rounding to nearest with - ties to even, so a correctly rounding parse decides the rule, and so does an - exact comparison of the literal's digits. A number that rounds to zero is in - range. + ties to even. A reader decides the rule exactly, on the literal's digits: its + significant digits and its decimal exponent against the 309 digits of + 2^1024 − 2^970, however many digits or zeros the literal is written with. A + float parser decides it only where it rounds correctly at every length and + every exponent. A number that rounds to zero is in range. - Arrays and objects nest at most 127 deep, the outermost counted as the first. RFC 7493 also asks a sender to keep a number within a double's precision and an @@ -630,12 +632,14 @@ payloads that break them, and a reader of this revision refuses: target); - a contract or measurement that repeats a member name within one object (an earlier reader kept the last), or that holds a noncharacter in a name or a - string. + string; +- a contract or measurement holding a number of magnitude 2^1024 − 2^970 or + more that an earlier reader accepted. Earlier readers left the range to their + float parser, so depending on how they were built they accepted some such + numbers, and refused some numbers below the limit that a reader of this + revision accepts. -The other rules of §2 refuse what earlier reference readers refused, with one -difference the other way: those readers, depending on how they were built, also -refused some numbers just below 2^1024 − 2^970, which a reader of this revision -accepts. +The other rules of §2 refuse what earlier reference readers refused. No file written by any reference version is affected: every reference writer wrote the canonical contract, one section of each kind, and sections within the diff --git a/wai/quantum-conformance/README.md b/wai/quantum-conformance/README.md index 293ef3cbb..d40d872c6 100644 --- a/wai/quantum-conformance/README.md +++ b/wai/quantum-conformance/README.md @@ -50,7 +50,9 @@ node wai/quantum-conformance/browser_verify.mjs [path/to/quantum-ml/pkg] [module Drives the deployed wasm-bindgen bundle in headless Chromium: the sink opens every file of both vector trees to its manifest entry, refuses every reject file with -its listed class and reads every accept file as listed, and the IDE builds its catalog circuits in each gateset to the +its listed class and reads every accept file as listed (the two 255-qubit accept +files only to the `simulate` stage: the sink simulates on construction, so they +are checked to be read, not compared), and the IDE builds its catalog circuits in each gateset to the matching manifest. It is run by hand, before the website deploys a bundle. Note that the IDE's `statevector_hash_hex()` is empty until `run()` is called — the circuit is built eagerly, the state is not. @@ -86,8 +88,9 @@ where it stops: `|0…0⟩`, where every controlled phase acts on zero amplitudes, so no change to a `P(k)` entry moves their hashes except through `H`, which follows `re P(3)`; the three vectors after them are there for that. -- **One control at most.** Gates with more than one control appear only in the - reject tree; no vector applies one. +- **One control at most.** No vector applies a gate with more than one control. + Such gates appear in the reject tree, and in the accept tree as circuit/1's + repeated control. - **One measurement.** The shot sampler is pinned by bell's measurement alone. The floating-point layers (`quantum_source`, `quantum_channel`) are held to diff --git a/wai/quantum-conformance/accept/accept.json b/wai/quantum-conformance/accept/accept.json index e46112e5d..4d1915f9c 100644 --- a/wai/quantum-conformance/accept/accept.json +++ b/wai/quantum-conformance/accept/accept.json @@ -41,6 +41,13 @@ "circuit_hash": "894d9a9582b19f27b8d08d06938357237fc00f7f30bf4bfc2be0c04fa5e47a4e", "measure": null }, + "v1_numbers_long_literals.wqc": { + "capability": "wai.quantum.circuit", + "n_qubits": 2, + "n_ops": 2, + "circuit_hash": "894d9a9582b19f27b8d08d06938357237fc00f7f30bf4bfc2be0c04fa5e47a4e", + "measure": null + }, "v1_numbers_toward_zero.wqc": { "capability": "wai.quantum.circuit", "n_qubits": 2, diff --git a/wai/quantum-conformance/accept/v1_numbers_long_literals.wqc b/wai/quantum-conformance/accept/v1_numbers_long_literals.wqc new file mode 100644 index 0000000000000000000000000000000000000000..70318dad2aac2c720db0b4e598d20d12e85febdf GIT binary patch literal 659833 zcmeIu!AiqG5C-6+c=95?!d|Vlv9^a?eE}80qr_w*3%1fU!4~n|O~D7~(Ze?|1H=Ba zhwtU_zL;crHrCVSc{VwYi*fl5ao>kBd{t?-9jZ+~tY>xV>LK+vMVN-o>vnic{VtTZ z<1(ycm+HTG6Z=`4_OV&rjls67#)rjx8s4iu?&9&&`XjaNXV*MmHHWH6^~0Fkhq9O} z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXFoG1{l xSIgyMHJ^q2oyh)EX&Tu&hk7T=Pz4|Z!Z7< literal 0 HcmV?d00001 diff --git a/wai/quantum-conformance/reject/reject.json b/wai/quantum-conformance/reject/reject.json index 31386b1dd..dd288d20a 100644 --- a/wai/quantum-conformance/reject/reject.json +++ b/wai/quantum-conformance/reject/reject.json @@ -21,6 +21,7 @@ "v1_contract_raw_noncharacter.wqc": "contract", "v1_contract_integer_rounds_to_infinity.wqc": "contract", "v1_contract_number_rounds_to_infinity.wqc": "contract", + "v1_contract_long_literal.wqc": "contract", "v1_contract_bom.wqc": "contract", "v1_contract_nan.wqc": "contract", "v1_contract_control_character.wqc": "contract", diff --git a/wai/quantum-conformance/reject/v1_contract_long_literal.wqc b/wai/quantum-conformance/reject/v1_contract_long_literal.wqc new file mode 100644 index 0000000000000000000000000000000000000000..7b6cfbd6be7b876ca0fd6265697be9671dd98687 GIT binary patch literal 655525 zcmeIuO-chn5C+hm;L446=&fN))C`!d7f=vf$~Zrv!9+5d!HDSD?SKdH0I#45iu$O< zdw#rM&O#A#{e<%{JB^FHe8uiK#&!ItQ@Nk&-8gN_CJoJ$#;awV$KA_*dQIaYuCMbl zZMz{gfAO{(%RU{uwz|#1eyH=|W-*VOdh8C}>1p$p`u=@rpDx?ax=qbP&K=`=Q7#k# z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0Rja6C$Q|Q T)vBuQzGoqXv)rL5iadV-_4P*^ literal 0 HcmV?d00001 diff --git a/wai/quantum-conformance/tools/referee.py b/wai/quantum-conformance/tools/referee.py index 209054e65..8a621c079 100644 --- a/wai/quantum-conformance/tools/referee.py +++ b/wai/quantum-conformance/tools/referee.py @@ -323,9 +323,11 @@ def out_of_range(literal): significant = digits.lstrip("0").rstrip("0") if not significant: return False - if len(exponent.lstrip("+-").lstrip("0")) > 30: # beyond any length a section can hold + e = exponent.lstrip("+-").lstrip("0") # never the raw text: int() limits its length + if len(e) > 18: # a section holds at most 2^32 digits, so the sign alone decides return not exponent.startswith("-") - k = len(whole) - (len(digits) - len(digits.lstrip("0"))) + int(exponent or "0") + e = -int(e or "0") if exponent.startswith("-") else int(e or "0") + k = len(whole) - (len(digits) - len(digits.lstrip("0"))) + e if k != len(RANGE_LIMIT): return k > len(RANGE_LIMIT) n = max(len(significant), len(RANGE_LIMIT)) @@ -700,6 +702,7 @@ REJECTS = { "v1_contract_raw_noncharacter": ("contract", "not JSON: noncharacter"), "v1_contract_integer_rounds_to_infinity": ("contract", "not JSON: number range"), "v1_contract_number_rounds_to_infinity": ("contract", "not JSON: number range"), + "v1_contract_long_literal": ("contract", "not JSON: number range"), "v1_contract_bom": ("contract", "not JSON: syntax"), "v1_contract_nan": ("contract", "not JSON: syntax"), "v1_contract_control_character": ("contract", "not JSON: syntax"), @@ -952,7 +955,8 @@ def check_rejects(rep, root): # recomputed here and compared with the listing. ACCEPTS = [ "v1_members_reordered", "v1_whitespace", "v1_further_members", "v1_escapes_and_characters", - "v1_depth_127", "v1_numbers_largest_finite", "v1_numbers_toward_zero", "v1_n_qubits_255", + "v1_depth_127", "v1_numbers_largest_finite", "v1_numbers_long_literals", "v1_numbers_toward_zero", + "v1_n_qubits_255", "v1_measure_lax", "v1_measure_missing_shots", "v1_measure_u64_max", "v1_measure_not_u64", "v1_measure_other_types", "v1_measure_not_object", "v1_oplog_trailing_bytes", "v1_param_on_h", "v1_repeated_control", "v1_unknown_sections", "v2_unknown_sections", "v2_n_qubits_255", diff --git a/wai/wai-quantum/src/ijson.rs b/wai/wai-quantum/src/ijson.rs index 0d0eeb8ca..49e6c8230 100644 --- a/wai/wai-quantum/src/ijson.rs +++ b/wai/wai-quantum/src/ijson.rs @@ -4,8 +4,9 @@ //! //! A general JSON parser leaves some of these rules to its build: whether a //! number near the largest double is in range, for one, can depend on which -//! float parser was compiled in. A conformance verdict must not, so this reader -//! keeps each number's literal text and decides each rule itself. +//! float parser was compiled in, and a float parser may stop reading a long +//! exponent. A conformance verdict must not, so this reader keeps each number's +//! literal text and decides each rule itself, the range rule on the digits. use std::collections::BTreeSet; use std::fmt; @@ -14,6 +15,11 @@ use std::fmt; /// counted as the first. pub(crate) const MAX_DEPTH: usize = 127; +/// 2^1024 − 2^970 in decimal: the least magnitude that rounds to an infinite +/// double, rounding to nearest with ties to even. A number of this magnitude or +/// more is out of range (extensions/quantum-sim §2). +pub(crate) const RANGE_LIMIT: &str = "179769313486231580793728971405303415079934132710037826936173778980444968292764750946649017977587207096330286416692887910946555547851940402630657488671505820681908902000708383676273854845817711531764475730270069855571366959622842914819860834936475292719074168444365510704342711559699508093042880177904174497792"; + /// A JSON value as the sections need it. A number keeps its literal text: the /// sections read only unsigned integers, and the range rule is decided on the /// text. @@ -121,11 +127,44 @@ pub(crate) fn is_noncharacter(c: char) -> bool { (0xFDD0..=0xFDEF).contains(&c) || c & 0xFFFE == 0xFFFE } -/// Whether a JSON number's literal rounds to an infinite IEEE 754 double: -/// whether its magnitude is at least 2^1024 − 2^970. The standard library's -/// parse rounds correctly to nearest, ties to even, at any length. -fn rounds_to_infinity(literal: &str) -> bool { - literal.parse::().map_or(true, f64::is_infinite) +/// Whether a JSON number's magnitude is [`RANGE_LIMIT`] or more, decided on +/// its digits. Written as `0.D × 10^k`, with `D` its significant digits, the +/// number is out of range when `k` exceeds the limit's 309 digits, and when `k` +/// equals it and `D` is at least the limit's digits. +fn out_of_range(literal: &str) -> bool { + let unsigned = literal.strip_prefix('-').unwrap_or(literal); + let (mantissa, exponent) = unsigned.split_once(['e', 'E']).unwrap_or((unsigned, "")); + let (whole, frac) = mantissa.split_once('.').unwrap_or((mantissa, "")); + let digits: Vec = whole.bytes().chain(frac.bytes()).collect(); + let lead = digits.iter().take_while(|&&d| d == b'0').count(); + let Some(last) = digits.iter().rposition(|&d| d != b'0') else { + return false; // zero + }; + let significant = &digits[lead..=last]; + let (negative, e) = match exponent.as_bytes().first() { + Some(b'-') => (true, &exponent[1..]), + Some(b'+') => (false, &exponent[1..]), + _ => (false, exponent), + }; + let e = e.trim_start_matches('0'); + // A section holds at most u32::MAX digits, so an exponent past 10^18 decides + // the rule by its sign alone. + if e.len() > 18 { + return !negative; + } + let e: i64 = if e.is_empty() { 0 } else { e.parse().expect("at most 18 decimal digits") }; + let k = whole.len() as i64 - lead as i64 + if negative { -e } else { e }; + let limit = RANGE_LIMIT.as_bytes(); + if k != limit.len() as i64 { + return k > limit.len() as i64; + } + for i in 0..significant.len().max(limit.len()) { + let (a, b) = (significant.get(i).copied().unwrap_or(b'0'), limit.get(i).copied().unwrap_or(b'0')); + if a != b { + return a > b; + } + } + true } struct Parser<'a> { @@ -354,7 +393,7 @@ impl Parser<'_> { } } let literal = &self.text[start..self.i]; - if rounds_to_infinity(literal) { + if out_of_range(literal) { return Err(NotIJson { rule: "number range", at: start }); } Ok(Json::Number(literal.to_string())) @@ -365,8 +404,46 @@ impl Parser<'_> { mod tests { use super::*; - /// 2^1024 − 2^970: the least magnitude that rounds to an infinite double. - const THRESHOLD: &str = "179769313486231580793728971405303415079934132710037826936173778980444968292764750946649017977587207096330286416692887910946555547851940402630657488671505820681908902000708383676273854845817711531764475730270069855571366959622842914819860834936475292719074168444365510704342711559699508093042880177904174497792"; + /// 2^e in decimal digits, most significant first. + fn pow2(e: u32) -> Vec { + let mut d = vec![1u8]; // least significant first + for _ in 0..e { + let mut carry = 0; + for x in d.iter_mut() { + let v = *x * 2 + carry; + (*x, carry) = (v % 10, v / 10); + } + if carry > 0 { + d.push(carry); + } + } + d.reverse(); + d + } + + /// `a − b` for decimal digit strings with `a ≥ b`, most significant first. + fn sub(a: &[u8], b: &[u8]) -> String { + let (mut out, mut borrow) = (Vec::new(), 0i8); + for i in 0..a.len() { + let x = a[a.len() - 1 - i] as i8 - borrow - b.len().checked_sub(1 + i).map_or(0, |j| b[j] as i8); + (borrow, out) = (i8::from(x < 0), [vec![(x + if x < 0 { 10 } else { 0 }) as u8], out].concat()); + } + let s: String = out.iter().map(|d| char::from(b'0' + d)).collect(); + s.trim_start_matches('0').to_string() + } + + /// The limit is 2^1024 − 2^970, derived here in decimal, and the standard + /// library's parse, where it is exact, puts the limit in the same place. + #[test] + fn the_range_limit_is_2_1024_minus_2_970() { + assert_eq!(sub(&pow2(1024), &pow2(970)), RANGE_LIMIT); + let below = sub(RANGE_LIMIT.as_bytes().iter().map(|b| b - b'0').collect::>().as_slice(), &[1]); + assert!(RANGE_LIMIT.parse::().unwrap().is_infinite()); + assert_eq!(below.parse::().unwrap(), f64::MAX); + for t in ["1e308", "1.7976931348623157e308", "1.7976931348623158e308", "1.7976931348623159e308", "2e308", "9e307", "1e-400", RANGE_LIMIT, &below] { + assert_eq!(out_of_range(t), t.parse::().unwrap().is_infinite(), "{t}"); + } + } fn rule(text: &str) -> Option<&'static str> { parse(text.as_bytes()).err().map(|e| e.rule) @@ -376,13 +453,13 @@ mod tests { /// with a fraction or exponent, and a number that underflows is in range. #[test] fn number_range_is_exact() { - assert_eq!(THRESHOLD.len(), 309); - let below = format!("{}1", &THRESHOLD[..308]); // THRESHOLD − 1, its last digit 2 + assert_eq!(RANGE_LIMIT.len(), 309); + let below = format!("{}1", &RANGE_LIMIT[..308]); // RANGE_LIMIT − 1, its last digit 2 assert_eq!(below.as_bytes()[308], b'1'); - assert_eq!(rule(THRESHOLD), Some("number range")); - assert_eq!(rule(&format!("-{THRESHOLD}")), Some("number range")); + assert_eq!(rule(RANGE_LIMIT), Some("number range")); + assert_eq!(rule(&format!("-{RANGE_LIMIT}")), Some("number range")); assert_eq!(rule(&below), None); - assert_eq!(rule(&format!("{}.{}e308", &THRESHOLD[..1], &THRESHOLD[1..])), Some("number range")); + assert_eq!(rule(&format!("{}.{}e308", &RANGE_LIMIT[..1], &RANGE_LIMIT[1..])), Some("number range")); assert_eq!(rule(&format!("{}.{}e308", &below[..1], &below[1..])), None); assert_eq!(rule("1.7976931348623158e308"), None); assert_eq!(rule("1.7976931348623159e308"), Some("number range")); @@ -392,6 +469,16 @@ mod tests { assert_eq!(rule("1e-400"), None); assert_eq!(rule("0e99999999999999999999999"), None); assert_eq!(rule("-0"), None); + // Long literals whose exponent offsets a run of zeros, past where a + // float parser may stop reading the exponent; and long exponents. + let zeros = "0".repeat(655_360); + assert_eq!(rule(&format!("1{zeros}e-655360")), None); + assert_eq!(rule(&format!("0.{}1e655669", &zeros[1..])), Some("number range")); + assert_eq!(rule(&format!("0.{}1e655668", &zeros[1..])), None); + assert_eq!(rule(&format!("1e{}1", "0".repeat(4300))), None); + assert_eq!(rule(&format!("1e{}", "9".repeat(30))), Some("number range")); + assert_eq!(rule(&format!("1e-{}", "9".repeat(30))), None); + assert_eq!(rule(&format!("0e{}", "9".repeat(30))), None); } /// Only a number with no sign, fraction or exponent is an unsigned integer, diff --git a/wai/wai-quantum/src/quantum.rs b/wai/wai-quantum/src/quantum.rs index 6dcde59a1..12b3836fd 100644 --- a/wai/wai-quantum/src/quantum.rs +++ b/wai/wai-quantum/src/quantum.rs @@ -54,7 +54,7 @@ //! this exact-reconstruction extension. [`StateVector::fidelity_fx`] is provided as the //! quantum-information-theoretic conformance metric such a future path would use. -use crate::ijson::{self, Json}; +use crate::ijson::{self, Json, RANGE_LIMIT}; /// Fractional bits of the fixed-point amplitude floor. Amplitudes live in /// `[-1, 1]`, so `2^FRAC` (≈ `1.07e9` at 30) is the unit and the i128 products in @@ -1283,9 +1283,6 @@ fn oplog_of(ops: &[Vec]) -> Vec { out } -/// 2^1024 − 2^970, the least magnitude that rounds to an infinite double: -/// the I-JSON range rule of extensions/quantum-sim §2 falls here. -const ROUNDS_TO_INFINITY: &str = "179769313486231580793728971405303415079934132710037826936173778980444968292764750946649017977587207096330286416692887910946555547851940402630657488671505820681908902000708383676273854845817711531764475730270069855571366959622842914819860834936475292719074168444365510704342711559699508093042880177904174497792"; /// `contract`, an object, with `members` added at its end. fn with_members(contract: &[u8], members: &[u8]) -> Vec { @@ -1383,10 +1380,16 @@ pub fn reject_corpus() -> Vec<(&'static str, Vec, &'static str)> { ("v1_contract_raw_noncharacter", v1(&with_members(&canon1, "\"note\":\"\u{ffff}\"".as_bytes())), "contract"), ( "v1_contract_integer_rounds_to_infinity", - v1(&with_members(&canon1, format!("\"x\":{ROUNDS_TO_INFINITY}").as_bytes())), + v1(&with_members(&canon1, format!("\"x\":{RANGE_LIMIT}").as_bytes())), "contract", ), ("v1_contract_number_rounds_to_infinity", v1(&with_members(&canon1, br#""x":1.7976931348623159e308"#)), "contract"), + // 10^309, written with a run of zeros its exponent offsets. + ( + "v1_contract_long_literal", + v1(&with_members(&canon1, format!("\"x\":0.{}1e655669", "0".repeat(655_359)).as_bytes())), + "contract", + ), ("v1_contract_bom", v1(&[b"\xef\xbb\xbf".as_slice(), &canon1].concat()), "contract"), ("v1_contract_nan", v1(&with_members(&canon1, br#""x":NaN"#)), "contract"), ("v1_contract_control_character", v1(&with_members(&canon1, b"\"note\":\"a\x01b\"")), "contract"), @@ -1499,7 +1502,7 @@ pub fn accept_corpus() -> Vec<(&'static str, Vec, Circuit, Option)> let spaced = String::from_utf8(canon1.clone()).expect("a contract is text").replace(':', " : ").replace(',', " ,\n\t"); let spaced = format!(" \r\n{spaced}\r\n "); let depth = format!(r#""x":{}{}"#, "[".repeat(126), "]".repeat(126)); - let largest_integer = format!("{}1", &ROUNDS_TO_INFINITY[..ROUNDS_TO_INFINITY.len() - 1]); + let largest_integer = format!("{}1", &RANGE_LIMIT[..RANGE_LIMIT.len() - 1]); let unknown = |magic: &[u8; 4], contract: &[u8]| { wqc_raw(magic, &[(0x7F, b"later"), (SECT_CONTRACT, contract), (SECT_OPLOG, &bell), (0x04, b"")]) }; @@ -1529,6 +1532,14 @@ pub fn accept_corpus() -> Vec<(&'static str, Vec, Circuit, Option)> bell1.clone(), None, ), + // Exactly 1 and exactly 10, written long: a run of zeros the exponent + // offsets, and an exponent with leading zeros. + ( + "v1_numbers_long_literals", + v1(&with_members(&canon1, format!("\"x\":1{}e-655360,\"y\":1e{}1", "0".repeat(655_360), "0".repeat(4300)).as_bytes())), + bell1.clone(), + None, + ), ( "v1_numbers_toward_zero", v1(&with_members(&canon1, br#""x":1e-400,"y":-0,"z":0e99999999999999999999,"w":-0.0E-0"#)), @@ -2268,6 +2279,7 @@ mod tests { ("v1_contract_nan", "syntax"), ("v1_contract_control_character", "syntax"), ("v2_n_qubits_rounds_to_infinity", "number range"), + ("v1_contract_long_literal", "number range"), ("v1_measure_duplicate_member", "duplicate member"), ("v1_measure_not_json", "syntax"), ] { -- 2.54.0 (Apple Git-157)