wai-quantum 0.6.10: one OpenQASM reader for the service, the command line and the browser IDE; receipts signed with the signer's own key #356

Closed
dcharlot wants to merge 4 commits from wai/quantum-one-front-end into main
23 changed files with 354 additions and 87 deletions

View file

@ -161,8 +161,10 @@ jobs:
# 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 OTT demo seals a figure the page supplies as
# parameter on a gate other than P, and shows no hash for an invalid circuit;
# its import reads a program as the job service does and refuses one that
# measures and then acts, its replies stay valid JSON whatever an error says,
# and it signs with the page's own key once given one (quantum_toolchain::tests). The OTT demo seals a figure the page supplies as
# an estimate, never a reading (video_ott::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
@ -171,7 +173,7 @@ jobs:
run: |
set -euo pipefail
cargo test --locked --lib 2>&1 | tee "$RUNNER_TEMP/wai-web-native.log"
grep -q "test result: ok. 32 passed; 0 failed" "$RUNNER_TEMP/wai-web-native.log"
grep -q "test result: ok. 35 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

View file

@ -6,6 +6,34 @@ wire format may change and such changes are recorded here.
## v0.4.0 — current
### wai-quantum 0.6.10: one OpenQASM reader for the service, the command line and the browser IDE
Additive, apart from the old line reader's refusals below. Every pinned digest is unchanged.
- **`wai_quantum::qasm`** (feature `quantum_qasm`) is the job service's OpenQASM reader,
moved into the library unchanged. The service, `wai-quantum run --qasm` and the browser
IDE's import now read a program the same way. Before, the IDE read two registers as
one, skipped a gate on a whole register, and read past what it did not know.
- **`qasm::dyadic_circuit`** gives the exact state vector's circuit for a program whose
measurements all come at the end, and refuses one that measures and then acts, resets
after a gate or uses `if`, with the reason.
- **`quantum_toolchain::from_qasm`**, the older line reader, refuses what it cannot take
instead of dropping the line: more than one register, `reset`, gate definitions,
`if`/`while`/`for`, a gate after a measurement, two statements on one line. Each
refusal names `wai_quantum::qasm`.
- **The IDE binding** (`wai-web`) builds its JSON with `serde_json`, so a quote or a
control character in a program or an error no longer breaks a reply.
`set_signing_key`, `public_key_hex`, `signer_did` and `key_is_demo` let a page sign
receipts with a key of its own; without one it signs with the published demo key and
says so.
- **The command line** signs with `--key FILE`, `$WAI_QUANTUM_KEY`, or this user's key at
`$XDG_CONFIG_HOME/wai/quantum-key` (or `~/.config/wai/quantum-key`). That key is made
from the operating system's random source on first use, with mode 0600.
`--demo-key` keeps the published key, with a note that its receipts show a result is
unaltered, not who signed it.
- **A receipt from the command line or the IDE names its signer by its key's `did:key`**
(`wai::signer_id::did_key`), where it used to carry a label naming no key.
### wai-quantum 0.6.9: cut-rank amplitudes with the circuit's own global phase
Additive. Every pinned digest is unchanged.

View file

@ -1017,7 +1017,7 @@ checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
[[package]]
name = "wai-quantum"
version = "0.6.9"
version = "0.6.10"
dependencies = [
"blake3",
"ed25519-dalek",

View file

@ -599,7 +599,7 @@ checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
[[package]]
name = "wai-quantum"
version = "0.6.9"
version = "0.6.10"
dependencies = [
"blake3",
"ed25519-dalek",

View file

@ -713,7 +713,7 @@ dependencies = [
[[package]]
name = "wai-quantum"
version = "0.6.9"
version = "0.6.10"
dependencies = [
"blake3",
"ed25519-dalek",

View file

@ -422,7 +422,7 @@ checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
[[package]]
name = "wai-quantum"
version = "0.6.9"
version = "0.6.10"
dependencies = [
"blake3",
"ed25519-dalek",

View file

@ -599,7 +599,7 @@ dependencies = [
[[package]]
name = "wai-quantum"
version = "0.6.9"
version = "0.6.10"
dependencies = [
"blake3",
"ed25519-dalek",

View file

@ -857,7 +857,7 @@ checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
[[package]]
name = "wai-quantum"
version = "0.6.9"
version = "0.6.10"
dependencies = [
"blake3",
"ed25519-dalek",

View file

@ -17,7 +17,7 @@ serde = { version = "1", features = ["derive"] }
serde_json = { version = "1", features = ["float_roundtrip"] }
blake3 = "1"
getrandom = "0.4"
wai-quantum = { path = "../wai-quantum", default-features = false, features = ["quantum", "quantum_toolchain", "quantum_receipt", "quantum_sv", "quantum_tn", "quantum_tableau", "quantum_cutrank", "quantum_mpsim", "quantum_spd", "quantum_noisy", "quantum_sv_metal"] }
wai-quantum = { path = "../wai-quantum", default-features = false, features = ["quantum", "quantum_toolchain", "quantum_receipt", "quantum_sv", "quantum_tn", "quantum_tableau", "quantum_cutrank", "quantum_qasm", "quantum_mpsim", "quantum_spd", "quantum_noisy", "quantum_sv_metal"] }
# proc_pid_rusage and waitid, for each job's energy (src/energy.rs).
[target.'cfg(target_os = "macos")'.dependencies]

View file

@ -20,7 +20,7 @@ mod energy;
mod http;
mod jobs;
mod mcp;
mod qasm;
use wai_quantum::qasm;
mod receipt;
mod run;

View file

@ -1079,7 +1079,7 @@ checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
[[package]]
name = "wai-quantum"
version = "0.6.9"
version = "0.6.10"
dependencies = [
"blake3",
"block2",

View file

@ -1,6 +1,6 @@
[package]
name = "wai-quantum"
version = "0.6.9"
version = "0.6.10"
edition = "2024"
rust-version = "1.97"
license = "Apache-2.0"
@ -80,6 +80,9 @@ quantum_tableau = []
# Exact amplitudes, probabilities and samples of a Clifford circuit measured in a
# product basis, at a cost set by its cut-rank (src/quantum_cutrank.rs).
quantum_cutrank = ["quantum_tableau", "quantum_sv"]
# OpenQASM 3 (and the OpenQASM 2 forms in common use) read to one program and
# lowered to each method's input (src/qasm.rs).
quantum_qasm = ["quantum", "quantum_sv", "quantum_spd", "quantum_tableau"]
quantum_mpsim = ["quantum_sv", "quantum_mps"]
quantum_pauli = ["quantum"]
# Sparse Pauli dynamics at utility scale: arbitrary-angle Pauli rotations on up
@ -206,7 +209,7 @@ quantum_phasor_meter = ["_signing"]
# Everything.
full = [
"quantum", "quantum_toolchain", "quantum_mps", "quantum_stabilizer", "quantum_tableau", "quantum_cutrank", "quantum_mpsim", "quantum_pauli", "quantum_spd", "quantum_spd_receipt", "quantum_bptn", "quantum_resource", "quantum_resource_receipt", "quantum_frame", "quantum_tn", "quantum_gbs", "quantum_sv", "quantum_arch", "quantum_ldpc", "quantum_qio", "quantum_uf", "quantum_noisy", "quantum_dmrg", "quantum_tdvp", "quantum_chem", "quantum_nqs", "quantum_qir", "quantum_info", "quantum_qudit", "quantum_comm", "quantum_source", "quantum_channel",
"quantum", "quantum_toolchain", "quantum_mps", "quantum_stabilizer", "quantum_tableau", "quantum_cutrank", "quantum_qasm", "quantum_mpsim", "quantum_pauli", "quantum_spd", "quantum_spd_receipt", "quantum_bptn", "quantum_resource", "quantum_resource_receipt", "quantum_frame", "quantum_tn", "quantum_gbs", "quantum_sv", "quantum_arch", "quantum_ldpc", "quantum_qio", "quantum_uf", "quantum_noisy", "quantum_dmrg", "quantum_tdvp", "quantum_chem", "quantum_nqs", "quantum_qir", "quantum_info", "quantum_qudit", "quantum_comm", "quantum_source", "quantum_channel",
"quantum_receipt", "quantum_ops", "quantum_cal", "quantum_mitigate", "quantum_qec",
"quantum_control", "quantum_noise", "quantum_compile", "quantum_atom", "quantum_qbom", "quantum_fabric",
"quantum_vml", "quantum_phasor", "quantum_qfhrr", "quantum_kernel", "quantum_qdata",

View file

@ -67,6 +67,7 @@ never creates information.
| `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); **device-ready OpenQASM 2/3** (`device::device_qasm`): every gate lowered exactly to its dialect's standard library, multi-controlled gates included, every qubit measured into `c[i]`, and the service's counts read back onto the crate's basis; **native programs** (`native::compile`): a processor's own basis (`rz`/`sx`/`x`/`cz`; `prx`/`cz`; or `gpi`/`gpi2`/`zz` with angles in turns, written exactly; the Z rotations carried in the phases of the last two) on its own qubits, routed over its coupling map, and checked against the circuit by simulation before it is sent — Bloch / entanglement / purity analysis |
| `quantum_qasm` | **the OpenQASM reader the job service runs**, for every surface that takes a program: OpenQASM 3 and the OpenQASM 2 forms in common use, every register, a gate on a whole register, gate definitions with parameters, `ctrl`/`negctrl` modifiers, angle expressions, comments, and measurement in every spelling. A program that measures and then acts, resets after a gate or uses `if` is kept as dynamic steps; loops, `inv`, `pow` and anything else not listed are refused with their line. Lowered to each method's input exactly up to a global phase. `qasm::dyadic_circuit` gives the exact state vector's circuit (dyadic angles) for a program whose measurements all come at the end, and refuses any other with the reason |
| `quantum_stabilizer` | stabilizer (CHP) tableau — `O(n²)`, scales far past statevector |
| `quantum_mps` | tensor-network (matrix-product-state) backend |
| `quantum_pauli` | sparse-Pauli / Heisenberg observable propagation over the dyadic gate set (up to 64 qubits) |

View file

@ -47,6 +47,11 @@ mod linalg;
#[cfg(any(feature = "quantum_tn", feature = "quantum_sv"))]
pub mod gates;
// OpenQASM 3 (and the OpenQASM 2 forms in common use): one front end for every
// surface, lowered to each method's input.
#[cfg(feature = "quantum_qasm")]
pub mod qasm;
#[cfg(any(
feature = "quantum_spd",
feature = "quantum_pauli",

View file

@ -16,7 +16,7 @@
//! OpenQASM 2's `if(c==n) gate;`. A program that measures a qubit and then
//! acts on it, resets one after its first gate, or uses `if` is dynamic: it
//! is kept as steps ([`Instr`]) as well as gates, and runs by
//! [`crate::dynamic`]. Every other program is exactly what it was before.
//! the service's `dynamic` module. Every other program is exactly what it was before.
//!
//! Control modifiers, `ctrl @`, `ctrl(n) @`, `negctrl @` and `negctrl(n) @`,
//! on `x y z h s sdg t tdg p` and on the controlled names `cx cy cz ch cp ccx
@ -35,10 +35,10 @@
//! lowering to the others.
use std::collections::HashMap;
use wai_quantum::gates::{C, Gate as Matrix, complex};
use wai_quantum::quantum::{BaseGate, Circuit as Dyadic, Gate as DyadicGate};
use wai_quantum::quantum_spd::{Op as Rot, Pauli, RotCircuit};
use wai_quantum::quantum_tableau::Clifford;
use crate::gates::{C, Gate as Matrix, complex};
use crate::quantum::{BaseGate, Circuit as Dyadic, Gate as DyadicGate};
use crate::quantum_spd::{Op as Rot, Pauli, RotCircuit};
use crate::quantum_tableau::Clifford;
/// The most qubits a program declares.
pub const MAX_QUBITS: u32 = 65_536;
@ -1119,6 +1119,20 @@ impl Parser {
}
/// Parses an OpenQASM program.
/// A program whose measurements all come at the end, as the dyadic circuit:
/// the reader of the bit-exact state vector, the in-browser IDE and the CLI.
/// Every register, broadcast, gate definition and modifier [`parse`] reads is
/// read the same way here. A program that measures a qubit and then acts on
/// it, resets a qubit after its first gate, or acts on measured bits is
/// refused, never rewritten: its meaning needs the measurements.
pub fn dyadic_circuit(src: &str) -> Result<Dyadic, String> {
let p = parse(src)?;
if p.dynamic.is_some() {
return Err("this program measures a qubit and then acts on it, resets a qubit after its first gate, or acts on measured bits; the exact state vector here takes programs whose measurements all come at the end".into());
}
p.dyadic()
}
pub fn parse(src: &str) -> Result<Program, String> {
let toks = lex(src)?;
let mut p = Parser {
@ -1292,7 +1306,7 @@ fn lowering_phase(op: &Op, gates: &[Clifford]) -> C {
let want = op.matrix().matrix;
let mut best = (0.0f64, complex(1.0, 0.0));
for col in 0..d {
let mut sv = wai_quantum::quantum_sv::StateVector::zero(k).expect("at most three qubits");
let mut sv = crate::quantum_sv::StateVector::zero(k).expect("at most three qubits");
sv.re[0] = 0.0;
sv.re[col] = 1.0;
for g in &local_gates {
@ -2119,8 +2133,8 @@ mod tests {
Program { n_qubits: n, ops, measured: (0..n).map(|q| (q, q)).collect(), n_bits: n, dynamic: None }
}
fn dense(p: &Program) -> wai_quantum::quantum_sv::StateVector {
let mut sv = wai_quantum::quantum_sv::StateVector::zero(p.n_qubits).unwrap();
fn dense(p: &Program) -> crate::quantum_sv::StateVector {
let mut sv = crate::quantum_sv::StateVector::zero(p.n_qubits).unwrap();
sv.run(&p.matrices(), 1).unwrap();
sv
}
@ -2276,7 +2290,7 @@ mod tests {
#[test]
fn the_clifford_lowering_gives_the_matrices_expectation_values() {
use wai_quantum::quantum_tableau::Tableau;
use crate::quantum_tableau::Tableau;
for seed in 0..12 {
let p = clifford_kinds(5, seed);
let sv = dense(&p);
@ -2304,7 +2318,7 @@ mod tests {
for seed in 0..6 {
let p = every_kind(4, seed + 300);
let want = dense(&p);
let mut got = wai_quantum::quantum_sv::StateVector::zero(p.n_qubits).unwrap();
let mut got = crate::quantum_sv::StateVector::zero(p.n_qubits).unwrap();
let local = p.local_matrices();
assert!(local.iter().all(|g| g.qubits.len() <= 2));
got.run(&local, 1).unwrap();
@ -2319,7 +2333,7 @@ mod tests {
#[test]
fn the_rotation_lowering_gives_the_matrices_expectation_values() {
use wai_quantum::quantum_spd::{SpdConfig, expectation};
use crate::quantum_spd::{SpdConfig, expectation};
for seed in 0..6 {
let p = every_kind(4, seed + 100);
let sv = dense(&p);

View file

@ -674,7 +674,15 @@ pub fn to_qasm(c: &Circuit) -> String {
/// if its gateset is then changed.
pub fn from_qasm(src: &str) -> Result<Circuit, String> {
let mut n: Option<u8> = None;
let mut registers = 0;
// first pass: qubit count
// Registers counted statement by statement, however lines are broken.
for stmt in src.lines().map(|l| l.split("//").next().unwrap_or("")).flat_map(|l| l.split(';')) {
let stmt = stmt.trim();
if stmt.starts_with("qubit") || stmt.starts_with("qreg ") {
registers += 1;
}
}
for raw in src.lines() {
let line = strip(raw);
if let Some(rest) = line.strip_prefix("qubit[")
@ -688,20 +696,42 @@ pub fn from_qasm(src: &str) -> Result<Circuit, String> {
}
}
let n = n.ok_or("no qubit register declaration (qubit[n] q; or qreg q[n];)")?;
// What this reader does not take is refused, never dropped: the program
// would mean something else. `wai_quantum::qasm` reads all of it.
let fuller = "; the full reader (wai_quantum::qasm, the IDE's and the service's) takes it";
if registers > 1 {
return Err(format!("{registers} qubit registers; this reader takes one{fuller}"));
}
let mut c = Circuit::new(n);
let mut measured = false;
for raw in src.lines() {
let line = strip(raw);
if line.is_empty() {
continue;
}
if line.contains(';') && !line.starts_with("OPENQASM") && !line.starts_with("include") {
return Err(format!("several statements on one line (`{line}`); this reader takes one a line{fuller}"));
}
let first: String = line.chars().take_while(|c| c.is_ascii_alphanumeric() || *c == '_').collect();
match first.as_str() {
"reset" => return Err(format!("`reset` changes the state; this reader does not take it{fuller}")),
"gate" => return Err(format!("gate definitions are not read here{fuller}")),
"if" | "while" | "for" => return Err(format!("`{first}` acts on measured bits; this reader does not take it{fuller}")),
_ => {}
}
// Measurement is recorded, not simulated here, and the exporters disagree
// about how to spell it: OpenQASM 2 writes `measure q[0] -> c[0];` while
// Qiskit's OpenQASM 3 exporter writes `c[0] = measure q[0];`, whose first
// token is the *classical* register. Skip either shape before parsing a
// gate name out of the line.
if line.contains("measure") {
measured = true;
continue;
}
let gate_line = !matches!(first.as_str(), "OPENQASM" | "include" | "qubit" | "qreg" | "creg" | "bit" | "barrier");
if measured && gate_line {
return Err(format!("a gate after a measurement (`{line}`): this reader takes measurements only at the end{fuller}"));
}
// Gate modifiers: `ctrl @`, `ctrl(n) @`, `negctrl @`, `negctrl(n) @`.
if let Some((polarity, call)) = modifiers(&line)? {
push_modified(&mut c, &polarity, &call)?;
@ -1322,6 +1352,24 @@ mod tests {
assert_eq!(recommend_backend(&Circuit::qft(6)).0, Backend::Statevector);
}
/// What this reader cannot take is refused, never dropped.
#[test]
fn what_the_reader_cannot_take_is_refused() {
for src in [
"OPENQASM 3.0;\nqubit[1] a;\nqubit[1] b;\nx a[0];\nx b[0];\n",
"OPENQASM 3.0;\nqubit[1] q;\nx q[0];\nreset q[0];\n",
"OPENQASM 3.0;\nqubit[1] q;\nbit[1] c;\nh q[0];\nc[0] = measure q[0];\nh q[0];\n",
"OPENQASM 3.0;\ngate flip a { x a; }\nqubit[1] q;\nflip q[0];\n",
"OPENQASM 3.0;\nqubit[2] q;\nh q[0]; cx q[0], q[1];\n",
] {
let e = from_qasm(src).unwrap_err();
assert!(e.contains("wai_quantum::qasm"), "{src}: {e}");
}
// Measurements at the end are still read (and dropped, as they change nothing).
let c = from_qasm("OPENQASM 3.0;\nqubit[1] q;\nbit[1] c;\nx q[0];\nc[0] = measure q[0];\n").unwrap();
assert_eq!(c.ops.len(), 1);
}
#[test]
fn qasm_round_trip() {
// Grover's search past three qubits uses a Z with three or more

3
wai/wai-rs/Cargo.lock generated
View file

@ -4280,6 +4280,7 @@ dependencies = [
"dav1d",
"ed25519-dalek",
"flac-bound",
"getrandom 0.2.17",
"image",
"jpeg-encoder",
"jpegxl-rs",
@ -4310,7 +4311,7 @@ version = "0.1.0"
[[package]]
name = "wai-quantum"
version = "0.6.9"
version = "0.6.10"
dependencies = [
"blake3",
"ed25519-dalek",

View file

@ -90,6 +90,8 @@ c2pa = { version = "0.90", optional = true, default-features = false, features =
# deterministic fixed-point simulation, replay-equivalence conformance.
blake3 = { version = "1.8", optional = true } # state hashes, node minting, op/seal commitments; `hazmat` (1.8) for the HTTP head proof
ed25519-dalek = { version = "2", optional = true } # op + seal signatures
# The CLI's own signing key, made from the operating system's random source.
getrandom = { version = "0.2", optional = true }
num-bigint = { version = "0.4", optional = true } # oer: exact decimal arithmetic in the composition checks
# The curve ed25519-dalek is built on, named directly only by `receipt_claims`: its
# conformance corpus builds the signatures jwp-receipts §3.1 step 3 must refuse (keys of
@ -177,6 +179,8 @@ semantic = ["codecs", "dep:ort", "dep:ndarray"]
# the dependency on its own, so the one verifier builds exactly when ed25519-dalek does
# and asks for no feature its caller does not need. Every signing feature enables it.
ed25519 = ["dep:ed25519-dalek"]
# The wai-quantum CLI's per-user signing key (src/bin/wai_quantum.rs).
cli_keys = ["dep:getrandom"]
# Interactive worlds (extensions/interactive-worlds): media that consumes
# input. Pure-Rust, no ML dep chain — the sim is closed-form fixed-point.
world = ["dep:blake3", "ed25519"]
@ -444,6 +448,8 @@ quantum_atom = ["quantum_ops", "wai-quantum/quantum_atom"]
# selection (Clifford → statevector → tensor-network cascade) + OpenQASM interop,
# all over the byte-exact fixed-point simulator. Framework-neutral, no cloud.
quantum_toolchain = ["quantum", "wai-quantum/quantum_toolchain"]
# OpenQASM 3 read one way on every surface (wai_quantum::qasm).
quantum_qasm = ["quantum", "wai-quantum/quantum_qasm"]
# Matrix Product State backend (extensions/quantum-sim § MPS, `wai.quantum.mps`):
# the tensor-network "classical referee" — evolve a bounded-bond MPS gate-by-gate
# with a deterministic complex Jacobi SVD truncation, reproducible on every machine
@ -617,6 +623,8 @@ name = "wai-quantum"
path = "src/bin/wai_quantum.rs"
required-features = [
"quantum_toolchain",
"quantum_qasm",
"cli_keys",
"quantum_receipt",
"quantum_qir",
"quantum_channel",

View file

@ -81,6 +81,7 @@ use wai::quantum_info::{chsh, DensityMatrix};
use wai::quantum_qudit::QuditState;
use wai::quantum_energy::{EnergyClass, MaybeLabelled};
use wai::quantum_receipt::QuantumReceipt;
use wai::signer_id::did_key;
use wai::quantum_resource::{self as qr, EstimateConfig, LogicalCounts, PhysicalQubit, QecModel};
use wai::quantum_resource_receipt::{Program, ResourceJob, ResourceReceipt};
use wai::quantum_spd_receipt::{SpdJob, SpdReceipt};
@ -149,7 +150,7 @@ fn usage() -> ! {
wai-quantum run --qir [--top K] [--json] [--gateset v1|v2] (QIR on stdin)\n \
wai-quantum qasm <algorithm> [-n QUBITS] [-p PARAM] [--gateset v1|v2]\n \
wai-quantum qir <algorithm> [-n QUBITS] [-p PARAM] [--no-measure] [--gateset v1|v2]\n \
wai-quantum receipt <algorithm> [-n QUBITS] [-p PARAM] [--shots N] [--seed S] [--gateset v1|v2]\n \
wai-quantum receipt <algorithm> [-n QUBITS] [-p PARAM] [--shots N] [--seed S] [--gateset v1|v2] [--key FILE | --demo-key]\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 \
@ -223,6 +224,61 @@ fn read_stdin() -> String {
}
/// Pull `--flag VALUE` out of the argument list.
/// The key a receipt is signed with:
/// - `--demo-key`: the published demo key, for receipts that repeat byte for
/// byte everywhere. Anyone can sign with it, so it proves nothing about who
/// signed, and a note on stderr says so;
/// - `--key FILE`, or `$WAI_QUANTUM_KEY` naming a file: that key;
/// - otherwise this user's key, `$XDG_CONFIG_HOME/wai/quantum-key` (or
/// `~/.config/wai/quantum-key`), made from the operating system's random
/// source on first use and readable by its owner only.
///
/// A key file holds the 32-byte Ed25519 seed as 64 hex digits.
fn signing_key(args: &[String]) -> SigningKey {
if flag(args, "--demo-key") {
eprintln!("note: signed with the published demo key; the receipt shows the result is unaltered, not who signed it");
return SigningKey::from_bytes(&[42u8; 32]);
}
let path = opt(args, "--key", "").or_else(|| std::env::var("WAI_QUANTUM_KEY").ok()).map(std::path::PathBuf::from).unwrap_or_else(|| {
let base = std::env::var_os("XDG_CONFIG_HOME").map(std::path::PathBuf::from).or_else(|| std::env::var_os("HOME").map(|h| std::path::Path::new(&h).join(".config"))).unwrap_or_else(|| die("no HOME to keep a key in: pass --key FILE"));
base.join("wai").join("quantum-key")
});
let parse = |text: &str| -> Option<[u8; 32]> {
let t = text.trim();
if t.len() != 64 {
return None;
}
let mut out = [0u8; 32];
for (i, b) in out.iter_mut().enumerate() {
*b = u8::from_str_radix(&t[2 * i..2 * i + 2], 16).ok()?;
}
Some(out)
};
match std::fs::read_to_string(&path) {
Ok(text) => SigningKey::from_bytes(&parse(&text).unwrap_or_else(|| die(&format!("{}: a key file holds 64 hex digits", path.display())))),
Err(e) if e.kind() == std::io::ErrorKind::NotFound && opt(args, "--key", "").is_none() && std::env::var_os("WAI_QUANTUM_KEY").is_none() => {
let mut seed = [0u8; 32];
getrandom::getrandom(&mut seed).unwrap_or_else(|e| die(&format!("no random source for a new key: {e}")));
if let Some(dir) = path.parent() {
std::fs::create_dir_all(dir).unwrap_or_else(|e| die(&format!("{}: {e}", dir.display())));
}
let text: String = seed.iter().map(|b| format!("{b:02x}")).collect();
let mut f = std::fs::OpenOptions::new();
f.write(true).create_new(true);
#[cfg(unix)]
{
use std::os::unix::fs::OpenOptionsExt;
f.mode(0o600);
}
use std::io::Write as _;
f.open(&path).and_then(|mut file| writeln!(file, "{text}")).unwrap_or_else(|e| die(&format!("{}: {e}", path.display())));
eprintln!("note: made a signing key at {}", path.display());
SigningKey::from_bytes(&seed)
}
Err(e) => die(&format!("{}: {e}", path.display())),
}
}
fn opt(args: &[String], long: &str, short: &str) -> Option<String> {
args.iter()
.position(|a| a == long || (!short.is_empty() && a == short))
@ -259,7 +315,7 @@ fn gateset_from(args: &[String]) -> Gateset {
fn circuit_from(args: &[String], algo: Option<&String>) -> Circuit {
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}")))
wai::qasm::dyadic_circuit(&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 {
@ -509,10 +565,8 @@ fn main() {
let shots: u64 = num(args, "--shots", "", 0);
let seed: u64 = num(args, "--seed", "", 0);
let measure = (shots > 0).then_some(Measure { seed, shots });
// Deterministic demo key: the receipt is reproducible byte-for-byte, so
// native and cloud-WASM runs sign identically. Supply real keys in prod.
let key = SigningKey::from_bytes(&[42u8; 32]);
let r = QuantumReceipt::seal(&key, "did:key:wai-quantum-cli", &c, measure, 0, None)
let key = signing_key(args);
let r = QuantumReceipt::seal(&key, did_key(key.verifying_key().as_bytes()), &c, measure, 0, None)
.unwrap_or_else(|e| die(&format!("seal: {e:?}")));
println!("{}", r.to_json());
}
@ -633,10 +687,8 @@ fn main() {
config: SpdConfig { threshold, max_weight: None },
};
if flag(args, "--receipt") {
// The same deterministic demo key as `receipt`: the run is
// bit-identical everywhere, so is its receipt.
let key = SigningKey::from_bytes(&[42u8; 32]);
let r = SpdReceipt::seal(&key, "did:key:wai-quantum-cli", &job, None, |run| {
let key = signing_key(args);
let r = SpdReceipt::seal(&key, did_key(key.verifying_key().as_bytes()), &job, None, |run| {
run();
0
})
@ -698,10 +750,8 @@ fn main() {
die("--receipt seals one estimate: pick a --qubit");
}
let job = ResourceJob { program: Program::Logical(counts), qubit: qubits[0].clone(), code, config: cfg };
// The same deterministic demo key as `receipt`: the estimate is
// bit-identical everywhere, so is its receipt.
let key = SigningKey::from_bytes(&[42u8; 32]);
let r = ResourceReceipt::seal(&key, "did:key:wai-quantum-cli", &job, None, |run| {
let key = signing_key(args);
let r = ResourceReceipt::seal(&key, did_key(key.verifying_key().as_bytes()), &job, None, |run| {
run();
0
})

View file

@ -775,6 +775,8 @@ pub use wai_quantum::quantum_atom;
// byte-exact fixed-point simulator. Framework-neutral and offline.
#[cfg(feature = "quantum_toolchain")]
pub use wai_quantum::quantum_toolchain;
#[cfg(feature = "quantum_qasm")]
pub use wai_quantum::qasm;
// Matrix Product State backend (`wai.quantum.mps`) — the tensor-network classical
// referee: a bounded-bond MPS evolved with a deterministic complex Jacobi SVD

View file

@ -1241,7 +1241,7 @@ version = "0.1.0"
[[package]]
name = "wai-quantum"
version = "0.6.9"
version = "0.6.10"
dependencies = [
"blake3",
"ed25519-dalek",

View file

@ -19,7 +19,7 @@ js-sys = "0.3"
# with the codec chain off. No second implementation: replay-equivalence
# in the browser is the identical Rust that generated the conformance
# corpus hashes.
wai = { path = "../wai-rs", default-features = false, features = ["world", "audio_scene", "haptic", "splat4d", "provenance", "score", "film", "neural_int", "generate", "video_int", "audio_int", "world_model", "world_int", "splat_int", "quantum", "quantum_ops", "quantum_cal", "quantum_mitigate", "quantum_qec", "quantum_control", "quantum_noise", "quantum_compile", "quantum_atom", "quantum_qbom", "quantum_receipt", "quantum_toolchain", "quantum_mps", "quantum_stabilizer", "quantum_pauli", "quantum_vml", "quantum_phasor", "quantum_qfhrr", "quantum_kernel", "quantum_phasor_meter", "quantum_qdata", "video_workflow", "staged", "stage_measure", "receipt_claims"] }
wai = { path = "../wai-rs", default-features = false, features = ["world", "audio_scene", "haptic", "splat4d", "provenance", "score", "film", "neural_int", "generate", "video_int", "audio_int", "world_model", "world_int", "splat_int", "quantum", "quantum_ops", "quantum_cal", "quantum_mitigate", "quantum_qec", "quantum_control", "quantum_noise", "quantum_compile", "quantum_atom", "quantum_qbom", "quantum_receipt", "quantum_toolchain", "quantum_qasm", "quantum_mps", "quantum_stabilizer", "quantum_pauli", "quantum_vml", "quantum_phasor", "quantum_qfhrr", "quantum_kernel", "quantum_phasor_meter", "quantum_qdata", "video_workflow", "staged", "stage_measure", "receipt_claims"] }
ed25519-dalek = "2" # actor keys for browser-side op authoring (same version as wai-rs)
blake3 = "1" # in-page receipt check: hash a decoded image against a pinned digest
# Awaits the WebGPU member's futures behind the promises of src/quantum_webgpu.rs;

View file

@ -14,15 +14,24 @@ use wai::quantum::{circuit_hash, BaseGate, Circuit, Gate, Gateset, Measure, Stat
use wai::quantum_mps;
use wai::quantum_pauli;
use wai::quantum_receipt::QuantumReceipt;
use wai::signer_id::did_key;
use wai::quantum_stabilizer;
use wai::quantum_toolchain as tc;
use wasm_bindgen::prelude::*;
const SCALE: f64 = (1u64 << FRAC) as f64;
fn signer() -> SigningKey {
/// The key a receipt is signed with before the page gives the IDE its own
/// ([`QuantumIde::set_signing_key`]). It is public, so a receipt it signs
/// proves nothing about who made it; [`QuantumIde::key_is_demo`] says so.
fn demo_signer() -> SigningKey {
SigningKey::from_bytes(&[0x51; 32])
}
/// `{"error": message}`, the message escaped as JSON requires.
fn error_json(message: impl std::fmt::Display) -> String {
serde_json::json!({ "error": message.to_string() }).to_string()
}
fn hex(b: &[u8]) -> String {
b.iter().map(|x| format!("{x:02x}")).collect()
}
@ -98,6 +107,8 @@ pub struct QuantumIde {
sv: Option<StateVector>,
/// Whether the console note has been printed for the circuit as it stands.
noted: bool,
/// The key receipts are signed with: this browser's, once the page sets it.
key: Option<SigningKey>,
}
#[wasm_bindgen]
@ -105,7 +116,30 @@ 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::with_gateset(n_qubits.clamp(1, 12), Gateset::V2), sv: None, noted: false }
QuantumIde { circuit: Circuit::with_gateset(n_qubits.clamp(1, 12), Gateset::V2), sv: None, noted: false, key: None }
}
// ---- signing key ----
/// Sign receipts with the Ed25519 key whose 32-byte seed is `seed`: the
/// page makes one at random and keeps it in this browser. Refuses any
/// other length.
pub fn set_signing_key(&mut self, seed: &[u8]) -> Result<(), String> {
let bytes: [u8; 32] = seed.try_into().map_err(|_| format!("a signing key's seed is 32 bytes, not {}", seed.len()))?;
self.key = Some(SigningKey::from_bytes(&bytes));
Ok(())
}
/// The public key receipts are signed with, as hex.
pub fn public_key_hex(&self) -> String {
hex(self.signer().verifying_key().as_bytes())
}
/// The signer a receipt names: that public key as a `did:key`.
pub fn signer_did(&self) -> String {
did_key(self.signer().verifying_key().as_bytes())
}
/// Whether receipts are still signed with the published demo key, which
/// proves nothing about who made them.
pub fn key_is_demo(&self) -> bool {
self.key.is_none()
}
// ---- gateset ----
@ -180,10 +214,20 @@ impl QuantumIde {
}
}
/// 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).
/// The program is read as the job service reads it ([`wai::qasm`]): every
/// register, broadcast, gate definition and modifier. Returns an error
/// string on failure (an unsupported construct with its line, a
/// non-dyadic angle, more qubits than the page holds). A program that
/// measures in the middle, resets after a gate or acts on measured bits is
/// refused, never rewritten, with a pointer to the console that runs it.
pub fn load_qasm(&mut self, src: &str) -> Result<(), String> {
let c = tc::from_qasm(src)?;
let c = wai::qasm::dyadic_circuit(src).map_err(|e| {
if e.starts_with("this program measures") {
format!("{e}. The console at /quantum-run/ runs it.")
} else {
e
}
})?;
if c.n_qubits > 12 {
return Err(format!("{} qubits exceeds the in-browser limit (12)", c.n_qubits));
}
@ -192,22 +236,13 @@ impl QuantumIde {
}
/// The op-log as JSON, for the editor to render.
pub fn gates_json(&self) -> String {
let items: Vec<String> = self
let items: Vec<serde_json::Value> = self
.circuit
.ops
.iter()
.map(|g| {
let ctrls: Vec<String> = g.controls.iter().map(|c| c.to_string()).collect();
format!(
"{{\"name\":\"{}\",\"controls\":[{}],\"target\":{},\"param\":{}}}",
name_of(g.base),
ctrls.join(","),
g.target,
g.param
)
})
.map(|g| serde_json::json!({ "name": name_of(g.base), "controls": g.controls, "target": g.target, "param": g.param }))
.collect();
format!("[{}]", items.join(","))
serde_json::Value::Array(items).to_string()
}
// ---- run ----
@ -314,13 +349,7 @@ impl QuantumIde {
pub fn transpile_line(&self) -> String {
let r = tc::route_line(&self.circuit);
let eq = tc::routing_equivalent(&self.circuit, &r);
format!(
"{{\"source_2q\":{},\"nonlocal\":{},\"routed_gates\":{},\"swaps\":{},\"equivalent\":{eq}}}",
r.source_2q,
r.source_nonlocal,
r.circuit.ops.len(),
r.swaps
)
serde_json::json!({ "source_2q": r.source_2q, "nonlocal": r.source_nonlocal, "routed_gates": r.circuit.ops.len(), "swaps": r.swaps, "equivalent": eq }).to_string()
}
/// Run the circuit through the tensor-network (MPS) backend at bond cap
/// `chi_max` and report `{bond, retained, fidelity}` against the exact dense
@ -328,17 +357,14 @@ impl QuantumIde {
pub fn run_mps(&self, chi_max: u32) -> String {
let dense = match self.circuit.simulate() {
Ok(d) => d,
Err(e) => return format!("{{\"error\":\"{e}\"}}"),
Err(e) => return error_json(e),
};
match quantum_mps::run(&self.circuit, chi_max.max(1) as usize) {
Ok((mps, r)) => {
let f = quantum_mps::fidelity(&mps, &dense);
format!(
"{{\"bond\":{},\"retained\":{:.6},\"fidelity\":{:.6}}}",
r.max_bond, r.retained_weight, f
)
serde_json::json!({ "bond": r.max_bond, "retained": r.retained_weight, "fidelity": f }).to_string()
}
Err(e) => format!("{{\"error\":\"{e:?}\"}}"),
Err(e) => error_json(format!("{e:?}")),
}
}
/// Sample the current circuit on the O(n²) stabilizer backend (if Clifford):
@ -347,9 +373,9 @@ impl QuantumIde {
match quantum_stabilizer::sample(&self.circuit, seed as u64, shots.max(1) as u64) {
Ok(v) => {
let (o, c) = v.first().copied().unwrap_or((0, 0));
format!("{{\"distinct\":{},\"top_index\":{o},\"top_count\":{c}}}", v.len())
serde_json::json!({ "distinct": v.len(), "top_index": o, "top_count": c }).to_string()
}
Err(e) => format!("{{\"error\":\"{}\"}}", e.0),
Err(e) => error_json(e.0),
}
}
/// Scale demonstration: build an `n`-qubit GHZ and sample it on the stabilizer
@ -364,16 +390,16 @@ impl QuantumIde {
c.cx(i - 1, i);
}
match quantum_stabilizer::sample(&c, seed as u64, shots.max(1) as u64) {
Ok(v) => format!("{{\"n\":{n},\"distinct\":{},\"gates\":{}}}", v.len(), c.ops.len()),
Err(e) => format!("{{\"error\":\"{}\"}}", e.0),
Ok(v) => serde_json::json!({ "n": n, "distinct": v.len(), "gates": c.ops.len() }).to_string(),
Err(e) => error_json(e.0),
}
}
/// Compute ⟨Z_q⟩ on the current circuit via sparse-Pauli / Heisenberg
/// propagation: `{value, peak}` (peak Pauli-sum size) or `{error}`.
pub fn expect_z_pauli(&self, q: u8, threshold: f64) -> String {
match quantum_pauli::expect_z(&self.circuit, q, threshold.max(0.0)) {
Ok((v, peak)) => format!("{{\"value\":{v:.6},\"peak\":{peak}}}"),
Err(e) => format!("{{\"error\":\"{}\"}}", e.0),
Ok((v, peak)) => serde_json::json!({ "value": v, "peak": peak }).to_string(),
Err(e) => error_json(e.0),
}
}
/// Scale demonstration: ⟨Z_0⟩ on an `n`-qubit low-magic circuit — a single
@ -401,8 +427,8 @@ impl QuantumIde {
}
}
match quantum_pauli::expect_z(&c, 0, threshold.max(0.0)) {
Ok((v, peak)) => format!("{{\"n\":{n},\"value\":{v:.6},\"peak\":{peak}}}"),
Err(e) => format!("{{\"error\":\"{}\"}}", e.0),
Ok((v, peak)) => serde_json::json!({ "n": n, "value": v, "peak": peak }).to_string(),
Err(e) => error_json(e.0),
}
}
/// Seal a signed run receipt over the byte-exact reconstruction (and a shot
@ -445,10 +471,14 @@ impl QuantumIde {
}
}
fn signer(&self) -> SigningKey {
self.key.clone().unwrap_or_else(demo_signer)
}
fn seal(&self, seed: u64, shots: u64) -> Option<QuantumReceipt> {
QuantumReceipt::seal(
&signer(),
"did:key:quantum-ide",
&self.signer(),
did_key(self.signer().verifying_key().as_bytes()),
&self.circuit,
Some(Measure { seed, shots }),
0,
@ -461,11 +491,8 @@ impl QuantumIde {
/// The algorithm library catalog as JSON `[{id,name,desc}]`, for the IDE dropdown.
#[wasm_bindgen]
pub fn ide_catalog() -> String {
let items: Vec<String> = tc::catalog()
.into_iter()
.map(|(id, name, desc)| format!("{{\"id\":\"{id}\",\"name\":\"{name}\",\"desc\":\"{desc}\"}}"))
.collect();
format!("[{}]", items.join(","))
let items: Vec<serde_json::Value> = tc::catalog().into_iter().map(|(id, name, desc)| serde_json::json!({ "id": id, "name": name, "desc": desc })).collect();
serde_json::Value::Array(items).to_string()
}
#[cfg(test)]
@ -600,4 +627,82 @@ mod tests {
assert!(ide.set_gateset(2));
assert_eq!(ide.note(), "");
}
fn loaded(src: &str) -> QuantumIde {
let mut ide = QuantumIde::new(2);
ide.load_qasm(src).unwrap_or_else(|e| panic!("{src}: {e}"));
ide.run().unwrap();
ide
}
/// The outcome with probability 1, or None.
fn certain(ide: &QuantumIde) -> Option<usize> {
let p = ide.probabilities();
p.iter().position(|&x| (x - 1.0).abs() < 1e-9)
}
/// OpenQASM means what it says: registers keep their own qubits, broadcast,
/// comments, single-qubit declarations and gate definitions are read, and a
/// program whose meaning needs its measurements is refused, not rewritten.
#[test]
fn imported_programs_mean_what_they_say() {
// Two registers are two qubits: |11⟩.
let ide = loaded("OPENQASM 3.0; qubit[1] a; qubit[1] b; x a[0]; x b[0];");
assert_eq!(ide.circuit.n_qubits, 2);
assert_eq!(certain(&ide), Some(3));
// OpenQASM 2's registers too: three qubits, one of them flipped.
let ide = loaded("OPENQASM 2.0; include \"qelib1.inc\"; qreg a[2]; qreg b[1]; x b[0];");
assert_eq!(ide.circuit.n_qubits, 3);
assert_eq!(certain(&ide).map(|i| i.count_ones()), Some(1));
// Broadcast, a single-qubit declaration, a block comment, a definition.
let ide = loaded("OPENQASM 3.0; qubit[2] q; /* both */ h q;");
assert!(ide.probabilities().iter().all(|&p| (p - 0.25).abs() < 1e-9), "{:?}", ide.probabilities());
let ide = loaded("OPENQASM 3.0; qubit q; x q;");
assert_eq!((ide.circuit.n_qubits, certain(&ide)), (1, Some(1)));
let ide = loaded("OPENQASM 3.0; gate flip a { x a; } qubit[1] q; flip q[0];");
assert_eq!(certain(&ide), Some(1));
// A final measurement leaves the state as it was.
let ide = loaded("OPENQASM 3.0; qubit[1] q; bit[1] c; x q[0]; c[0] = measure q[0];");
assert_eq!(certain(&ide), Some(1));
// A measurement acted on afterwards, or a reset after a gate, is refused.
let mut ide = QuantumIde::new(2);
for src in ["OPENQASM 3.0; qubit[1] q; bit[1] c; h q[0]; c[0] = measure q[0]; h q[0];", "OPENQASM 3.0; qubit[1] q; x q[0]; reset q[0];"] {
let e = ide.load_qasm(src).unwrap_err();
assert!(e.contains("measures a qubit and then acts on it") && e.contains("/quantum-run/"), "{src}: {e}");
}
// A missing semicolon is an error with its line, as in the service.
assert!(ide.load_qasm("OPENQASM 3.0;\nqubit[2] q\nh q[0];").is_err());
}
/// Every JSON the panels read parses, whatever an error message holds.
#[test]
fn the_panels_get_json_that_parses() {
let mut ide = QuantumIde::new(8);
assert!(ide.load_algorithm("grover", 8, 0));
ide.run().unwrap();
for text in [ide.run_mps(8), ide.transpile_line(), ide.sample_stabilizer(1.0, 16), ide.stabilizer_scale(20, 1.0, 16), ide.expect_z_pauli(0, 1e-6), ide.pauli_scale(8, 1e-4), ide.gates_json(), super::ide_catalog()] {
serde_json::from_str::<serde_json::Value>(&text).unwrap_or_else(|e| panic!("{text}: {e}"));
}
assert_eq!(super::error_json("say \"no\" \\ here"), r#"{"error":"say \"no\" \\ here"}"#);
}
/// Receipts are signed with the key the page gives, and say when they are
/// not.
#[test]
fn receipts_use_the_browsers_key() {
let mut ide = QuantumIde::new(2);
assert!(ide.load_algorithm("bell", 2, 0));
ide.run().unwrap();
assert!(ide.key_is_demo());
let demo = ide.public_key_hex();
ide.set_signing_key(&[7u8; 32]).unwrap();
assert!(!ide.key_is_demo());
assert_ne!(ide.public_key_hex(), demo);
assert!(ide.receipt_verifies(1.0, 64));
let r: serde_json::Value = serde_json::from_str(&ide.receipt_json(1.0, 64)).unwrap();
assert_eq!(r["signer_pubkey"], ide.public_key_hex());
assert_eq!(r["signer_id"], ide.signer_did());
assert!(ide.signer_did().starts_with("did:key:z6Mk"));
assert!(ide.set_signing_key(&[1u8; 31]).is_err());
}
}