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
23 changed files with 354 additions and 87 deletions
8
.github/workflows/wai-web-wasm.yml
vendored
8
.github/workflows/wai-web-wasm.yml
vendored
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
2
wai/tools/quantum-meter/Cargo.lock
generated
2
wai/tools/quantum-meter/Cargo.lock
generated
|
|
@ -1017,7 +1017,7 @@ checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
|
|||
|
||||
[[package]]
|
||||
name = "wai-quantum"
|
||||
version = "0.6.9"
|
||||
version = "0.6.10"
|
||||
dependencies = [
|
||||
"blake3",
|
||||
"ed25519-dalek",
|
||||
|
|
|
|||
2
wai/tools/quantum-wgsl-check/Cargo.lock
generated
2
wai/tools/quantum-wgsl-check/Cargo.lock
generated
|
|
@ -599,7 +599,7 @@ checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
|
|||
|
||||
[[package]]
|
||||
name = "wai-quantum"
|
||||
version = "0.6.9"
|
||||
version = "0.6.10"
|
||||
dependencies = [
|
||||
"blake3",
|
||||
"ed25519-dalek",
|
||||
|
|
|
|||
2
wai/wai-pack/Cargo.lock
generated
2
wai/wai-pack/Cargo.lock
generated
|
|
@ -713,7 +713,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "wai-quantum"
|
||||
version = "0.6.9"
|
||||
version = "0.6.10"
|
||||
dependencies = [
|
||||
"blake3",
|
||||
"ed25519-dalek",
|
||||
|
|
|
|||
2
wai/wai-quantum-qpu/Cargo.lock
generated
2
wai/wai-quantum-qpu/Cargo.lock
generated
|
|
@ -422,7 +422,7 @@ checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
|
|||
|
||||
[[package]]
|
||||
name = "wai-quantum"
|
||||
version = "0.6.9"
|
||||
version = "0.6.10"
|
||||
dependencies = [
|
||||
"blake3",
|
||||
"ed25519-dalek",
|
||||
|
|
|
|||
2
wai/wai-quantum-serve/Cargo.lock
generated
2
wai/wai-quantum-serve/Cargo.lock
generated
|
|
@ -599,7 +599,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "wai-quantum"
|
||||
version = "0.6.9"
|
||||
version = "0.6.10"
|
||||
dependencies = [
|
||||
"blake3",
|
||||
"ed25519-dalek",
|
||||
|
|
|
|||
2
wai/wai-quantum-service/Cargo.lock
generated
2
wai/wai-quantum-service/Cargo.lock
generated
|
|
@ -857,7 +857,7 @@ checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
|
|||
|
||||
[[package]]
|
||||
name = "wai-quantum"
|
||||
version = "0.6.9"
|
||||
version = "0.6.10"
|
||||
dependencies = [
|
||||
"blake3",
|
||||
"ed25519-dalek",
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ mod energy;
|
|||
mod http;
|
||||
mod jobs;
|
||||
mod mcp;
|
||||
mod qasm;
|
||||
use wai_quantum::qasm;
|
||||
mod receipt;
|
||||
mod run;
|
||||
|
||||
|
|
|
|||
2
wai/wai-quantum/Cargo.lock
generated
2
wai/wai-quantum/Cargo.lock
generated
|
|
@ -1079,7 +1079,7 @@ checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
|
|||
|
||||
[[package]]
|
||||
name = "wai-quantum"
|
||||
version = "0.6.9"
|
||||
version = "0.6.10"
|
||||
dependencies = [
|
||||
"blake3",
|
||||
"block2",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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) |
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
@ -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
3
wai/wai-rs/Cargo.lock
generated
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
2
wai/wai-web/Cargo.lock
generated
2
wai/wai-web/Cargo.lock
generated
|
|
@ -1241,7 +1241,7 @@ version = "0.1.0"
|
|||
|
||||
[[package]]
|
||||
name = "wai-quantum"
|
||||
version = "0.6.9"
|
||||
version = "0.6.10"
|
||||
dependencies = [
|
||||
"blake3",
|
||||
"ed25519-dalek",
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue