| Filename | Latest commit message | Latest commit date |
|---|---|---|
|
Some checks failed
WAI energy receipts / does this change touch it (pull_request) Successful in 4s
standards workspaces build + test / which workspaces this change touches (pull_request) Successful in 6s
standards workspaces build + test / macOS energy backends (pull_request) Has been skipped
wai-quantum service / does this change touch it (pull_request) Successful in 5s
repository checks / lockfiles, tests in CI, workflow hygiene, spec and section references, public text (pull_request) Successful in 38s
WAI energy receipts / energy receipts · macos (pull_request) Successful in 3m49s
wai-quantum service / service · macos (pull_request) Successful in 27s
WAI energy receipts / energy receipts · linux (pull_request) Successful in 8m19s
wai-quantum service / service · linux (pull_request) Successful in 39s
WAI energy receipts / energy receipts · linux-arm64 (pull_request) Failing after 5m10s
The ghz job's energy must be an OnChipCounter figure meeting the resolution floor, or unmetered with a reason (SMOKE_EXPECT_ENERGY=1 requires the figure); a changed figure must fail verification; and a GPU job's figure must be partial, naming the GPU it does not read. All held against the branch's release build on the build host. Co-Authored-By: Claude Opus 5.5 <[email protected]> |
||
| .. | ||
| src | ||
| testdata | ||
| Cargo.lock | ||
| Cargo.toml | ||
| README.md | ||
| smoke.sh | ||
wai-quantum-service
The WAI quantum stack as a public job service. Send an OpenQASM program; get back measurement counts, Pauli expectation values or amplitudes. Each result names the method that produced it and how accurate it is, and carries a signature you can check.
No account and no key. The public instance is at https://wai.transaction.science/quantum-jobs/v1.
# A Bell pair, 1000 shots.
curl -s https://wai.transaction.science/quantum-jobs/v1/run \
-H 'content-type: application/json' \
-d '{"qasm": "OPENQASM 3.0; qubit[2] q; bit[2] c; h q[0]; cx q[0], q[1]; c = measure q;", "shots": 1000}'
Jobs
A job is JSON:
| field | meaning |
|---|---|
qasm |
the program: OpenQASM 3, or OpenQASM 2 (required) |
method |
auto (the default) or a method below |
shots |
measurement shots (default 1024 when nothing else is asked for; at most 1,000,000) |
seed |
the sampling seed (default 1); the same job and seed give the same counts |
observables |
Pauli strings such as "Z0 Z1", or {"pauli": "X3 Y4", "coeff": 0.5} |
amplitudes |
bit strings, one character per qubit, qubit n − 1 first |
noise |
{"depolarizing_1q", "depolarizing_2q", "amplitude_damping", "dephasing", "readout"}, each a probability |
options |
per method: threshold, max_weight and ladder (pauli), max_bond and cutoff (mps), fusion (statevector), trajectories, memory_gb |
Counts are keyed by the program's classical bits, the highest bit first. With no measurement in the program, every qubit is measured into the bit of its own index.
The program may use the standard gates, user gate definitions, and the control modifiers ctrl @, ctrl(n) @, negctrl @ and negctrl(n) @ (on x y z h s sdg t tdg p and cx cy cz ch cp ccx ccz). Classical control, inv @, pow @ and gates after a qubit's measurement are refused, each with its line and what to write instead (src/qasm.rs lists everything read).
Methods
| method | takes | answers | accuracy |
|---|---|---|---|
exact |
angles that are dyadic multiples of 2π, up to 26 qubits | counts, expectations, amplitudes, a state hash | bit-exact |
statevector |
any circuit, up to 32 qubits | counts, expectations, amplitudes | exact |
statevector_single |
any circuit with gates on at most four qubits, up to 33 qubits, where the host has a GPU | counts, expectations, amplitudes | single |
stabilizer |
Clifford circuits (rotations by whole quarter turns), up to 32768 qubits, optionally with depolarizing, dephasing and readout noise | counts, expectations | exact; with noise, an estimate |
pauli |
any circuit, up to 1024 qubits | expectations | bounded |
mps |
any circuit, up to 4096 qubits, at bond dimension up to 256; best when entanglement stays low | counts, expectations, amplitudes | exact when nothing is truncated, otherwise heuristic |
tensor_network |
any circuit | amplitudes | exact |
density_matrix |
noisy circuits, up to 16 qubits within a job's memory | counts, expectations | exact |
trajectories |
noisy circuits, up to 32 qubits | counts, expectations | estimate |
How the methods run:
- statevector groups consecutive gates into passes over the state. Each pass applies its gates tile by tile, in a buffer that fits a core's cache. The grouping never changes a bit of the result.
- pauli reports each value with its truncation
bound. It holds at most as many Pauli terms as the job's memory allows; a sum that would outgrow it raises its threshold until it fits (threshold_reached), and the bound covers everything that dropped. Unlessladderisfalse, it also reports aladder: the value and bound at up to three thresholds a decade above the one asked for, so you can see the value settle as the threshold drops. - stabilizer, with noise, samples by Pauli frames against one noiseless outcome of the tableau, 64 shots to a machine word. Its counts have the law of independent noisy shots. Its expectation values are estimates with standard errors, from at least 10,000 shots unless
trajectoriessets the number. Amplitude damping is not a Pauli channel and is refused. - trajectories treats every shot as an independent noisy trajectory in law. Shots that took the same errors share one state, so the error-free part of a circuit, and the error-free majority of the shots, run once.
historiesin the result counts the distinct error histories that ran.
auto picks the first method that takes the job. With noise, it tries density_matrix, then stabilizer, then trajectories. For counts, it tries stabilizer, exact, statevector, statevector_single, then mps. For expectations alone, it tries stabilizer, exact, statevector, statevector_single, pauli, then mps. For amplitudes alone, it tries exact, statevector, statevector_single, tensor_network, then mps. The job's why names the method chosen and the ones that could not take it.
The accuracy classes:
- bit-exact: the same bits on every machine. Amplitudes are held in 30-bit fixed point, and every output is an exact function of them.
- exact: no approximation beyond double-precision rounding.
- single: no approximation beyond single-precision rounding, about 10⁻⁷ relative per gate; sums are taken in double precision.
- bounded: an approximation, with a rigorous bound on the error (
bound) beside each value. - estimate: a statistical estimate, with its standard error (
stderr). - heuristic: an approximation, reported with the standard estimate of its error but not a bound. A matrix product state that truncated reports its discarded weight and its fidelity estimate, and a
warningwhen the estimate is below 0.9.
Counts are always seeded samples.
Endpoints
GET /v1 |
the service: its methods, limits, load and receipt key |
POST /v1/run |
submit a job and wait up to 25 s: the finished job, or 202 and the job if it is still running |
POST /v1/jobs |
submit a job: 202 and the job, with its id |
GET /v1/jobs/{id} |
the job: its status, the request as submitted, and its result when it is done |
DELETE /v1/jobs/{id} |
cancel a queued or running job |
POST /v1/verify |
{"request": ..., "result": ...}: whether the result's receipt seals that request and that result |
Every refusal says why: 400 for a program or job that does not read, 422 for one larger than a job may be, 429 past the per-client limits, 503 when the queue is full. GET /v1 states the limits.
Receipts
Every finished result carries a receipt: an Ed25519 signature, by the key GET /v1 publishes, over the BLAKE3 digest of the request as submitted and the result as returned. POST /v1/verify checks one, and so can any Ed25519 implementation:
- digest = BLAKE3(
wai:quantum-service-receipt‖ 0x02 ‖ len(R) ‖ R ‖ len(S) ‖ S), each len a big-endian u64; - R and S are the RFC 8785 canonical texts (
"canon": "rfc8785") of the request and of the result without itsreceipt: keys sorted, no whitespace, every number written as ECMAScript writes the double it reads.
So a result verifies after any JSON reader has carried it, a browser's included. Whole numbers in a request must be within ±(2^53 − 1), so that every reader reads them alike. Receipts issued before the canonical form carry no canon and still verify (see src/receipt.rs).
Energy
Every finished result carries energy, inside the body the receipt seals: the CPU energy the job's worker process spent, from its start to its exit. The service reads it from the kernel's per-process count (proc_pid_rusage, ri_energy_nj, on macOS) after the worker exits and before it is reaped. An exited process has left every core, so its count is complete.
"energy": {
"joules_micro": 2540113,
"energy_provenance": "OnChipCounter",
"energy_uncertainty": { "relative_ppm": 1, "window_us": 2603412 },
"energy_counter_filtering": "Unknown",
"coverage": "whole",
"covers": "the CPU energy of the job's worker process, from its start to its exit, with the CPU energy system services billed to it",
"excludes": ["memory", "the host's idle draw"],
"cpu_ms": 31680,
"source": "proc_pid_rusage ri_energy_nj + ri_billed_energy − ri_serviced_energy, read after the worker exited and before it was reaped"
}
The fields follow WAI's energy-measurement extension (§2):
energy_provenanceis the acquisition class.OnChipCountermeans the silicon's own accounting, with no calibration behind it.energy_uncertainty.relative_ppmdeclares the part of the uncertainty that can be evaluated: here, the rounding to whole microjoules. It is not the figure's total error, because the counter's own systematic error is not evaluated.window_usis the worker's lifetime.energy_counter_filteringisUnknown, because the platform does not document whether the count is filtered.
A statevector_single job runs on the GPU, which this counter does not read, so its figure is partial and says so in excludes. So is a figure that includes CPU energy system services billed to the worker, since such credits land late.
Where the service cannot read the count (off macOS), energy is {"unmetered": "<why>"}. It is never a figure of zero. wai-quantum-service run FILE runs a job in its own process and reports no energy.
Running it
cargo build --release
WAI_QSVC_ADDR=127.0.0.1:8241 ./target/release/wai-quantum-service serve
bash smoke.sh http://127.0.0.1:8241
Each job runs in a child process of the same binary (worker), at a lower scheduling priority than the host's other work, and is watched for its resident memory and its time.
These variables set the limits:
| variable | default | meaning |
|---|---|---|
WAI_QSVC_BUDGET_GB |
96 | memory all running jobs share |
WAI_QSVC_JOB_GB |
72 | memory one job may take |
WAI_QSVC_WORKERS |
4 | jobs running at once |
WAI_QSVC_MIN_FREE_PCT |
15 | share of the host's memory a starting job must leave free |
WAI_QSVC_JOB_SECS |
900 | the longest a job may run |
WAI_QSVC_CLIENT_ACTIVE |
3 | queued or running jobs one client may hold |
WAI_QSVC_CLIENT_BURST |
30 | submissions one client may make per window |
WAI_QSVC_CLIENT_WINDOW_SECS |
600 | the window |
WAI_QSVC_MAX_QUEUE |
256 | jobs waiting at once |
WAI_QSVC_RETAIN_SECS |
86400 | how long results are kept |
WAI_QSVC_TRUSTED_PROXIES |
127.0.0.1,::1 |
peers whose X-Forwarded-For names the client |
WAI_QSVC_KEY |
~/.config/wai-quantum-service/signing.key |
the signing key, created on first start |
wai-quantum-service run job.json runs one job in the current process and prints its plan and its result.