wai-quantum: pulse programs simulated under a Lindblad master equation, the same bits everywhere, their receipts binding the calibrations #551

Merged
dcharlot merged 10 commits from wai/quantum-pulse into main 2026-10-31 07:15:50 -04:00
64 changed files with 15166 additions and 435 deletions

201
.github/workflows/wai-quantum-pulse.yml vendored Normal file
View file

@ -0,0 +1,201 @@
name: wai-quantum pulse and dynamics
# Pulse programs (wai/wai-quantum): OpenPulse read into a schedule on frames
# (qasm::pulse), the pulse simulator that integrates it under a Lindblad
# master equation (quantum_pulse), and pulse runs as jobs, their schedules
# and their checks (engine::pulse).
#
# The simulator is held to closed forms within the error it states (a Rabi
# turn, a quarter turn of phase, a detuned drive, T1 decay, T2 dephasing),
# and its result is the same bits on every platform: a test pins the final
# density matrix's digest, and this workflow runs it on x86_64 Linux, arm64
# Linux, arm64 macOS and wasm32-wasip2 (wasmtime). The browser build runs a
# pulse job to the same pinned digest (wai-web-wasm.yml, wai-web's wasm32
# tests).
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
on:
push:
branches: [main]
paths:
- 'wai/wai-quantum/**'
- 'rust-toolchain.toml'
- '.github/workflows/wai-quantum-pulse.yml'
pull_request:
paths:
- 'wai/wai-quantum/**'
- 'rust-toolchain.toml'
- '.github/workflows/wai-quantum-pulse.yml'
jobs:
relevant:
name: does this change touch it
runs-on: linux-arm64
outputs:
run: ${{ steps.gate.outputs.run }}
steps:
- uses: actions/checkout@v5
with:
fetch-depth: 0
- id: gate
run: |
set -euo pipefail
r=$(python3 scripts/pr-touches-paths.py .github/workflows/wai-quantum-pulse.yml \
"${{ github.event.pull_request.base.sha }}" \
"${{ github.event.pull_request.head.sha }}")
echo "this change touches the pulse layer: $r"
echo "run=$r" >> "$GITHUB_OUTPUT"
native:
name: reader, simulator and pulse jobs · linux, release
needs: relevant
if: needs.relevant.outputs.run == 'true'
runs-on: linux
timeout-minutes: 60
steps:
- uses: actions/checkout@v5
- name: Install Rust 1.98.0
shell: bash
run: |
export PATH="$HOME/.cargo/bin:$PATH"
command -v rustup >/dev/null 2>&1 || \
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --profile minimal --default-toolchain 1.98.0
rustup toolchain install 1.98.0 --profile minimal
rustup default 1.98.0
echo "$HOME/.cargo/bin" >> "$GITHUB_PATH"
# A module filter is satisfied by any one test of the module, so the
# count is pinned and each test is required by name to have run and passed.
- name: The pulse reader, the simulator and pulse jobs
shell: bash
working-directory: wai/wai-quantum
run: |
set -euo pipefail
cargo test --locked --release --features full --lib -- qasm::pulse:: quantum_pulse:: engine::pulse:: 2>&1 | tee "$RUNNER_TEMP/pulse.log"
grep -qE '^test result: ok\. 26 passed; 0 failed' "$RUNNER_TEMP/pulse.log" || { echo "::error::the pulse tests did not all pass (26 expected)"; exit 1; }
for t in qasm::pulse::tests::a_program_reads_to_its_schedule_on_frames \
qasm::pulse::tests::barriers_align_frames_and_a_gate_starts_its_frames_together \
qasm::pulse::tests::phase_and_frequency_follow_the_frame_and_the_carrier_runs_on \
qasm::pulse::tests::each_template_samples_as_its_formula_says \
qasm::pulse::tests::a_parametric_calibration_binds_its_angle_and_a_fixed_one_wins \
qasm::pulse::tests::durations_in_nanoseconds_must_be_whole_samples \
qasm::pulse::tests::ports_map_by_name_and_by_the_options \
qasm::pulse::tests::what_the_pulse_reader_does_not_take_is_refused_with_its_code_and_place \
qasm::pulse::tests::the_schedule_writes_as_json_with_every_instruction \
quantum_pulse::tests::a_resonant_drive_turns_the_qubit_as_rabi_says \
quantum_pulse::tests::a_quarter_turn_of_phase_turns_the_axis_to_minus_y \
quantum_pulse::tests::a_detuned_drive_turns_less_and_faster \
quantum_pulse::tests::relaxation_and_dephasing_decay_as_t1_and_t2_say \
quantum_pulse::tests::the_same_schedule_gives_the_same_bits \
quantum_pulse::tests::simultaneous_captures_give_the_joint_reading \
quantum_pulse::tests::three_levels_show_leakage_and_keep_the_trace \
quantum_pulse::tests::the_bound_is_stated_and_a_step_past_it_is_refused \
quantum_pulse::tests::what_the_model_does_not_hold_is_refused \
quantum_pulse::tests::a_detuned_play_split_in_two_is_the_same_play \
engine::pulse::tests::a_pulse_job_runs_end_to_end_and_its_answer_follows_its_schemas \
engine::pulse::tests::a_target_gives_the_qubits_their_frequency_and_times_and_its_hash_is_bound \
engine::pulse::tests::what_a_pulse_job_cannot_be_is_refused_with_its_code \
engine::pulse::tests::a_pulse_job_is_checked_by_running_it_again \
engine::pulse::tests::a_schedule_is_drawn_with_its_digests \
engine::pulse::tests::the_request_schema_holds_the_options_the_engine_reads \
engine::pulse::tests::a_pulse_job_is_the_same_bits_on_every_platform; do
grep -q "^test $t \.\.\. ok$" "$RUNNER_TEMP/pulse.log" || { echo "::error::$t did not run and pass"; exit 1; }
done
# Without warnings: a feature gate that leaves code unreachable or a
# value unused in one feature set shows here.
- name: Each feature set that holds the pulse layer builds, without warnings
shell: bash
working-directory: wai/wai-quantum
env:
RUSTFLAGS: -D warnings
run: |
set -euo pipefail
cargo check --locked --no-default-features --features quantum_pulse --lib
cargo check --locked --no-default-features --features quantum_engine --lib
cargo check --locked --no-default-features --features quantum_engine,quantum_receipt --lib
# The same bits on other processors and operating systems: the pinned
# digests of the simulator's and a pulse job's final density matrix.
bits:
name: the same bits · ${{ matrix.label }}
needs: relevant
if: needs.relevant.outputs.run == 'true'
strategy:
fail-fast: false
matrix:
label: [linux-arm64, macos]
runs-on: ${{ matrix.label }}
timeout-minutes: 60
steps:
- uses: actions/checkout@v5
# launchd's PATH omits cargo on the macOS host runner.
- name: Install Rust 1.98.0
shell: bash
run: |
export PATH="$HOME/.cargo/bin:$PATH"
command -v rustup >/dev/null 2>&1 || \
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --profile minimal --default-toolchain 1.98.0
rustup toolchain install 1.98.0 --profile minimal
rustup default 1.98.0
echo "$HOME/.cargo/bin" >> "$GITHUB_PATH"
- name: The pinned digests, in a debug build and a release build
shell: bash
working-directory: wai/wai-quantum
run: |
set -euo pipefail
export PATH="$HOME/.cargo/bin:$PATH"
for profile in "" "--release"; do
nice -n 10 cargo test --locked $profile --no-default-features --features quantum_engine,quantum_receipt --lib -- \
quantum_pulse::tests::the_same_schedule_gives_the_same_bits \
engine::pulse::tests::a_pulse_job_is_the_same_bits_on_every_platform 2>&1 | tee "$RUNNER_TEMP/bits.log"
grep -qE '^test result: ok\. 2 passed; 0 failed' "$RUNNER_TEMP/bits.log" || { echo "::error::the pinned bits differ here ($(uname -sm), ${profile:-debug})"; exit 1; }
done
wasip2:
name: reader, simulator and pulse jobs under wasm32-wasip2 (wasmtime)
needs: relevant
if: needs.relevant.outputs.run == 'true'
runs-on: linux
timeout-minutes: 60
steps:
- uses: actions/checkout@v5
- name: Install Rust 1.98.0 with the wasm32-wasip2 target
shell: bash
run: |
export PATH="$HOME/.cargo/bin:$PATH"
command -v rustup >/dev/null 2>&1 || \
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --profile minimal --default-toolchain 1.98.0
rustup toolchain install 1.98.0 --profile minimal
rustup default 1.98.0
rustup target add --toolchain 1.98.0 wasm32-wasip2
echo "$HOME/.cargo/bin" >> "$GITHUB_PATH"
# The release binary, pinned by version and SHA-256, rather than a build from source.
- name: Install wasmtime 49.0.2
shell: bash
run: |
set -euo pipefail
[ "$(uname -m)" = x86_64 ] || { echo "::error::the pinned wasmtime archive is x86_64; this host is $(uname -m)"; exit 1; }
if ! command -v xz >/dev/null 2>&1; then
SUDO=""; [ "$(id -u)" -ne 0 ] && SUDO="sudo"
$SUDO apt-get update -qq
$SUDO apt-get install -y -qq xz-utils
fi
v=49.0.2
f="wasmtime-v$v-x86_64-linux.tar.xz"
curl --proto '=https' --tlsv1.2 -sSfL -o "$RUNNER_TEMP/$f" \
"https://github.com/bytecodealliance/wasmtime/releases/download/v$v/$f"
echo "a4d6e9e3a5a60f527cf7793d674c48930c80c2e8977995b8a275cad3254b9322 $RUNNER_TEMP/$f" | sha256sum -c -
tar -xJf "$RUNNER_TEMP/$f" -C "$RUNNER_TEMP"
echo "$RUNNER_TEMP/wasmtime-v$v-x86_64-linux" >> "$GITHUB_PATH"
# A debug build, so integer overflow panics instead of wrapping.
- name: The pulse tests under wasmtime, to the pinned bits
shell: bash
working-directory: wai/wai-quantum
env:
CARGO_TARGET_WASM32_WASIP2_RUNNER: wasmtime
run: |
set -euo pipefail
cargo test --locked --target wasm32-wasip2 --features full --lib -- qasm::pulse:: quantum_pulse:: engine::pulse:: 2>&1 | tee "$RUNNER_TEMP/wasip2.log"
grep -qE '^test result: ok\. 26 passed; 0 failed' "$RUNNER_TEMP/wasip2.log" || { echo "::error::the pulse tests did not all pass under wasip2 (26 expected)"; exit 1; }

View file

@ -177,9 +177,10 @@ jobs:
# page to the verdicts its manifest gives and each worked out again by
# engine_verdict, a repair there, the answer conformance set
# (wai/quantum-conformance/answers/bundle.json) read to its codes, root and
# order, its stored answers' receipts verified by running them again, and a
# course check whose bytes match the native build's pinned digest. The count
# is the four tests above and these fourteen.
# order, its stored answers' receipts verified by running them again, a
# course check whose bytes match the native build's pinned digest, and a
# pulse run whose final density matrix is the native build's bits (its
# pinned digest). The count is the four tests above and these fifteen.
- name: The job engine in a page (wasm32 tests under Node)
shell: bash
working-directory: wai/wai-web
@ -187,7 +188,7 @@ jobs:
set -euo pipefail
CARGO_TARGET_WASM32_UNKNOWN_UNKNOWN_RUNNER=wasm-bindgen-test-runner \
cargo test --locked --target wasm32-unknown-unknown --features quantum_engine 2>&1 | tee "$RUNNER_TEMP/wai-web-engine-wasm.log"
grep -q "test result: ok. 18 passed; 0 failed" "$RUNNER_TEMP/wai-web-engine-wasm.log"
grep -q "test result: ok. 19 passed; 0 failed" "$RUNNER_TEMP/wai-web-engine-wasm.log"
# Its native half: the service's form and receipt, any angle on the state
# vector, what does not run saying why, plan, lint, draw and methods as JSON,
# critique, equivalence and variants as JSON, the classical shortcuts, the
@ -203,14 +204,15 @@ jobs:
# sealed there and worked out again, a repair held to the flagged
# lines there, an answer in the answer view layer streamed in, its calls
# run and its seal verified there, a correctness record and its corpus made
# there, and the circuits course read and checked there.
# there, the circuits course read and checked there, and a pulse program's
# schedule drawn, its run as a job to the pinned bits and its check there.
- name: The job engine in a page (native unit tests)
shell: bash
working-directory: wai/wai-web
run: |
set -euo pipefail
cargo test --locked --lib --features quantum_engine quantum_engine:: 2>&1 | tee "$RUNNER_TEMP/wai-web-engine.log"
grep -q "test result: ok. 26 passed; 0 failed" "$RUNNER_TEMP/wai-web-engine.log"
grep -q "test result: ok. 27 passed; 0 failed" "$RUNNER_TEMP/wai-web-engine.log"
# The crate's native unit tests, run on the host: the pure-Rust halves of the
# bindings, which wasm_tests does not run. They decode the committed integer
# payloads to their sidecars' SHA-256 (int_payload::tests) and check that the

View file

@ -178,6 +178,48 @@ wire format may change and such changes are recorded here.
implementation, `channel_lineup_verify.py`, from the text alone, agrees on every vector, and a
CI step runs both.
### wai-quantum: pulse and dynamics, simulated, the same bits everywhere, its receipts binding the calibrations
- **OpenPulse read** (`qasm::pulse`): a pulse program's `cal` and `defcal` blocks on physical qubits run in
order into a schedule on frames: ports (by the names `d0`, `m0`, `a0`, or mapped), frames with times of their
own, the waveform templates `constant`, `gaussian`, `sech`, `gaussian_square`, `drag` and `sine` and explicit
samples, `play`, `delay`, `shift_phase`, `set_phase`, `shift_frequency`, `set_frequency`, captures and
barriers. A gate starts when its qubits and its frames are free, and a frame's carrier runs on across a
frequency change. Durations are whole samples of `dt`. What the reader does not take is refused with a code and
its place: `Q453` to `Q462` join the table after `Q452`, renumbering nothing.
- **The pulse simulator** (`quantum_pulse`, in `quantum_engine` and `full`): two or three levels per qubit in its
rotating frame, the drive its frames' carriers give, anharmonicity, static couplings, and relaxation and
dephasing from T1 and T2, integrated sample by sample in each tone's frame by a twelfth-order series that uses
only `+ − × ÷` and `sqrt`, so the final density matrix is the same bits on x86_64, arm64, wasm32-wasip2 and in
the browser; CI pins its digest on each. The plan states a proved bound on the trace distance from the model's
exact solution, with an estimate of rounding beside it, and refuses a step longer than 1/λ or a schedule whose
error would pass its tolerance, naming the steps that would meet it. The tests hold it to closed forms within
that bound: Rabi turns, a quarter turn of phase, detuned drives, T1 decay and T2 dephasing.
- **Pulse runs as jobs** (`engine::pulse`): the method `pulse` everywhere jobs run (`wai-quantum run --method
pulse --pulse OPTIONS`, both MCP servers' run tools, the job service's `/v1/jobs` and `/v1/run`, the page's
`run`). The device comes from a target's calibration snapshot and the options over the defaults, each
parameter saying where it came from; the counts from the captures' joint reading, into the program's bits;
the accuracy class is `bounded`. The result's `pulse.calibration` holds the control program's digest, its
calibrations', the schedule's, the device's and the target snapshot's, `pulse.integrator` the integrator and
its step, and `pulse.density_hash` the final state; the receipt seals them.
- **Schedules and checks**: `wai-quantum pulse schedule` (each frame's timeline, the gate calls, the waveforms)
and `wai-quantum pulse check` (a sealed pulse job held to its request by running it again, each differing
field named); the MCP tools `pulse_schedule` and `pulse_check` on both servers; the service's
`POST /v1/pulse/schedule` and `/v1/pulse/check`; and `QuantumEngine.pulse` in the page. `J040` and `J041` join
the job layer's refusals. `schemas/pulse.v1.json` is new, and the job request and result schemas gain the
pulse options and the pulse result. [The pulse page](wai-quantum/docs/pulse.md) gives the model, the bound
and the options.
- **The Studio's pulse pane**: a pulse program's schedule drawn as a timeline (a lane per frame, each play its
waveform's envelope, captures, barriers, phase and frequency changes, and the gate calls above), a program the
pulse reader refuses answered with its code, an example to load, and a run on the pulse simulator with its
readings, its bound and what its receipt binds, each answer's seal checked in the page; the desktop app serves
the same calls (`pulse`).
- **CI:** `wai-quantum-pulse.yml` runs the 26 tests by name on Linux, the pinned digests on arm64 Linux and on
macOS in debug and release builds, and the tests under wasmtime; `wai-web-wasm.yml`'s pins move to 27 (native)
and 19 (wasm32).
- **Breaking:** `api:rust:wai_quantum::engine::schema::FILES`: the committed schemas grew from 22 to 23 with
`pulse.v1.json`. Code that names the array's length iterates over `FILES` instead.
### wai-quantum: Quantum Studio desktop installers, package-manager entries and signed updates
- **Installers** (`.github/workflows/wai-quantum-desktop-release.yml`, on a tag `wai-quantum-desktop-v<version>`):

View file

@ -110,6 +110,10 @@ A proposal is verified only when it reads, keeps the rules and passes the check
"Error correction", a closed section under the result, opens on a code chosen from a list: the rotated surface code, the triangular colour code, the toric code, the Steane code, and two bivariate-bicycle codes. Its lattice is drawn with each check and qubit, and its distance certificate stated under it: in each sector, the least weight of a logical found, and every lighter support ruled out by an exhaustive search; "Show witness" rings a logical of that weight on the lattice. Clicking a qubit (or Enter on it) flips it, and its syndrome lights the checks it violates; "Decode" corrects the syndrome with the decoder chosen, draws the correction, and says whether the residual is a product of stabilizers (the logical state unharmed) or a logical error. Under "Threshold", "Measure" samples logical failure rates at distances 3, 5 and 7 over three physical rates and plots them on log scales, each point with its failures, shots and standard error. Every answer is sealed with this browser's key (the desktop app's, in the app) and checked here. What each decoder guarantees is on [the error-correction page](../wai-quantum/docs/qec.md).
## Pulse
"Pulse", a closed section beside error correction, draws the schedule of a pulse program, OpenPulse calibrations (`cal`, `defcal`) on physical qubits: each frame a lane with its port and qubit, each play its waveform's envelope at its samples, each capture a hatched box, each barrier a rule, each phase or frequency change a tick, and each gate call a bracket above the lanes, every mark titled with its samples and its line. A program the pulse reader does not take is answered with its refusal and its code; "Load a pulse example" puts a DRAG `x` and `sx` and a measurement as a readout tone and a capture in the editor. "Run on the pulse simulator" runs the program as a job of the method `pulse`: each capture's reading, the counts, the bound the integrator states, and what the receipt binds (the program, its calibrations, the schedule, the device and the final state's digest). Every answer is sealed with this browser's key (the desktop app's, in the app) and checked here. The model and the bound are on [the pulse page](../wai-quantum/docs/pulse.md).
With noise set, "Mitigate", in the Noise section, mitigates the run's expectation values (zero-noise extrapolation, error amplification or cancellation from a learned noise model, Clifford data regression, virtual distillation, or readout inversion). The result shows each value raw and mitigated with its error bar, beside the ideal one the simulation knows, with the method's bias and error-bar assumptions and the circuits and shots it took.
## What is new, and Found

View file

@ -78,6 +78,8 @@ export class TabEngine {
estimateVerify(body) { return this.call('estimate_verify', { body }); }
// Error correction: `name` is code, decode or threshold; each answer sealed with this browser's key.
qec(name, args) { return this.call('qec', { name, args }); }
// Pulse programs: `op` is schedule or check, sealed with this browser's key (a pulse run is run({method: 'pulse'})).
pulse(op, args) { return this.call('pulse', { name: op, args }); }
// A job's verdict ({request, result} or {request, error}), worked out here; with
// an intent, that intent checked against the job's program.
verdict(job, intent) { return this.call('verdict', { job, intent }); }
@ -165,6 +167,8 @@ export class ServiceEngine {
async methods() { return this.json(await fetch(`${this.base}/v1/methods`)); }
// Error correction on the service, sealed with its key.
async qec(name, args) { return this.json(await fetch(`${this.base}/v1/qec/${encodeURIComponent(name)}`, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(args ?? {}) })); }
// A pulse program's schedule, or a pulse job checked, on the service, sealed with its key.
async pulse(op, args) { return this.json(await fetch(`${this.base}/v1/pulse/${encodeURIComponent(op)}`, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(args ?? {}) })); }
async describe() { return this.json(await fetch(`${this.base}/v1`)); }
cancel() {
this.stopped = true;
@ -207,6 +211,8 @@ export class DesktopEngine {
estimateVerify(body) { return this.call('estimate_verify', { body: JSON.stringify(body) }); }
// Error correction, sealed with this user's key.
qec(name, args) { return this.call('qec', { op: name, args: JSON.stringify(args ?? {}) }); }
// A pulse program's schedule, or a pulse job checked, sealed with this user's key.
pulse(op, args) { return this.call('pulse', { op, args: JSON.stringify(args ?? {}) }); }
verdict(job, intent) { return this.call('verdict', { job: JSON.stringify(job), intent: intent ? JSON.stringify(intent) : '' }); }
repair(qasm, options) { return this.call('repair', { qasm, options: JSON.stringify(options ?? {}) }); }
instantiate(qasm, inputs) { return this.call('instantiate', { qasm, inputs: JSON.stringify(inputs ?? {}) }); }

View file

@ -48,6 +48,7 @@ import { rewritePanel } from './rewrite-view.js';
import { compilePanel } from './compile-view.js';
import { estimatePanel } from './estimate-view.js';
import { qecPane } from './qec-view.js';
import { pulsePane } from './pulse-view.js';
import * as Predict from './predict.js';
import { HardwareEngine, askDialog, hardwareSegment } from './hardware.js';
import { debuggerPane } from './debugger.js';
@ -460,8 +461,10 @@ function mount(root) {
// ---- error correction: a code, an error placed on it, its decode, a threshold ----
const qec = qecPane({ engine: local });
// ---- pulse: the program's schedule on frames, and a run of it under the master equation ----
const pulse = pulsePane({ engine: local, source: () => text(), load: (s) => setText(s) });
root.replaceChildren(header, course.node, el('div', { class: 'qs-grid' }, program, el('div', { class: 'qs-side' }, circuitPane, debug.node, gallery.node, workbench.node, found.node, resultPane, qec.node, versions.node, aiPane, answerPane, historyPane, studyUi.node)), footer);
root.replaceChildren(header, course.node, el('div', { class: 'qs-grid' }, program, el('div', { class: 'qs-side' }, circuitPane, debug.node, gallery.node, workbench.node, found.node, resultPane, qec.node, pulse.node, versions.node, aiPane, answerPane, historyPane, studyUi.node)), footer);
// On the desktop, a newer version found as the app opens, offered above the frame.
if (desktop) {
const update = updateBar({ updates: new Updates(window.__TAURI__.core.invoke), say: (m, cls) => setStatus(m, cls) });

View file

@ -0,0 +1,170 @@
// The pulse pane (engine::pulse): the program's schedule on frames, drawn as a
// timeline (each frame a lane; each play its waveform's envelope at its
// samples; each capture a hatched box; each barrier a rule; each phase or
// frequency change a tick; each gate call a bracket above the lanes), and a
// pulse run of it as a job of the method `pulse`: each capture's reading, the
// bound the integrator states, the final density matrix's digest, and the
// digests its receipt binds, its seal checked here. The pane stays closed
// until it is opened; a program that is not a pulse program is answered with
// the reader's refusal, and the example loads one.
import { el } from './dom.js';
const NS = 'http://www.w3.org/2000/svg';
// A pulse program: an `x` as a DRAG pulse and a measurement as a readout tone
// and a capture.
export const PULSE_EXAMPLE = `OPENQASM 3.0;
defcalgrammar "openpulse";
cal {
extern port d0;
extern port m0;
extern port a0;
frame q0 = newframe(d0, 5.0e9, 0);
frame r0 = newframe(m0, 7.1e9, 0);
frame c0 = newframe(a0, 7.1e9, 0);
}
defcal x $0 { play(q0, drag(0.4, 64dt, 16dt, 0.2)); }
defcal sx $0 { play(q0, drag(0.2, 64dt, 16dt, 0.2)); }
defcal measure $0 -> bit {
play(r0, gaussian_square(0.1, 200dt, 160dt, 10dt));
return capture_v2(c0, 200dt);
}
bit c;
sx $0;
x $0;
c = measure $0;
`;
function node(tag, attrs = {}, ...kids) {
const e = document.createElementNS(NS, tag);
for (const [k, v] of Object.entries(attrs)) if (v != null) e.setAttribute(k, String(v));
for (const k of kids) if (k != null) e.append(typeof k === 'string' ? document.createTextNode(k) : k);
return e;
}
const fmt = (x, d = 3) => (Math.abs(x) >= 1e4 || (x !== 0 && Math.abs(x) < 1e-3) ? x.toExponential(d - 1) : String(+x.toFixed(d)));
// The schedule as an SVG timeline. `s` is the schedule's JSON
// (qasm::pulse::Schedule::to_json, with its waveforms' samples).
export function scheduleSvg(s) {
const frames = s.frames ?? [];
const W = 760, L = 132, R = 12, TOP = 30, LANE = 44, AXIS = 26;
const H = TOP + LANE * Math.max(frames.length, 1) + AXIS;
const total = Math.max(s.duration ?? 0, 1);
const X = (t) => L + (t / total) * (W - L - R);
const peak = Math.max(1e-12, ...(s.waveforms ?? []).flatMap((w) => w.samples.map(([re, im]) => Math.hypot(re, im))));
const svg = node('svg', { viewBox: `0 0 ${W} ${H}`, class: 'qs-pulse-svg', role: 'img', 'aria-label': `Pulse schedule: ${s.duration} samples (${fmt(s.duration_ns)} ns) on ${frames.length} frame${frames.length === 1 ? '' : 's'}, ${(s.calls ?? []).length} gate call${(s.calls ?? []).length === 1 ? '' : 's'}` });
// The gate calls, above the lanes.
const calls = node('g', { class: 'qs-pulse-calls' });
(s.calls ?? []).forEach((c, i) => {
const x0 = X(c.start), x1 = Math.max(X(c.end), x0 + 2), y = 6 + (i % 2) * 10;
const label = `${c.name}${c.params?.length ? `(${c.params.map((p) => fmt(p)).join(', ')})` : ''} ${c.qubits.map((q) => `$${q}`).join(', ')}`;
calls.append(node('g', { class: 'qs-pulse-call' },
node('path', { d: `M${x0},${y + 8} V${y} H${x1} V${y + 8}`, class: 'qs-pulse-bracket' }),
node('text', { x: (x0 + x1) / 2, y: y - 1, 'text-anchor': 'middle', class: 'qs-pulse-call-label' }, label),
node('title', {}, `${label}: samples ${c.start}–${c.end}, line ${c.line}`)));
});
svg.append(calls);
frames.forEach((f, i) => {
const y0 = TOP + i * LANE, mid = y0 + LANE / 2, amp = LANE / 2 - 5;
const lane = node('g', { class: `qs-pulse-lane ${f.kind}`, 'data-frame': f.name });
lane.append(
node('line', { x1: L, x2: W - R, y1: mid, y2: mid, class: 'qs-pulse-base' }),
node('text', { x: 4, y: mid - 4, class: 'qs-pulse-frame' }, f.name),
node('text', { x: 4, y: mid + 10, class: 'qs-pulse-port' }, `${f.port} · ${f.kind} $${f.qubit}`));
for (const ins of (s.instructions ?? []).filter((x) => x.frame === i)) {
const x0 = X(ins.start), x1 = X(ins.start + ins.length);
if (ins.op === 'play') {
const w = s.waveforms[ins.waveform];
const every = w.samples_every ?? 1;
const n = w.samples.length;
const pts = w.samples.map(([re, im], k) => `${fmt(X(ins.start + (k * every)), 2)},${fmt(mid - (Math.hypot(re, im) / peak) * amp, 2)}`);
lane.append(node('g', { class: 'qs-pulse-play' },
node('rect', { x: x0, y: y0 + 3, width: Math.max(x1 - x0, 1), height: LANE - 6, class: 'qs-pulse-box' }),
node('polyline', { points: `${fmt(x0, 2)},${mid} ${pts.join(' ')} ${fmt(x1, 2)},${mid}`, class: 'qs-pulse-env' }),
node('title', {}, `play ${w.name} on ${f.name}: samples ${ins.start}–${ins.start + ins.length} (${n * every} samples), phase ${fmt(ins.phase)} rad at ${fmt(ins.frequency_hz / 1e9, 6)} GHz, line ${ins.line}`)));
} else if (ins.op === 'capture') {
lane.append(node('g', { class: 'qs-pulse-capture' },
node('rect', { x: x0, y: y0 + 3, width: Math.max(x1 - x0, 1), height: LANE - 6, class: 'qs-pulse-cap' }),
node('title', {}, `capture on ${f.name}: samples ${ins.start}–${ins.start + ins.length}${ins.bit ? ` into ${ins.bit}` : ''}, line ${ins.line}`)));
} else if (ins.op === 'barrier') {
lane.append(node('line', { x1: x0, x2: x0, y1: y0 + 2, y2: y0 + LANE - 2, class: 'qs-pulse-barrier' }, node('title', {}, `barrier at sample ${ins.start}`)));
} else if (ins.op !== 'delay') {
lane.append(node('circle', { cx: x0, cy: y0 + 6, r: 3, class: 'qs-pulse-tick' }, node('title', {}, `${ins.op}(${fmt(ins.value, 6)}) at sample ${ins.start}, line ${ins.line}`)));
}
}
svg.append(lane);
});
// The time axis, in samples and in nanoseconds.
const ya = TOP + LANE * Math.max(frames.length, 1) + 4;
svg.append(node('g', { class: 'qs-pulse-axis' },
node('line', { x1: L, x2: W - R, y1: ya, y2: ya }),
node('text', { x: L, y: ya + 16, 'text-anchor': 'start' }, '0'),
node('text', { x: W - R, y: ya + 16, 'text-anchor': 'end' }, `${s.duration} samples · ${fmt(s.duration_ns)} ns at dt = ${s.dt_ns} ns`)));
return svg;
}
// `engine` answers pulse(op, args), run(request) and verify(body); `source()`
// gives the editor's program and `load(text)` replaces it.
export function pulsePane({ engine, source, load }) {
const draw = el('button', { class: 'qs-btn qs-small', id: 'qs-pulse-draw', type: 'button', text: 'Draw schedule', title: 'The program’s schedule on frames, read by the pulse reader' });
const run = el('button', { class: 'qs-btn qs-small', id: 'qs-pulse-run', type: 'button', text: 'Run on the pulse simulator', title: 'Integrate the schedule under the master equation: its readings, the error bound, the receipt' });
const example = el('button', { class: 'qs-btn qs-small', id: 'qs-pulse-example', type: 'button', text: 'Load a pulse example', title: 'A DRAG pulse for x and sx, and a measurement as a readout tone and a capture' });
const shots = el('input', { class: 'qs-input qs-small-input', id: 'qs-pulse-shots', type: 'text', value: '1000', inputmode: 'numeric', 'aria-label': 'Shots' });
const summary = el('p', { class: 'qs-why', id: 'qs-pulse-summary', text: 'Draw the schedule of a pulse program: OpenPulse calibrations (cal, defcal) on physical qubits.' });
const timeline = el('div', { class: 'qs-diagram qs-pulse-timeline', id: 'qs-pulse-timeline' });
const result = el('p', { class: 'qs-note', id: 'qs-pulse-result' });
const binds = el('p', { class: 'qs-note qs-pulse-binds', id: 'qs-pulse-binds' });
const details = el('details', { class: 'qs-pulse-details', id: 'qs-pulse' },
el('summary', { class: 'qs-head' }, el('h2', { id: 'qs-pulse-h', text: 'Pulse' }), el('span', { class: 'qs-note', text: 'the schedule on frames, and a run of it under the master equation' })),
el('div', { class: 'qs-shortcuts' },
el('div', { class: 'qs-shortcuts-head' }, draw, run, el('label', { class: 'qs-inline', for: 'qs-pulse-shots' }, el('span', { text: 'shots' }), shots), el('span', { class: 'qs-grow' }), example),
summary, timeline, result, binds));
const pane = el('section', { class: 'qs-pane qs-pulse', id: 'qs-pulse-pane', 'aria-labelledby': 'qs-pulse-h' }, details);
let busy = false;
details.addEventListener('toggle', () => { if (details.open && !timeline.firstChild && !busy) show(); });
async function verified(body) {
try { const v = await engine.verify(body); return v.valid === true; } catch (e) { return false; }
}
async function show() {
if (busy) return;
busy = true; draw.disabled = true;
summary.textContent = 'Reading the program’s calibrations…';
try {
const body = await engine.pulse('schedule', { qasm: source() });
if (body.error) throw new Error(body.error);
const s = body.result;
timeline.replaceChildren(scheduleSvg(s));
const ok = await verified(body);
const plays = s.instructions.filter((i) => i.op === 'play').length, caps = s.instructions.filter((i) => i.op === 'capture').length;
summary.textContent = `${s.duration} samples (${fmt(s.duration_ns)} ns) at dt = ${s.dt_ns} ns on ${s.frames.length} frame${s.frames.length === 1 ? '' : 's'}: ${plays} play${plays === 1 ? '' : 's'}, ${caps} capture${caps === 1 ? '' : 's'}, ${s.calls.length} gate call${s.calls.length === 1 ? '' : 's'}; the schedule is sealed and its seal ${ok ? 'verifies here' : 'DOES NOT VERIFY here'}.`;
} catch (e) {
timeline.replaceChildren();
summary.textContent = `${e.message} Load the pulse example to see one.`;
} finally { busy = false; draw.disabled = false; }
}
async function go() {
if (busy) return;
busy = true; run.disabled = true;
result.textContent = 'Integrating the schedule…'; binds.textContent = '';
try {
const n = Number.parseInt(shots.value, 10);
const job = await engine.run({ qasm: source(), method: 'pulse', shots: Number.isFinite(n) && n >= 0 ? n : 1000 });
const r = job.result, p = r.pulse, a = r.accuracy;
const reads = (p.captures ?? []).map((c) => `$${c.qubit}${c.bit ? ` → ${c.bit}` : ''}: P(1) = ${c.p1.toFixed(6)}`).join('; ');
const counts = r.counts ? Object.entries(r.counts).map(([k, v]) => `${k}: ${v}`).join(', ') : 'none (the captures start at different samples)';
const ok = await verified(job);
result.textContent = `${reads || 'no capture: the final state read'}. Counts ${counts}. Accuracy ${a.class}: every probability within ${a.bound.toExponential(2)} of the model’s exact solution (truncation proved ${a.truncation_bound.toExponential(1)}, rounding estimated ${a.rounding_estimate.toExponential(1)}); ${p.integrator.substeps} step${p.integrator.substeps === 1 ? '' : 's'} of ${fmt(p.integrator.step_ns)} ns a sample, order ${p.integrator.order}. The result is sealed and its seal ${ok ? 'verifies here' : 'DOES NOT VERIFY here'}.`;
const c = p.calibration;
binds.textContent = `The receipt binds the program ${c.program.slice(0, 12)}…, its calibrations ${c.calibrations.slice(0, 12)}…, the schedule ${c.schedule.slice(0, 12)}…, the device ${c.device.slice(0, 12)}… and the final state ${p.density_hash.slice(0, 12)}…, the same bits on every platform.`;
if (!timeline.firstChild) show();
} catch (e) {
result.textContent = e.message;
} finally { busy = false; run.disabled = false; }
}
draw.addEventListener('click', show);
run.addEventListener('click', go);
example.addEventListener('click', () => { load(PULSE_EXAMPLE); timeline.replaceChildren(); result.textContent = ''; binds.textContent = ''; show(); });
return { node: pane, show };
}

View file

@ -574,6 +574,28 @@
.qs-qec-details > summary::-webkit-details-marker { display: none; }
.qs-qec-details > summary::before { content: '▸'; color: var(--text-muted); margin-right: 6px; }
.qs-qec-details[open] > summary::before { content: '▾'; }
/* The pulse pane: each frame a lane, plays as their envelopes, captures hatched. */
.qs-pulse .qs-shortcuts-head { flex-wrap: wrap; }
.qs-pulse .qs-small-input { width: 5em; min-height: 26px; padding: 2px 6px; }
.qs-pulse-timeline { max-width: 760px; max-height: none; overflow: visible; }
.qs-pulse-svg { width: 100%; height: auto; display: block; font: 10px var(--font-mono, ui-monospace, monospace); }
.qs-pulse-base { stroke: var(--border-subtle, var(--border)); stroke-width: 1; }
.qs-pulse-frame { fill: var(--text-primary, currentColor); font-weight: 600; }
.qs-pulse-port, .qs-pulse-axis text, .qs-pulse-call-label { fill: var(--text-muted, currentColor); }
.qs-pulse-axis line { stroke: var(--border-subtle, var(--border)); }
.qs-pulse-box { fill: color-mix(in srgb, var(--info-text) 10%, transparent); stroke: none; }
.qs-pulse-lane.measure .qs-pulse-box { fill: color-mix(in srgb, var(--text-secondary) 10%, transparent); }
.qs-pulse-env { fill: none; stroke: var(--info-text); stroke-width: 1.4; stroke-linejoin: round; }
.qs-pulse-lane.measure .qs-pulse-env { stroke: var(--text-secondary); }
.qs-pulse-cap { fill: color-mix(in srgb, var(--warn-text) 22%, transparent); stroke: var(--warn-text); stroke-width: 1; stroke-dasharray: 3 2; }
.qs-pulse-barrier { stroke: var(--text-muted); stroke-width: 1; stroke-dasharray: 2 2; }
.qs-pulse-tick { fill: var(--std-text); }
.qs-pulse-bracket { fill: none; stroke: var(--text-secondary); stroke-width: 1; }
.qs-pulse-details > summary { list-style: none; cursor: pointer; }
.qs-pulse-details > summary::-webkit-details-marker { display: none; }
.qs-pulse-details > summary::before { content: '▸'; color: var(--text-muted); margin-right: 6px; }
.qs-pulse-details[open] > summary::before { content: '▾'; }
/* Answer views: an answer in the answer view layer, each view drawn as its statement arrives. */
.qs-answers > summary { cursor: pointer; font-weight: 600; }
.qs-answers .qs-row { align-items: flex-end; }

View file

@ -48,6 +48,7 @@ self.onmessage = async (ev) => {
case 'estimate': text = e().estimate(JSON.stringify(ev.data.request)); break;
case 'estimate_verify': text = mod.engine_estimate_verify(JSON.stringify(ev.data.body)); break;
case 'qec': text = e().qec(ev.data.name, JSON.stringify(ev.data.args ?? {})); break;
case 'pulse': text = e().pulse(ev.data.name, JSON.stringify(ev.data.args ?? {})); break;
case 'verdict': text = mod.engine_verdict(JSON.stringify(ev.data.job), ev.data.intent ? JSON.stringify(ev.data.intent) : '', memoryMb); break;
case 'repair': text = mod.engine_repair(ev.data.qasm, JSON.stringify(ev.data.options ?? {})); break;
case 'lint_with': text = mod.engine_lint_with(ev.data.qasm, JSON.stringify(ev.data.inputs ?? {})); break;

View file

@ -1171,6 +1171,29 @@ const wbIdle = () => page.waitForFunction(() => !document.querySelector('#qs-wb-
const series = await page.$$eval('#qs-qec-plot svg .qs-qec-line', (l) => l.length);
const reading = await page.textContent('#qs-qec-reading');
check(series === 3 && /code capacity/.test(reading) && /seal verifies here/.test(reading), `a threshold is measured and plotted, one line per distance (${series}; ${reading})`);
// Pulse: a program that is not a pulse program is refused by the pulse
// reader; the example's schedule is drawn on its frames, and its run
// integrated, bounded and sealed.
check(!(await page.$eval('#qs-pulse', (d) => d.open)), 'the pulse pane waits closed until it is opened');
await setProgram('OPENQASM 3.0;\nqubit q;\nh q;\n');
await page.click('#qs-pulse > summary');
await page.waitForFunction(() => /\[Q460\]/.test(document.querySelector('#qs-pulse-summary')?.textContent ?? ''), null, { timeout: LONG });
check((await page.$$('#qs-pulse-timeline svg')).length === 0, `a circuit with declared qubits is refused by the pulse reader, with its code (${await page.textContent('#qs-pulse-summary')})`);
await page.click('#qs-pulse-example');
await page.waitForFunction(() => /seal verifies here/.test(document.querySelector('#qs-pulse-summary')?.textContent ?? ''), null, { timeout: LONG });
const lanes = await page.$$eval('#qs-pulse-timeline .qs-pulse-lane', (l) => l.map((x) => x.getAttribute('data-frame')));
const plays = await page.$$eval('#qs-pulse-timeline .qs-pulse-play', (p) => p.length);
const caps = await page.$$eval('#qs-pulse-timeline .qs-pulse-capture', (p) => p.length);
const calls = await page.$$eval('#qs-pulse-timeline .qs-pulse-call-label', (c) => c.map((x) => x.textContent));
check(lanes.join(',') === 'q0,r0,c0' && plays === 3 && caps === 1 && calls.join(',') === 'sx $0,x $0,measure $0', `the example's schedule is drawn: a lane per frame, its plays, its capture and its gate calls (${lanes}; ${plays} plays; ${caps} capture; ${calls})`);
check(/328 samples \(164 ns\)/.test(await page.textContent('#qs-pulse-summary')), `the schedule's length is stated (${await page.textContent('#qs-pulse-summary')})`);
await page.fill('#qs-pulse-shots', '500');
await page.click('#qs-pulse-run');
await page.waitForFunction(() => /seal verifies here/.test(document.querySelector('#qs-pulse-result')?.textContent ?? ''), null, { timeout: LONG });
const pr = await page.textContent('#qs-pulse-result');
const pb = await page.textContent('#qs-pulse-binds');
check(/\$0 → c: P\(1\) = /.test(pr) && /Accuracy bounded/.test(pr) && /The receipt binds the program/.test(pb), `the pulse run gives the capture's reading, its bound and what its receipt binds, sealed (${pr.slice(0, 200)}; ${pb.slice(0, 120)})`);
await (await page.$('#qs-pulse-pane')).screenshot({ path: join(SHOTS, 'studio-pulse.png') });
// A run that mitigates its value under its noise.
await setProgram('OPENQASM 3.0;\nqubit[2] q;\nh q[0];\ncx q[0], q[1];\nrz(0.3) q[1];\n');
await page.fill('#qs-obs', 'Z0 Z1');

View file

@ -13,6 +13,7 @@
"wai/wai-quantum/docs/qasm-diagnostics.md",
"wai/wai-quantum/docs/qasm-feature-matrix.md",
"wai/wai-quantum/docs/qec.md",
"wai/wai-quantum/docs/pulse.md",
"wai/wai-quantum/VERSIONING.md",
"wai/wai-quantum/DEPRECATIONS.md",
"wai/wai-quantum/course/README.md",

View file

@ -0,0 +1,13 @@
OPENQASM 3.0;
defcalgrammar "openpulse";
cal {
extern port d0;
extern port a0;
frame q0 = newframe(d0, 5.0e9, 0);
frame c0 = newframe(a0, 5.0e9, 0);
}
defcal x $0 { play(q0, drag(0.4, 64dt, 16dt, 0.2)); }
defcal measure $0 -> bit { return capture_v2(c0, 100dt); }
bit c;
x $0;
c = measure $0;

View file

@ -255,6 +255,19 @@ wai-quantum qec threshold --family color --distances 3,5,7 --p 0.02,0.05
`qec code` shows a code (`surface:D`, `toric:L`, `steane`, `color:D`, `bb:72`, `bb:90`, `bb:108`, `bb:144` or `gross`, `bb:288`) with its distance certificate: in each sector, the least weight of a logical found, a witness of that weight, and every lighter support ruled out by an exhaustive search. `qec decode` decodes at code capacity (`matching`, `union_find`, `color`, `relay_bp`, `search`) or at circuit level (`matching`, `correlated`, `union_find`, `bp_osd`, `window`); a model alone can be decoded but not sampled, since it says how faults happen, not which did. `qec threshold` measures failure rates with their standard errors and fits the code model. Every answer is signed with your key (`--json` prints it sealed). The research forms stay at `wai-quantum lab qec`. What each decoder guarantees is on [the error-correction page](../wai-quantum/docs/qec.md).
## Pulse programs
A pulse program gives its gates as OpenPulse calibrations on physical qubits. It runs as a job of the method `pulse`: integrated under a master equation, its result the same bits on every platform, its error bounded and stated, and its receipt binding the control program, its calibrations, the device and the integrator.
```sh runnable
wai-quantum pulse schedule x-pulse.qasm --demo-key # each frame's timeline: plays, captures, barriers; the gate calls
wai-quantum run x-pulse.qasm --method pulse --shots 1000 --demo-key # the capture's reading, the bound, what the receipt binds
wai-quantum run x-pulse.qasm --method pulse --pulse '{"levels": 3}' --demo-key --json > pulse.json
wai-quantum pulse check pulse.json --demo-key # the receipt, the program's digest, and the run again, bit for bit
```
`--pulse` takes the run's options as JSON or a file of it: the sample period (`dt_ns`), the levels kept, the tolerance on the stated error, the steps per sample, each qubit's parameters, couplings and ports. `--target` gives each qubit's frequency, T1 and T2 from a target's calibration. A step or a schedule whose error cannot be stated within the tolerance is refused (`J041`). The model, the bound and what a receipt binds are on [the pulse page](../wai-quantum/docs/pulse.md).
## Compiling
```sh runnable

View file

@ -253,6 +253,19 @@ wai-quantum qec threshold --family color --distances 3,5,7 --p 0.02,0.05
`qec code` 显示一个码(`surface:D`、`toric:L`、`steane`、`color:D`、`bb:72`、`bb:90`、`bb:108`、`bb:144` 或 `gross`、`bb:288`)及其距离证书:在每个扇区中,找到的逻辑算符的最小权重、该权重的一个见证,以及经穷举搜索排除的每个更轻的支撑。`qec decode` 在码容量层面(`matching`、`union_find`、`color`、`relay_bp`、`search`)或线路层面(`matching`、`correlated`、`union_find`、`bp_osd`、`window`)解码;仅有模型时可以解码但不能采样,因为模型说明的是故障如何发生,而不是发生了哪些。`qec threshold` 测量失败率及其标准误差,并拟合码模型。每个回答都以你的密钥签名(`--json` 打印封存后的结果)。研究用的形式仍在 `wai-quantum lab qec`。各解码器保证什么,见[纠错页面](../wai-quantum/docs/qec.md)。
## 脉冲程序
脉冲程序以作用于物理量子比特的 OpenPulse 校准来给出它的门。它作为方法 `pulse` 的作业运行:在主方程下积分,结果在每个平台上比特一致,误差有界并予以说明,回执绑定控制程序、其校准、设备和积分器。
```sh runnable
wai-quantum pulse schedule x-pulse.qasm --demo-key # 每个帧的时间线:播放、采集、屏障;各个门调用
wai-quantum run x-pulse.qasm --method pulse --shots 1000 --demo-key # 采集的读数、误差上界,以及回执所绑定的内容
wai-quantum run x-pulse.qasm --method pulse --pulse '{"levels": 3}' --demo-key --json > pulse.json
wai-quantum pulse check pulse.json --demo-key # 回执、程序的摘要,以及重新运行,逐比特一致
```
`--pulse` 以 JSON 或包含 JSON 的文件给出运行选项:采样周期(`dt_ns`)、保留的能级数、所述误差的容差、每个采样的步数、每个量子比特的参数、耦合和端口。`--target` 从目标的校准中给出每个量子比特的频率、T1 和 T2。误差无法在容差内说明的步长或时序会被拒绝(`J041`)。模型、误差上界以及回执所绑定的内容见[脉冲页面](../wai-quantum/docs/pulse.md)。
## 编译
```sh runnable

View file

@ -50,7 +50,7 @@ Parameters:
- `inputs` (object): Values for a generator's integer inputs, as {"n": 5}: the program is read as its family's circuit at those values.
- `shots` (integer): Shots to sample; 1024 when nothing else is asked.
- `seed` (integer): The sampling seed (default 1).
- `method` (string): auto (the default: the strongest method the program admits), or one of list_methods' names.
- `method` (string): auto (the default: the strongest method the program admits), or one of list_methods' names; pulse runs a pulse program (OpenPulse calibrations on physical qubits, see pulse_schedule) under the pulse options, its error bounded.
- `observables` (array): Pauli observables, as "Z0 Z1" or "0.5 X2".
- `amplitudes` (array): Basis states whose amplitudes to return, as bit strings with the highest qubit first.
- `noise` (object): Channel probabilities
@ -60,6 +60,7 @@ Parameters:
- `intent` (any): What the program must do, checked by the engine with the run: the result's "verdict" states how far it checked (proof, evidence, executed, parsed, refused) and whether the intent holds, sealed by the receipt.
- `repair` (any): Repair the program first, on the lines its diagnostics flag alone (as repair_qasm does): true, or the choices {"max_iterations": N, "dyadic_bits": K, "sizes": {"q": 3}}. The result's "repaired" holds the program that ran and each repair.
- `mitigate` (any): Mitigate the job's expectation values (or, for readout, its counts) under its own noise: zne (the whole program folded U(U†U)^k and extrapolated), pea (the noise learned as a sparse Pauli–Lindblad model and amplified), pec (the learned channels inverted and sampled), cdr (a line fitted over near-Clifford training circuits), vd (two copies), readout (the counts' flips inverted). Needs noise and observables (shots for readout), at most 8 qubits. The result's `mitigation` gives raw and mitigated values with standard errors, the ideal value (known because this is a simulation), the method's bias and variance assumptions and its cost. A method's name, or an object.
- `pulse` (object): A pulse run's options (method "pulse"): the sample period, the levels kept, the tolerance on the stated error, the steps a sample is divided into, each qubit's parameters, static couplings, and ports beyond the names d0, m0, a0.
### `plan_quantum_circuit`: Plan a quantum program
@ -70,7 +71,7 @@ Parameters:
- `inputs` (object): Values for a generator's integer inputs, as {"n": 5}: the program is read as its family's circuit at those values.
- `shots` (integer): Shots to sample; 1024 when nothing else is asked.
- `seed` (integer): The sampling seed (default 1).
- `method` (string): auto (the default: the strongest method the program admits), or one of list_methods' names.
- `method` (string): auto (the default: the strongest method the program admits), or one of list_methods' names; pulse runs a pulse program (OpenPulse calibrations on physical qubits, see pulse_schedule) under the pulse options, its error bounded.
- `observables` (array): Pauli observables, as "Z0 Z1" or "0.5 X2".
- `amplitudes` (array): Basis states whose amplitudes to return, as bit strings with the highest qubit first.
- `noise` (object): Channel probabilities
@ -80,6 +81,7 @@ Parameters:
- `intent` (any): What the program must do, checked by the engine with the run: the result's "verdict" states how far it checked (proof, evidence, executed, parsed, refused) and whether the intent holds, sealed by the receipt.
- `repair` (any): Repair the program first, on the lines its diagnostics flag alone (as repair_qasm does): true, or the choices {"max_iterations": N, "dyadic_bits": K, "sizes": {"q": 3}}. The result's "repaired" holds the program that ran and each repair.
- `mitigate` (any): Mitigate the job's expectation values (or, for readout, its counts) under its own noise: zne (the whole program folded U(U†U)^k and extrapolated), pea (the noise learned as a sparse Pauli–Lindblad model and amplified), pec (the learned channels inverted and sampled), cdr (a line fitted over near-Clifford training circuits), vd (two copies), readout (the counts' flips inverted). Needs noise and observables (shots for readout), at most 8 qubits. The result's `mitigation` gives raw and mitigated values with standard errors, the ideal value (known because this is a simulation), the method's bias and variance assumptions and its cost. A method's name, or an object.
- `pulse` (object): A pulse run's options (method "pulse"): the sample period, the levels kept, the tolerance on the stated error, the steps a sample is divided into, each qubit's parameters, static couplings, and ports beyond the names d0, m0, a0.
### `lint_qasm`: Check an OpenQASM program
@ -398,7 +400,7 @@ Parameters:
- `inputs` (object): Values for a generator's integer inputs, as {"n": 5}: the program is read as its family's circuit at those values.
- `shots` (integer): Shots to sample; 1024 when nothing else is asked.
- `seed` (integer): The sampling seed (default 1).
- `method` (string): auto (the default: the strongest method the program admits), or one of list_methods' names.
- `method` (string): auto (the default: the strongest method the program admits), or one of list_methods' names; pulse runs a pulse program (OpenPulse calibrations on physical qubits, see pulse_schedule) under the pulse options, its error bounded.
- `observables` (array): Pauli observables, as "Z0 Z1" or "0.5 X2".
- `amplitudes` (array): Basis states whose amplitudes to return, as bit strings with the highest qubit first.
- `noise` (object): Channel probabilities
@ -408,6 +410,7 @@ Parameters:
- `intent` (any): What the program must do, checked by the engine with the run: the result's "verdict" states how far it checked (proof, evidence, executed, parsed, refused) and whether the intent holds, sealed by the receipt.
- `repair` (any): Repair the program first, on the lines its diagnostics flag alone (as repair_qasm does): true, or the choices {"max_iterations": N, "dyadic_bits": K, "sizes": {"q": 3}}. The result's "repaired" holds the program that ran and each repair.
- `mitigate` (any): Mitigate the job's expectation values (or, for readout, its counts) under its own noise: zne (the whole program folded U(U†U)^k and extrapolated), pea (the noise learned as a sparse Pauli–Lindblad model and amplified), pec (the learned channels inverted and sampled), cdr (a line fitted over near-Clifford training circuits), vd (two copies), readout (the counts' flips inverted). Needs noise and observables (shots for readout), at most 8 qubits. The result's `mitigation` gives raw and mitigated values with standard errors, the ideal value (known because this is a simulation), the method's bias and variance assumptions and its cost. A method's name, or an object.
- `pulse` (object): A pulse run's options (method "pulse"): the sample period, the levels kept, the tolerance on the stated error, the steps a sample is divided into, each qubit's parameters, static couplings, and ports beyond the names d0, m0, a0.
### `qec_code`: Show an error-correcting code and certify its distance
@ -450,6 +453,22 @@ Parameters:
- `rounds` (integer): surface: rounds (default the distance).
- `seed` (integer)
### `pulse_schedule`: Read a pulse program into its schedule
Read a pulse program (OpenPulse calibrations: ports, frames, waveforms, play, delay, shift_phase, set_phase, shift_frequency, set_frequency, captures and barriers) into its schedule on frames: every instruction at its sample, each waveform's samples (gaussian, drag, gaussian_square, constant, sech, sine, or explicit), each gate call with the samples its calibration spans, and the digests a pulse run's receipt binds (the program, its calibrations, the schedule). Durations are whole samples of dt (pulse.dt_ns, default 0.5 ns) or refused. A construct the reader does not take is refused with a Q code and its line. Sealed with this user's key. To run it, give the program to run_quantum_circuit with method "pulse".
Parameters:
- `qasm` (string, required): A pulse program: OpenQASM 3 with `defcalgrammar "openpulse";`, `cal` blocks declaring ports (d0 drives qubit 0, m0 is its measurement port, a0 its acquisition port), frames (newframe(port, frequency Hz, phase)) and waveforms, a `defcal` for every gate it calls on physical qubits ($0), and the calls.
- `pulse` (object): A pulse run's options (method "pulse"): the sample period, the levels kept, the tolerance on the stated error, the steps a sample is divided into, each qubit's parameters, static couplings, and ports beyond the names d0, m0, a0.
### `pulse_check`: Check a pulse run by running it again
Check a pulse job as a host returned it ({"request", "result"}, the result with its receipt): the receipt's signature, the program's digest against the request's text, and the request run again here with the result held to the one given field by field (the density matrix's digest among them), each difference named. A pulse run is the same bits on every platform, so a result that does not reproduce was not made from that request by this engine. Sealed with this user's key. Quote `verified` and the statement.
Parameters:
- `request` (object, required): The pulse job's request.
- `result` (object, required): Its result, with its receipt.
### `providers_list`: List the person's quantum providers
The person's own quantum providers, as their registry lists them: each one's name, kind, protocol, endpoint or command, and where its credential is kept (never the credential itself). With free: true, also what anyone can use at no cost: the engine's methods on this machine with their limits, the free job service, and each provider whose own description states free use (this asks each provider to describe itself).
@ -494,7 +513,7 @@ Parameters:
- `observables` (array): Pauli strings to take expectation values of, e.g. ["Z0", "Z0 Z1", "X2 Y3"]. Qubits count from 0.
- `amplitudes` (array): Basis states to give amplitudes of: one character per qubit, qubit n-1 first, e.g. "0011".
- `noise` (object): Optional noise, each a probability: depolarizing_1q, depolarizing_2q, amplitude_damping, dephasing (after each gate on its qubits), readout (each measured bit).
- `method` (string, one of auto, exact, statevector, statevector_single, stabilizer, pauli, mps, tensor_network, cutrank, cutrank_single, density_matrix, trajectories): Leave as auto unless you need a particular method.
- `method` (string, one of auto, exact, statevector, statevector_single, stabilizer, pauli, mps, tensor_network, cutrank, cutrank_single, density_matrix, trajectories, pulse): Leave as auto unless you need a particular method; pulse runs a pulse program (OpenPulse calibrations on physical qubits, see pulse_schedule) under the pulse options, its error bounded.
- `seed` (integer): Sampling seed (default 1); the same job and seed give the same counts.
- `options` (object): Advanced, per method: max_bond (mps), threshold and max_terms (pauli), fusion (statevector), trajectories.
- `compile` (object): Compile the program first (as compile_qasm does) and run the compiled circuit: with basis clifford_t it runs bit-exact, and the result's `compiled` states its distance from the program.
@ -502,6 +521,7 @@ Parameters:
- `intent` (any): What the program must do, checked by the engine with the run: the result's `verdict` states how far it checked (proof, evidence, executed, parsed, refused) and whether the intent holds, sealed by the receipt.
- `repair` (any): Repair the program first, on the lines its diagnostics flag alone (as repair_qasm does): true, or the choices {"max_iterations": N, "dyadic_bits": K, "sizes": {"q": 3}}. The result's `repaired` holds the program that ran and each repair.
- `mitigate` (any): Mitigate the job's expectation values (or, for readout, its counts) under its own noise: zne (the whole program folded U(U†U)^k and extrapolated), pea (the noise learned as a sparse Pauli–Lindblad model and amplified), pec (the learned channels inverted and sampled), cdr (a line fitted over near-Clifford training circuits), vd (two copies), readout (the counts' flips inverted). Needs noise and observables (shots for readout), at most 8 qubits. The result's `mitigation` gives raw and mitigated values with standard errors, the ideal value (known because this is a simulation), the method's bias and variance assumptions and its cost. A method's name, or an object.
- `pulse` (object): A pulse run's options (method "pulse"): the sample period, the levels kept, the tolerance on the stated error, the steps a sample is divided into, each qubit's parameters, static couplings, and ports beyond the names d0, m0, a0.
### `run_quantum_set`: Run a set of quantum programs, or one over a grid
@ -513,7 +533,7 @@ Parameters:
- `observables` (array): Pauli strings to take expectation values of, e.g. ["Z0", "Z0 Z1", "X2 Y3"]. Qubits count from 0.
- `amplitudes` (array): Basis states to give amplitudes of: one character per qubit, qubit n-1 first, e.g. "0011".
- `noise` (object): Optional noise, each a probability: depolarizing_1q, depolarizing_2q, amplitude_damping, dephasing (after each gate on its qubits), readout (each measured bit).
- `method` (string, one of auto, exact, statevector, statevector_single, stabilizer, pauli, mps, tensor_network, cutrank, cutrank_single, density_matrix, trajectories): Leave as auto unless you need a particular method.
- `method` (string, one of auto, exact, statevector, statevector_single, stabilizer, pauli, mps, tensor_network, cutrank, cutrank_single, density_matrix, trajectories, pulse): Leave as auto unless you need a particular method; pulse runs a pulse program (OpenPulse calibrations on physical qubits, see pulse_schedule) under the pulse options, its error bounded.
- `seed` (integer): Sampling seed (default 1); the same job and seed give the same counts.
- `options` (object): Advanced, per method: max_bond (mps), threshold and max_terms (pauli), fusion (statevector), trajectories.
- `compile` (object): Compile the program first (as compile_qasm does) and run the compiled circuit: with basis clifford_t it runs bit-exact, and the result's `compiled` states its distance from the program.
@ -521,6 +541,7 @@ Parameters:
- `intent` (any): What the program must do, checked by the engine with the run: the result's `verdict` states how far it checked (proof, evidence, executed, parsed, refused) and whether the intent holds, sealed by the receipt.
- `repair` (any): Repair the program first, on the lines its diagnostics flag alone (as repair_qasm does): true, or the choices {"max_iterations": N, "dyadic_bits": K, "sizes": {"q": 3}}. The result's `repaired` holds the program that ran and each repair.
- `mitigate` (any): Mitigate the job's expectation values (or, for readout, its counts) under its own noise: zne (the whole program folded U(U†U)^k and extrapolated), pea (the noise learned as a sparse Pauli–Lindblad model and amplified), pec (the learned channels inverted and sampled), cdr (a line fitted over near-Clifford training circuits), vd (two copies), readout (the counts' flips inverted). Needs noise and observables (shots for readout), at most 8 qubits. The result's `mitigation` gives raw and mitigated values with standard errors, the ideal value (known because this is a simulation), the method's bias and variance assumptions and its cost. A method's name, or an object.
- `pulse` (object): A pulse run's options (method "pulse"): the sample period, the levels kept, the tolerance on the stated error, the steps a sample is divided into, each qubit's parameters, static couplings, and ports beyond the names d0, m0, a0.
- `programs` (array): The set's programs: each a program's text, or a job ({"qasm", …}) whose own fields win over the set's.
- `sweep` (object): Each input of qasm and its values: every combination runs, the names in sorted order, the first varying slowest; an integer input's values are whole numbers.
@ -534,7 +555,7 @@ Parameters:
- `observables` (array): Pauli strings to take expectation values of, e.g. ["Z0", "Z0 Z1", "X2 Y3"]. Qubits count from 0.
- `amplitudes` (array): Basis states to give amplitudes of: one character per qubit, qubit n-1 first, e.g. "0011".
- `noise` (object): Optional noise, each a probability: depolarizing_1q, depolarizing_2q, amplitude_damping, dephasing (after each gate on its qubits), readout (each measured bit).
- `method` (string, one of auto, exact, statevector, statevector_single, stabilizer, pauli, mps, tensor_network, cutrank, cutrank_single, density_matrix, trajectories): Leave as auto unless you need a particular method.
- `method` (string, one of auto, exact, statevector, statevector_single, stabilizer, pauli, mps, tensor_network, cutrank, cutrank_single, density_matrix, trajectories, pulse): Leave as auto unless you need a particular method; pulse runs a pulse program (OpenPulse calibrations on physical qubits, see pulse_schedule) under the pulse options, its error bounded.
- `seed` (integer): Sampling seed (default 1); the same job and seed give the same counts.
- `options` (object): Advanced, per method: max_bond (mps), threshold and max_terms (pauli), fusion (statevector), trajectories.
- `compile` (object): Compile the program first (as compile_qasm does) and run the compiled circuit: with basis clifford_t it runs bit-exact, and the result's `compiled` states its distance from the program.
@ -542,6 +563,7 @@ Parameters:
- `intent` (any): What the program must do, checked by the engine with the run: the result's `verdict` states how far it checked (proof, evidence, executed, parsed, refused) and whether the intent holds, sealed by the receipt.
- `repair` (any): Repair the program first, on the lines its diagnostics flag alone (as repair_qasm does): true, or the choices {"max_iterations": N, "dyadic_bits": K, "sizes": {"q": 3}}. The result's `repaired` holds the program that ran and each repair.
- `mitigate` (any): Mitigate the job's expectation values (or, for readout, its counts) under its own noise: zne (the whole program folded U(U†U)^k and extrapolated), pea (the noise learned as a sparse Pauli–Lindblad model and amplified), pec (the learned channels inverted and sampled), cdr (a line fitted over near-Clifford training circuits), vd (two copies), readout (the counts' flips inverted). Needs noise and observables (shots for readout), at most 8 qubits. The result's `mitigation` gives raw and mitigated values with standard errors, the ideal value (known because this is a simulation), the method's bias and variance assumptions and its cost. A method's name, or an object.
- `pulse` (object): A pulse run's options (method "pulse"): the sample period, the levels kept, the tolerance on the stated error, the steps a sample is divided into, each qubit's parameters, static couplings, and ports beyond the names d0, m0, a0.
- `programs` (array): The set's programs: each a program's text, or a job ({"qasm", …}) whose own fields win over the set's.
- `sweep` (object): Each input of qasm and its values: every combination runs, the names in sorted order, the first varying slowest; an integer input's values are whole numbers.
@ -660,13 +682,14 @@ Parameters:
- `observables` (array): Pauli strings to take expectation values of, e.g. ["Z0", "Z0 Z1", "X2 Y3"]. Qubits count from 0.
- `amplitudes` (array): Basis states to give amplitudes of: one character per qubit, qubit n-1 first, e.g. "0011".
- `noise` (object): Optional noise, each a probability: depolarizing_1q, depolarizing_2q, amplitude_damping, dephasing (after each gate on its qubits), readout (each measured bit).
- `mitigate` (any): Mitigate the job's expectation values (or, for readout, its counts) under its own noise: zne (the whole program folded U(U†U)^k and extrapolated), pea (the noise learned as a sparse Pauli–Lindblad model and amplified), pec (the learned channels inverted and sampled), cdr (a line fitted over near-Clifford training circuits), vd (two copies), readout (the counts' flips inverted). Needs noise and observables (shots for readout), at most 8 qubits. The result's `mitigation` gives raw and mitigated values with standard errors, the ideal value (known because this is a simulation), the method's bias and variance assumptions and its cost. A method's name, or an object.
- `pulse` (object): A pulse run's options (method "pulse"): the sample period, the levels kept, the tolerance on the stated error, the steps a sample is divided into, each qubit's parameters, static couplings, and ports beyond the names d0, m0, a0.
- `seed` (integer): Sampling seed (default 1); the same job and seed give the same counts.
- `options` (object): Advanced, per method: max_bond (mps), threshold and max_terms (pauli), fusion (statevector), trajectories.
- `compile` (object): Compile the program first (as compile_qasm does) and run the compiled circuit: with basis clifford_t it runs bit-exact, and the result's `compiled` states its distance from the program.
- `target` (any, required): A device target: a built-in target's name (list_targets) or a target document (wai.quantum.target/1). The program is then compiled to its gates, placed where it is most likely to succeed and simulated under its noise, its hash in the receipt; not with noise or amplitudes. The service reads the document given and fetches nothing.
- `intent` (any): What the program must do, checked by the engine with the run: the result's `verdict` states how far it checked (proof, evidence, executed, parsed, refused) and whether the intent holds, sealed by the receipt.
- `repair` (any): Repair the program first, on the lines its diagnostics flag alone (as repair_qasm does): true, or the choices {"max_iterations": N, "dyadic_bits": K, "sizes": {"q": 3}}. The result's `repaired` holds the program that ran and each repair.
- `mitigate` (any): Mitigate the job's expectation values (or, for readout, its counts) under its own noise: zne (the whole program folded U(U†U)^k and extrapolated), pea (the noise learned as a sparse Pauli–Lindblad model and amplified), pec (the learned channels inverted and sampled), cdr (a line fitted over near-Clifford training circuits), vd (two copies), readout (the counts' flips inverted). Needs noise and observables (shots for readout), at most 8 qubits. The result's `mitigation` gives raw and mitigated values with standard errors, the ideal value (known because this is a simulation), the method's bias and variance assumptions and its cost. A method's name, or an object.
### `quantum_circuit_examples`: Ready-to-run quantum examples
@ -713,6 +736,22 @@ Parameters:
- `rounds` (integer): surface: rounds (default the distance).
- `seed` (integer)
### `pulse_schedule`: Read a pulse program into its schedule
Read a pulse program (OpenPulse calibrations: ports, frames, waveforms, play, delay, shift_phase, set_phase, shift_frequency, set_frequency, captures and barriers) into its schedule on frames: every instruction at its sample, each waveform's samples (gaussian, drag, gaussian_square, constant, sech, sine, or explicit), each gate call with the samples its calibration spans, and the digests a pulse run's receipt binds (the program, its calibrations, the schedule). Durations are whole samples of dt (pulse.dt_ns, default 0.5 ns) or refused. A construct the reader does not take is refused with a Q code and its line. Sealed with the service's key. To run it, give the program to run_quantum_circuit with method "pulse".
Parameters:
- `qasm` (string, required): A pulse program: OpenQASM 3 with `defcalgrammar "openpulse";`, `cal` blocks declaring ports (d0 drives qubit 0, m0 is its measurement port, a0 its acquisition port), frames (newframe(port, frequency Hz, phase)) and waveforms, a `defcal` for every gate it calls on physical qubits ($0), and the calls.
- `pulse` (object): A pulse run's options (method "pulse"): the sample period, the levels kept, the tolerance on the stated error, the steps a sample is divided into, each qubit's parameters, static couplings, and ports beyond the names d0, m0, a0.
### `pulse_check`: Check a pulse run by running it again
Check a pulse job as a host returned it ({"request", "result"}, the result with its receipt): the receipt's signature, the program's digest against the request's text, and the request run again here with the result held to the one given field by field (the density matrix's digest among them), each difference named. A pulse run is the same bits on every platform, so a result that does not reproduce was not made from that request by this engine. Sealed with the service's key. Quote `verified` and the statement.
Parameters:
- `request` (object, required): The pulse job's request.
- `result` (object, required): Its result, with its receipt.
## Resources, on both servers
### `wai-quantum://docs/language`: the OpenQASM the engine reads
@ -864,9 +903,15 @@ Each construct below is supported (read as the language defines it), lowered (re
| Construct | Status | Codes |
|---|---|---|
| `defcalgrammar "openpulse";` | preserved | Q452 |
| `cal { … }` | preserved, never run | Q451 |
| `defcal …` | preserved, never run; the gate's matrix comes from its definition or the library | Q450, Q126 |
| `defcalgrammar "openpulse";` | preserved; in a pulse run, read (another grammar is refused) | Q452, Q454 |
| `cal { … }` | preserved, never run by the circuit methods; in a pulse run, read as OpenPulse and run in order | Q451, Q453 |
| `defcal …` | preserved, never run by the circuit methods; the gate's matrix comes from its definition or the library. In a pulse run, read as OpenPulse and run at each call on its physical qubits | Q450, Q126, Q453, Q459 |
| Ports, frames, waveforms (`constant`, `gaussian`, `sech`, `gaussian_square`, `drag`, `sine`, explicit samples) | in a pulse run, lowered to the schedule | Q453, Q455 to Q458 |
| `play`, `delay`, `shift_phase`, `set_phase`, `shift_frequency`, `set_frequency`, `capture_v0` to `capture_v4`, `barrier` | in a pulse run, lowered to the schedule on frames | Q453, Q457, Q458, Q461 |
| Plays on a measurement port | in a pulse run, scheduled and timed; the readout is not simulated | Q462 |
A pulse run is the job method `pulse`; [the pulse page](pulse.md) gives the
model, the bound and the options.
#### The bit-exact method
@ -1008,6 +1053,16 @@ Every code:
| `Q450` | preserved | `defcal`: kept verbatim for export, never executed; the gate's matrix comes from its `gate` definition or the standard library. |
| `Q451` | preserved | `cal { … }`: kept verbatim for export, never executed. |
| `Q452` | preserved | `defcalgrammar`: kept verbatim for export. |
| `Q453` | lowered | In a pulse run, a `cal` or `defcal` body read as OpenPulse and lowered to the schedule on frames (plays, delays, phase and frequency changes, captures and barriers) that the pulse simulator integrates. |
| `Q454` | refused | In a pulse run, a `defcalgrammar` other than `"openpulse"`: the pulse reader reads OpenPulse alone. |
| `Q455` | refused | A statement in a calibration that the pulse reader does not take: classical control, a frame's attributes or `get_phase`, an `extern` function, a frame made inside a `defcal`, a nested calibration, or a `defcal` over qubit arguments (the message names it and what to write instead). |
| `Q456` | refused | A waveform that does not read: a template the reader does not know, arguments of the wrong number or kind, a length or a width that is not positive, a sample that is not a number, or more samples than it holds. |
| `Q457` | refused | A frame or a port that does not read: a play, a delay or a phase or frequency change on a name that is not a frame, `newframe` on a name that is not a port, a frequency that is not a number, or a port that reaches no qubit. |
| `Q458` | refused | A duration in a calibration that is not a whole, non-negative number of samples at the sample period (`dt`), or a schedule longer than the reader holds. |
| `Q459` | refused | In a pulse run, a gate or a measurement on physical qubits that no `defcal` calibrates for those qubits and those parameters. |
| `Q460` | refused | In a pulse run, a statement outside the calibrations that the pulse reader does not take: declared qubits (a pulse program names physical qubits, `$0`), a modifier, `reset`, classical control, a subroutine, or a measurement into something other than a bit. |
| `Q461` | refused | A capture that does not read: not on a frame of an acquisition port, without a duration or a waveform, or its result put somewhere other than a bit. |
| `Q462` | warned | A play on a measurement port: scheduled and timed, its effect on the readout not simulated (a capture reads the qubit's state). |
| `Q500` | refused | An angle that is not a dyadic multiple of 2π (2πm/2^k, k ≤ 32): the bit-exact method refuses it, never rounds it; the methods in double precision take it. |
| `Q501` | refused | More than 255 qubits for the bit-exact circuit. |
| `Q502` | refused | A program that measures a qubit and then acts on it, resets one after its first gate, or acts on measured bits: the bit-exact circuit takes programs whose measurements all come at the end. |
@ -1029,6 +1084,7 @@ Every code:
| `cutrank_single` | single | as for cutrank; the table on a GPU, its arithmetic in single precision | counts, amplitudes |
| `density_matrix` | exact | any, with noise | counts, expectations |
| `trajectories` | estimate | any, with noise | counts, expectations |
| `pulse` | bounded | pulse programs: OpenPulse calibrations (cal, defcal) on physical qubits, every gate called with its calibration | counts, populations, captures |
The accuracy classes:
@ -1351,6 +1407,9 @@ Commands:
qec Error correction: a code and its certified distance, a syndrome or a circuit's
detectors decoded, a threshold measured, each answer signed (`wai-quantum qec
--help`)
pulse Pulse programs: a program's schedule on frames drawn as a timeline, and a pulse job
checked by running it again; a pulse program runs with `run --method pulse`
(`wai-quantum pulse --help`)
eval Score a model on a task set with the engine: whether its program reads, the verdict's
level and whether the intent holds, and the joules per solved task where its server's
energy is metered; or write each task's answer three ways for a token count
@ -1484,6 +1543,10 @@ Options:
pea, pec, cdr, vd or readout, or its options as JSON ({"method": "pea", "factors": [1,
1.5, 2]})
--pulse <JSON|FILE>
A pulse run's options (with --method pulse), as JSON or a file of it: dt_ns, levels,
tolerance, substeps, qubits, couplings and ports (`wai-quantum pulse --help`)
--json
Print the job as the service returns it: request, plan, result and receipt
@ -1610,6 +1673,10 @@ Options:
pea, pec, cdr, vd or readout, or its options as JSON ({"method": "pea", "factors": [1,
1.5, 2]})
--pulse <JSON|FILE>
A pulse run's options (with --method pulse), as JSON or a file of it: dt_ns, levels,
tolerance, substeps, qubits, couplings and ports (`wai-quantum pulse --help`)
--json
Print the plan as JSON
@ -2235,6 +2302,10 @@ Options:
pea, pec, cdr, vd or readout, or its options as JSON ({"method": "pea", "factors": [1,
1.5, 2]})
--pulse <JSON|FILE>
A pulse run's options (with --method pulse), as JSON or a file of it: dt_ns, levels,
tolerance, substeps, qubits, couplings and ports (`wai-quantum pulse --help`)
--json
Print the job as the service returns it: request, plan, result and receipt
@ -4762,6 +4833,121 @@ Options:
Print version
```
### `wai-quantum pulse`
```text
Pulse programs: a program's schedule on frames drawn as a timeline, and a pulse job checked by
running it again; a pulse program runs with `run --method pulse` (`wai-quantum pulse --help`)
Usage: wai-quantum pulse [OPTIONS] <COMMAND>
Commands:
schedule A pulse program's schedule: each frame's timeline, every instruction at its sample, the
waveforms and the gate calls
check A pulse job (`run --method pulse --json`) checked: its receipt, its program's digest,
and the request run again here, its result held to the one given
help Print this message or the help of the given subcommand(s)
Options:
--input <NAME=VALUE>
A value for a generator's integer `input`, as n=5 (repeat for each input): the program is
read as its family's circuit at those values
--lang <LOCALE>
The language of the words the command line writes itself, en or zh-CN (by default the one
LANG names); results, codes and the engine's own messages stay in English
-h, --help
Print help
-V, --version
Print version
```
### `wai-quantum pulse schedule`
```text
A pulse program's schedule: each frame's timeline, every instruction at its sample, the waveforms
and the gate calls
Usage: wai-quantum pulse schedule [OPTIONS] [FILE]
Arguments:
[FILE]
The program; `-` or nothing reads standard input
Options:
--input <NAME=VALUE>
A value for a generator's integer `input`, as n=5 (repeat for each input): the program is
read as its family's circuit at those values
--pulse <JSON|FILE>
The pulse options, as JSON or a file of it (only dt_ns and ports matter here)
--lang <LOCALE>
The language of the words the command line writes itself, en or zh-CN (by default the one
LANG names); results, codes and the engine's own messages stay in English
--width <WIDTH>
The timeline's width, in columns
[default: 60]
--json
Print the answer sealed with this user's key, as JSON (`wai-quantum verify` checks it)
--key <FILE>
Sign with the key in this file (64 hex digits)
--demo-key
Sign with the published demo key: the receipt then shows a result is unaltered, not who
signed it
-h, --help
Print help
-V, --version
Print version
```
### `wai-quantum pulse check`
```text
A pulse job (`run --method pulse --json`) checked: its receipt, its program's digest, and the
request run again here, its result held to the one given
Usage: wai-quantum pulse check [OPTIONS] [FILE]
Arguments:
[FILE]
The job, as `run --json` or the service returns it; `-` reads standard input
Options:
--input <NAME=VALUE>
A value for a generator's integer `input`, as n=5 (repeat for each input): the program is
read as its family's circuit at those values
--json
Print the answer sealed with this user's key, as JSON (`wai-quantum verify` checks it)
--key <FILE>
Sign with the key in this file (64 hex digits)
--lang <LOCALE>
The language of the words the command line writes itself, en or zh-CN (by default the one
LANG names); results, codes and the engine's own messages stay in English
--demo-key
Sign with the published demo key: the receipt then shows a result is unaltered, not who
signed it
-h, --help
Print help
-V, --version
Print version
```
### `wai-quantum eval`
```text

View file

@ -79,6 +79,8 @@ Runs on this machine, signed with the user's key; nothing leaves the machine.
- `qec_code`: Show an error-correcting code and certify its distance
- `qec_decode`: Decode a syndrome or a circuit's detectors
- `qec_threshold`: Measure logical failure rates over distances
- `pulse_schedule`: Read a pulse program into its schedule
- `pulse_check`: Check a pulse run by running it again
- `providers_list`: List the person's quantum providers
- `provider_estimate`: Plan a run on a provider, and its cost
- `run_on_provider`: Run on a provider (can spend the person's money)
@ -107,6 +109,8 @@ Runs on this machine, signed with the user's key; nothing leaves the machine.
- `qec_code`: Show an error-correcting code and certify its distance
- `qec_decode`: Decode a syndrome or a circuit's detectors
- `qec_threshold`: Measure logical failure rates over distances
- `pulse_schedule`: Read a pulse program into its schedule
- `pulse_check`: Check a pulse run by running it again
## Resources and prompts, on both servers

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -48,6 +48,7 @@
"compile": { "source": "Compile a program: passes each checked where they act, Clifford+T within an ε budget (or the standard gates), and a signed certificate of every pass and the distance (`wai-quantum compile --help`)", "text": "编译程序:每一遍处理(pass)都在其作用之处经过检查,在 ε 预算内得到 Clifford+T(或标准门),并为每一遍处理和距离出具签名证书(`wai-quantum compile --help`)" },
"estimate": { "source": "What a fault-tolerant machine would need to run a program: its logical counts (T, Toffolis, rotations at their synthesised T counts, depth, qubits with ancillas), physical qubits, runtime and factories, and the frontier between qubits and time, each estimate signed (`wai-quantum estimate --help`)", "text": "容错机器运行某个程序需要什么:逻辑计数(T 门、Toffoli 门、按综合后 T 门数计的旋转、深度、含辅助量子比特的量子比特数)、物理量子比特、运行时间和工厂,以及量子比特与时间之间的权衡前沿,每份估算都附签名(`wai-quantum estimate --help`)" },
"qec": { "source": "Error correction: a code and its certified distance, a syndrome or a circuit's detectors decoded, a threshold measured, each answer signed (`wai-quantum qec --help`)", "text": "纠错:一个码及其经过认证的距离、对校验子或线路探测器的解码、测得的阈值,每个回答都带签名(`wai-quantum qec --help`)" },
"pulse": { "source": "Pulse programs: a program's schedule on frames drawn as a timeline, and a pulse job checked by running it again; a pulse program runs with `run --method pulse` (`wai-quantum pulse --help`)", "text": "脉冲程序:把程序在帧上的时序画成时间线,并通过重新运行来检查脉冲作业;脉冲程序用 `run --method pulse` 运行(`wai-quantum pulse --help`)" },
"repair": { "source": "Repair a program from its diagnostics, on the lines they flag alone: the mechanical fixes, a proposed fix checked against those lines, and what needs a person (exit 0 it reads, 1 something needs a person, 2 a proposal rejected)", "text": "依据诊断修复程序,只改动诊断所标记的行:机械性的修正、对照这些行检查过的修复建议,以及需要人来处理的部分(退出状态 0 可读取,1 有需要人处理的部分,2 建议被拒绝)" },
"submit": { "source": "Send a program, a set or a sweep to the job service, follow its status to the end, and print its result", "text": "把程序、程序集或参数扫描发送给作业服务,跟踪其状态直至结束,并打印其结果" },
"task": { "source": "The job service as a resource an on-premises scheduler hands its batch jobs: describe it, acquire and release a lease, start, follow, read and stop tasks (`wai-quantum task --help`)", "text": "把作业服务作为本地调度器分配给其批处理作业的资源:描述它,获取和释放租约,启动、跟踪、读取和停止任务(`wai-quantum task --help`)" },

View file

@ -42,6 +42,8 @@ Generated by `wai-quantum docs skill` from the servers' own lists; `wai-quantum
- `qec_code`: Show an error-correcting code and certify its distance
- `qec_decode`: Decode a syndrome or a circuit's detectors
- `qec_threshold`: Measure logical failure rates over distances
- `pulse_schedule`: Read a pulse program into its schedule
- `pulse_check`: Check a pulse run by running it again
- `providers_list`: List the person's quantum providers
- `provider_estimate`: Plan a run on a provider, and its cost
- `run_on_provider`: Run on a provider (can spend the person's money)
@ -69,6 +71,8 @@ Generated by `wai-quantum docs skill` from the servers' own lists; `wai-quantum
- `qec_code`: Show an error-correcting code and certify its distance
- `qec_decode`: Decode a syndrome or a circuit's detectors
- `qec_threshold`: Measure logical failure rates over distances
- `pulse_schedule`: Read a pulse program into its schedule
- `pulse_check`: Check a pulse run by running it again
## Resources, on both servers

View file

@ -293,6 +293,37 @@ has "$tmp/lab-qec" "^failures"
has "$tmp/mitigated" "^mitigation zne"
echo "qec and mitigation: ok"
# Pulse: a program's schedule drawn, its run as a job (the error bounded and
# stated), its receipt verified, the job checked by running it again, and a
# tolerance the integrator cannot meet refused with its code.
cat > "$tmp/xm.qasm" <<'EOF'
OPENQASM 3.0;
defcalgrammar "openpulse";
cal {
extern port d0;
extern port a0;
frame q0 = newframe(d0, 5.0e9, 0);
frame c0 = newframe(a0, 5.0e9, 0);
}
defcal x $0 { play(q0, gaussian(0.4, 64dt, 16dt)); }
defcal measure $0 -> bit { return capture_v2(c0, 100dt); }
bit c;
x $0;
c = measure $0;
EOF
"$W" pulse schedule "$tmp/xm.qasm" --demo-key > "$tmp/pulse-schedule" 2> /dev/null
has "$tmp/pulse-schedule" "^schedule 164 samples"
has "$tmp/pulse-schedule" "call x [$]0 samples 0–64"
"$W" run "$tmp/xm.qasm" --method pulse --shots 200 --json --demo-key > "$tmp/pulse-run.json" 2> /dev/null
python3 -c 'import json,sys; r=json.load(open(sys.argv[1]))["result"]; a=r["accuracy"]; assert r["method"]=="pulse" and a["class"]=="bounded" and a["bound"] <= 1e-6 and r["pulse"]["captures"][0]["bit"]=="c" and sum(r["counts"].values())==200, r' "$tmp/pulse-run.json"
"$W" verify "$tmp/pulse-run.json" > "$tmp/pulse-verify"
has "$tmp/pulse-verify" "VALID"
"$W" pulse check "$tmp/pulse-run.json" --demo-key > "$tmp/pulse-check" 2> /dev/null
has "$tmp/pulse-check" "^check VERIFIED"
if "$W" run "$tmp/xm.qasm" --method pulse --pulse '{"tolerance": 1e-15}' --demo-key > "$tmp/pulse-tight" 2>&1; then fail "a tolerance past double precision ran"; fi
has "$tmp/pulse-tight" "\[J041\]"
echo "pulse: ok"
# The front door: a project made, its own test run as made, AGENTS.md left as
# it is by a rerun, the MCP entry, and the server's resources and prompts.
"$W" new "$tmp/proj" --template grover > "$tmp/new"

View file

@ -55,6 +55,9 @@
//! wai-quantum lab … # the research tools
//! wai-quantum providers … # your own quantum providers; run --provider NAME runs on one
//! wai-quantum targets list | import | show # device targets; run --target NAME runs as the device would
//! wai-quantum run rabi.qasm --method pulse # a pulse program (OpenPulse) integrated, its error bounded, the same bits everywhere
//! wai-quantum pulse schedule rabi.qasm # its frames' timelines, plays, waveforms and gate calls
//! wai-quantum pulse check job.json # a pulse job's receipt, its program, and the run again, bit for bit
//! wai-quantum completions zsh # shell completion
//! ```
//!
@ -93,6 +96,7 @@ mod mcp;
mod project;
#[cfg(feature = "providers")]
mod providers;
mod pulse;
mod qec;
mod repair;
mod rewrite;
@ -378,6 +382,11 @@ enum Command {
#[command(subcommand)]
cmd: qec::Cmd,
},
/// Pulse programs: a program's schedule on frames drawn as a timeline, and a pulse job checked by running it again; a pulse program runs with `run --method pulse` (`wai-quantum pulse --help`)
Pulse {
#[command(subcommand)]
cmd: pulse::Cmd,
},
/// Score a model on a task set with the engine: whether its program reads, the verdict's level and whether the intent holds, and the joules per solved task where its server's energy is metered; or write each task's answer three ways for a token count (`wai-quantum eval --help`)
Eval {
#[command(subcommand)]
@ -475,6 +484,9 @@ struct Job {
/// Mitigate the expectation values (or, for readout, the counts) under the job's noise: zne, pea, pec, cdr, vd or readout, or its options as JSON ({"method": "pea", "factors": [1, 1.5, 2]})
#[arg(long, value_name = "METHOD|JSON")]
mitigate: Option<String>,
/// A pulse run's options (with --method pulse), as JSON or a file of it: dt_ns, levels, tolerance, substeps, qubits, couplings and ports (`wai-quantum pulse --help`)
#[arg(long, value_name = "JSON|FILE", value_parser = pulse::options_arg)]
pulse: Option<Value>,
}
/// `name=v1,v2,…`: each value a whole number if it reads as one, else a number.
@ -682,6 +694,9 @@ fn request(job: &Job, qasm: String) -> Value {
if let Some(m) = &job.mitigate {
r["mitigate"] = if m.trim_start().starts_with('{') { serde_json::from_str(m).unwrap_or_else(|_| json!(m)) } else { json!(m) };
}
if let Some(p) = &job.pulse {
r["pulse"] = p.clone();
}
r
}
@ -867,6 +882,9 @@ fn print_job(body: &Value, top: usize) {
println!("from ideal total variation distance {d:.6}, Hellinger fidelity {:.6}", r["ideal"]["hellinger_fidelity"].as_f64().unwrap_or(f64::NAN));
}
}
if r.get("pulse").is_some_and(Value::is_object) {
print!("{}", pulse::report_run(r));
}
for x in r.pointer("/repaired/repairs").and_then(Value::as_array).into_iter().flatten() {
let lines: Vec<String> = x["edits"].as_array().into_iter().flatten().filter_map(|e| e["line"].as_u64()).map(|l| l.to_string()).collect();
println!("repaired {}: {} (line {})", x["code"].as_str().unwrap_or("—"), x["rule"].as_str().unwrap_or(""), lines.join(", "));
@ -1266,6 +1284,7 @@ fn main() {
}
}
Command::Qec { cmd } => qec::main(cmd),
Command::Pulse { cmd } => pulse::main(cmd),
Command::Lab { args } => lab::main(args),
Command::Targets { cmd } => targets::main(cmd),
#[cfg(feature = "providers")]
@ -1331,7 +1350,7 @@ mod tests {
fn job(qasm: &str) -> (Job, String) {
(
Job { file: None, shots: None, seed: None, method: None, observables: vec![], amplitudes: vec![], noise: vec![], options: vec![], memory_gb: 1, history: None, compile: None, target: None, intent: vec![], verdict: false, repair: false, max_iterations: None, programs: vec![], sweep: vec![], mitigate: None },
Job { file: None, shots: None, seed: None, method: None, observables: vec![], amplitudes: vec![], noise: vec![], options: vec![], memory_gb: 1, history: None, compile: None, target: None, intent: vec![], verdict: false, repair: false, max_iterations: None, programs: vec![], sweep: vec![], mitigate: None, pulse: None },
qasm.to_string(),
)
}

View file

@ -91,6 +91,7 @@ fn output_of(tool: &str) -> Option<&'static str> {
"providers_list" => "local-tools.v1.json#/$defs/providers_list",
"provider_estimate" => "local-tools.v1.json#/$defs/provider_estimate",
"qec_code" | "qec_decode" | "qec_threshold" => "qec.v1.json",
"pulse_schedule" | "pulse_check" => "pulse.v1.json",
_ => return None,
})
}
@ -104,7 +105,7 @@ fn job_schema() -> Value {
"inputs": inputs_schema(),
"shots": { "type": "integer", "minimum": 0, "description": "Shots to sample; 1024 when nothing else is asked." },
"seed": { "type": "integer", "minimum": 0, "description": "The sampling seed (default 1)." },
"method": { "type": "string", "description": "auto (the default: the strongest method the program admits), or one of list_methods' names." },
"method": { "type": "string", "description": "auto (the default: the strongest method the program admits), or one of list_methods' names; pulse runs a pulse program (OpenPulse calibrations on physical qubits, see pulse_schedule) under the pulse options, its error bounded." },
"observables": { "type": "array", "items": { "type": "string" }, "description": "Pauli observables, as \"Z0 Z1\" or \"0.5 X2\"." },
"amplitudes": { "type": "array", "items": { "type": "string" }, "description": "Basis states whose amplitudes to return, as bit strings with the highest qubit first." },
"noise": { "type": "object", "description": "Channel probabilities", "properties": catalog::NOISE_CHANNELS.iter().map(|k| (k.to_string(), json!({ "type": "number", "minimum": 0, "maximum": 1 }))).collect::<serde_json::Map<_, _>>(), "additionalProperties": false },
@ -113,7 +114,8 @@ fn job_schema() -> Value {
"target": { "description": "A device target: its name (list_targets) or a target document (wai.quantum.target/1). The program is then compiled to the target's gates, placed where it is most likely to succeed and simulated under the target's noise, the target's hash in the receipt; noise may not be given beside it." },
"intent": intent_schema(),
"repair": { "description": "Repair the program first, on the lines its diagnostics flag alone (as repair_qasm does): true, or the choices {\"max_iterations\": N, \"dyadic_bits\": K, \"sizes\": {\"q\": 3}}. The result's \"repaired\" holds the program that ran and each repair." },
"mitigate": wai_quantum::engine::mitigate::schema()
"mitigate": wai_quantum::engine::mitigate::schema(),
"pulse": wai_quantum::engine::pulse::options_schema()
}
})
}
@ -176,6 +178,7 @@ pub(crate) fn tools() -> Value {
if let Some(a) = t.as_array_mut() {
a.extend(crate::targets::mcp_tools(job_schema()));
a.extend(wai_quantum::engine::qec::tools("this user's key").as_array().cloned().unwrap_or_default());
a.extend(wai_quantum::engine::pulse::tools("this user's key").as_array().cloned().unwrap_or_default());
}
#[cfg(feature = "providers")]
crate::providers::add_tools(&mut t);
@ -464,6 +467,10 @@ fn call(name: &str, args: &Value, memory_gb: u64, key: &SigningKey, client: &Cli
let result = wai_quantum::engine::qec::call(op, args)?;
return Ok(wai_quantum::engine::qec::seal(args, result, key));
}
if let Some(op) = wai_quantum::engine::pulse::op_of_tool(name) {
let result = wai_quantum::engine::pulse::call(op, args)?;
return Ok(wai_quantum::engine::pulse::seal(args, result, key));
}
// A tool that reads a generator at its inputs itself takes them as given.
let raw = args;
// The debugger reads a generator at its inputs itself, so its lines are the generator's.
@ -848,7 +855,7 @@ mod tests {
let list = handle(&json!({ "jsonrpc": "2.0", "id": 2, "method": "tools/list" }), 1, &key(), &mut Client::default()).unwrap();
let names: Vec<&str> = list["result"]["tools"].as_array().unwrap().iter().map(|t| t["name"].as_str().unwrap()).collect();
#[allow(unused_mut)]
let mut want = vec!["run_quantum_circuit", "plan_quantum_circuit", "lint_qasm", "repair_qasm", "critique_qasm", "check_equivalent", "improve_qasm", "classical_shortcuts", "compile_qasm", "estimate_resources", "draw_circuit", "debug_step", "debug_branches", "verify_quantum_result", "discover_evaluate", "discover_submit", "discover_archive", "novelty_qasm", "discover_novelty", "rewrite_qasm", "converge_cohort", "converge_history", "study_measure", "study_cohort", "taste_rank", "taste_next", "history_commit", "history_changes", "history_blame", "history_log", "list_methods", "qasm_diagnostics", "list_targets", "show_target", "run_on_target", "qec_code", "qec_decode", "qec_threshold"];
let mut want = vec!["run_quantum_circuit", "plan_quantum_circuit", "lint_qasm", "repair_qasm", "critique_qasm", "check_equivalent", "improve_qasm", "classical_shortcuts", "compile_qasm", "estimate_resources", "draw_circuit", "debug_step", "debug_branches", "verify_quantum_result", "discover_evaluate", "discover_submit", "discover_archive", "novelty_qasm", "discover_novelty", "rewrite_qasm", "converge_cohort", "converge_history", "study_measure", "study_cohort", "taste_rank", "taste_next", "history_commit", "history_changes", "history_blame", "history_log", "list_methods", "qasm_diagnostics", "list_targets", "show_target", "run_on_target", "qec_code", "qec_decode", "qec_threshold", "pulse_schedule", "pulse_check"];
#[cfg(feature = "providers")]
want.extend(["providers_list", "provider_estimate", "run_on_provider"]);
assert_eq!(names, want);
@ -1330,6 +1337,17 @@ mod tests {
call_tool("run_quantum_circuit", json!({ "qasm": "OPENQASM 3.0;\nqubit[2] q;\nh q[0];\ncx q[0], q[1];\nrz(0.3) q[1];\n", "observables": ["Z0 Z1"], "noise": { "depolarizing_2q": 0.02 }, "mitigate": "zne" }), false, &mut called, &mut hold);
call_tool("run_quantum_circuit", json!({ "qasm": "OPENQASM 3.0;\nqubit[2] q;\nbit[2] c;\nh q[0];\ncx q[0], q[1];\nc = measure q;\n", "shots": 200, "noise": { "readout": 0.05 }, "mitigate": "readout" }), false, &mut called, &mut hold);
call_tool("compile_qasm", json!({ "qasm": "OPENQASM 3.0;\nqubit[3] q;\nh q[0];\ncx q[0], q[1];\ncx q[1], q[2];\nx q[0];\n", "basis": "standard", "twirl": { "seed": 3 }, "dd": "xy4" }), false, &mut called, &mut hold);
// A pulse program: its schedule, its run and plan as a job, the run
// checked by running it again, and refusals with their codes.
let pulse = "OPENQASM 3.0;\ndefcalgrammar \"openpulse\";\ncal {\n extern port d0;\n extern port a0;\n frame q0 = newframe(d0, 5.0e9, 0);\n frame c0 = newframe(a0, 5.0e9, 0);\n}\ndefcal x $0 { play(q0, drag(0.4, 64dt, 16dt, 0.2)); }\ndefcal measure $0 -> bit { return capture_v2(c0, 100dt); }\nbit c;\nx $0;\nc = measure $0;\n";
call_tool("pulse_schedule", json!({ "qasm": pulse, "pulse": { "dt_ns": 0.5 } }), false, &mut called, &mut hold);
call_tool("pulse_schedule", json!({ "qasm": "OPENQASM 3.0;\ncal { play(f, constant(0.1, 1dt)); }\n" }), true, &mut called, &mut hold);
let pulsed = call_tool("run_quantum_circuit", json!({ "qasm": pulse, "method": "pulse", "shots": 100, "pulse": { "levels": 3, "qubits": { "0": { "t1_us": 50, "t2_us": 40 } } } }), false, &mut called, &mut hold);
assert_eq!((pulsed["result"]["method"].as_str(), pulsed["result"]["verdict"]["level"].as_str()), (Some("pulse"), Some("executed")), "{pulsed}");
call_tool("plan_quantum_circuit", json!({ "qasm": pulse, "method": "pulse" }), false, &mut called, &mut hold);
call_tool("run_quantum_circuit", json!({ "qasm": pulse, "method": "pulse", "pulse": { "tolerance": 1e-15 } }), true, &mut called, &mut hold);
let checked = call_tool("pulse_check", json!({ "request": pulsed["request"], "result": pulsed["result"] }), false, &mut called, &mut hold);
assert_eq!(checked["result"]["verified"], true, "{checked}");
call_tool("verify_quantum_result", json!({ "job": est }), false, &mut called, &mut hold);
call_tool("list_targets", json!({}), false, &mut called, &mut hold);
call_tool("show_target", json!({ "target": "synthetic-line-5", "view": true, "document": true }), false, &mut called, &mut hold);

View file

@ -0,0 +1,263 @@
//! `wai-quantum pulse`: pulse programs, as the engine serves them
//! ([`wai_quantum::engine::pulse`]): a program's schedule on frames drawn as
//! a timeline, and a pulse job checked by running it again. A pulse program
//! runs as a job: `wai-quantum run prog.qasm --method pulse`.
//!
//! ```text
//! wai-quantum pulse schedule rabi.qasm # each frame's timeline, its plays and the gate calls
//! wai-quantum pulse schedule rabi.qasm --json # the schedule sealed with this user's key
//! wai-quantum run rabi.qasm --method pulse --pulse '{"levels": 3}' --json > job.json
//! wai-quantum pulse check job.json # its receipt, its program, and the run again, bit for bit
//! ```
use serde_json::{Value, json};
use std::path::PathBuf;
use wai_quantum::engine::{job, pulse};
#[derive(clap::Subcommand)]
pub enum Cmd {
/// A pulse program's schedule: each frame's timeline, every instruction at its sample, the waveforms and the gate calls
Schedule {
/// The program; `-` or nothing reads standard input
file: Option<PathBuf>,
/// The pulse options, as JSON or a file of it (only dt_ns and ports matter here)
#[arg(long, value_name = "JSON|FILE", value_parser = options_arg)]
pulse: Option<Value>,
/// The timeline's width, in columns
#[arg(long, default_value_t = 60)]
width: usize,
#[command(flatten)]
out: super::qec::Out,
},
/// A pulse job (`run --method pulse --json`) checked: its receipt, its program's digest, and the request run again here, its result held to the one given
Check {
/// The job, as `run --json` or the service returns it; `-` reads standard input
file: Option<PathBuf>,
#[command(flatten)]
out: super::qec::Out,
},
}
/// The pulse options a flag gives: JSON, or a file that holds it.
pub fn options_arg(s: &str) -> Result<Value, String> {
let text = if s.trim_start().starts_with('{') { s.to_string() } else { std::fs::read_to_string(s).map_err(|e| format!("{s}: {e}"))? };
let v: Value = serde_json::from_str(&text).map_err(|e| format!("the pulse options are a JSON object: {e}"))?;
if !v.is_object() {
return Err("the pulse options are a JSON object, as {\"dt_ns\": 0.5, \"levels\": 3}".into());
}
Ok(v)
}
fn f(v: &Value) -> f64 {
v.as_f64().unwrap_or(f64::NAN)
}
/// Each frame's timeline as a row of `width` columns: █ where it plays, ▒
/// where it captures, │ at a barrier, · where it idles.
pub fn timeline(s: &Value, width: usize) -> String {
let width = width.clamp(8, 400);
let total = s["duration"].as_u64().unwrap_or(0).max(1);
let col = |t: u64| ((t as u128 * width as u128) / total as u128).min(width as u128 - 1) as usize;
let mut out = String::new();
let frames = s["frames"].as_array().cloned().unwrap_or_default();
let name_w = frames.iter().filter_map(|x| x["name"].as_str()).map(|n| n.chars().count()).max().unwrap_or(1);
for (i, fr) in frames.iter().enumerate() {
let mut row = vec!['·'; width];
let (mut plays, mut caps) = (0, 0);
for ins in s["instructions"].as_array().into_iter().flatten().filter(|x| x["frame"].as_u64() == Some(i as u64)) {
let (start, len) = (ins["start"].as_u64().unwrap_or(0), ins["length"].as_u64().unwrap_or(0));
let mark = match ins["op"].as_str() {
Some("play") => {
plays += 1;
'█'
}
Some("capture") => {
caps += 1;
'▒'
}
Some("barrier") => {
if row[col(start)] == '·' {
row[col(start)] = '│';
}
continue;
}
_ => continue,
};
if len > 0 {
for c in row.iter_mut().take(col(start + len - 1) + 1).skip(col(start)) {
*c = mark;
}
}
}
out += &format!(
"{:<name_w$} {:<4} {:<7} ${:<3} {:>11.6} GHz {} {plays} play{}, {caps} capture{}\n",
fr["name"].as_str().unwrap_or(""),
fr["port"].as_str().unwrap_or(""),
fr["kind"].as_str().unwrap_or(""),
fr["qubit"],
f(&fr["frequency_hz"]) / 1e9,
row.into_iter().collect::<String>(),
if plays == 1 { "" } else { "s" },
if caps == 1 { "" } else { "s" },
);
}
out
}
/// A schedule as a reader takes it in.
pub fn report_schedule(s: &Value, width: usize) -> String {
let dt = f(&s["dt_ns"]);
let mut out = format!(
"schedule {} samples ({} ns) at dt = {dt} ns: {} frames, {} waveforms, {} instructions, {} gate calls\n",
s["duration"],
f(&s["duration_ns"]),
s["frames"].as_array().map_or(0, Vec::len),
s["waveforms"].as_array().map_or(0, Vec::len),
s["instructions"].as_array().map_or(0, Vec::len),
s["calls"].as_array().map_or(0, Vec::len)
);
out += &timeline(s, width);
for c in s["calls"].as_array().into_iter().flatten() {
let qs: Vec<String> = c["qubits"].as_array().into_iter().flatten().map(|q| format!("${q}")).collect();
let ps: Vec<String> = c["params"].as_array().into_iter().flatten().map(|p| format!("{}", f(p))).collect();
let params = if ps.is_empty() { String::new() } else { format!("({})", ps.join(", ")) };
out += &format!("call {}{params} {} samples {}–{} (line {})\n", c["name"].as_str().unwrap_or(""), qs.join(", "), c["start"], c["end"], c["line"]);
}
for w in s["waveforms"].as_array().into_iter().flatten() {
let peak = w["samples"].as_array().into_iter().flatten().map(|x| f(&x[0]).hypot(f(&x[1]))).fold(0.0, f64::max);
out += &format!("waveform {} {} samples, peak |amplitude| {peak:.6}\n", w["name"].as_str().unwrap_or(""), w["length"]);
}
if let Some(d) = s["decimated"].as_str() {
out += &format!("note {d}\n");
}
for d in s["diagnostics"].as_array().into_iter().flatten().filter(|d| d["outcome"] != "lowered") {
out += &format!("{} line {}: {}\n", d["code"].as_str().unwrap_or(""), d["line"], d["message"].as_str().unwrap_or(""));
}
let c = &s["calibration"];
out += &format!("digests program {}, calibrations {}, schedule {}\n", c["program"].as_str().unwrap_or(""), c["calibrations"].as_str().unwrap_or(""), c["schedule"].as_str().unwrap_or(""));
out
}
/// A pulse run's result as `run` prints it, after the counts.
pub fn report_run(r: &Value) -> String {
let p = &r["pulse"];
let i = &p["integrator"];
let mut out = format!(
"pulse {} samples ({} ns) on {}; {} order {}, {} step{} of {} ns a sample, {} steps\n",
p["duration"],
f(&p["duration_ns"]),
p["simulated_qubits"].as_array().into_iter().flatten().map(|q| format!("${q}")).collect::<Vec<_>>().join(", "),
i["name"].as_str().unwrap_or(""),
i["order"],
i["substeps"],
if i["substeps"] == 1 { "" } else { "s" },
f(&i["step_ns"]),
i["steps"]
);
out += &format!("read {}\n", p["read"].as_str().unwrap_or(""));
for c in p["captures"].as_array().into_iter().flatten() {
let to = c["bit"].as_str().map(|b| format!(" into {b}")).unwrap_or_default();
out += &format!("capture ${} on {} at sample {}{to}: P(1) = {:.9}\n", c["qubit"], c["frame"].as_str().unwrap_or(""), c["start"], f(&c["p1"]));
}
for l in p["leakage"].as_array().into_iter().flatten() {
out += &format!("leakage ${}: P(2) = {:.3e}\n", l["qubit"], f(&l["p2"]));
}
for q in p["device"]["qubits"].as_array().into_iter().flatten() {
let t = |k: &str| q.get(k).and_then(Value::as_f64).map_or("none".to_string(), |x| format!("{} µs", x / 1e3));
out += &format!(
"device ${}: {:.6} GHz ({}), Rabi {} MHz ({}), T1 {} ({}), T2 {} ({})\n",
q["qubit"],
f(&q["frequency_hz"]) / 1e9,
q["from"]["frequency"].as_str().unwrap_or(""),
f(&q["rabi_hz"]) / 1e6,
q["from"]["rabi"].as_str().unwrap_or(""),
t("t1_ns"),
q["from"]["t1"].as_str().unwrap_or(""),
t("t2_ns"),
q["from"]["t2"].as_str().unwrap_or("")
);
}
let c = &p["calibration"];
out += &format!("density {}\n", p["density_hash"].as_str().unwrap_or(""));
out += &format!("binds program {}, calibrations {}, schedule {}, device {}\n", c["program"].as_str().unwrap_or(""), c["calibrations"].as_str().unwrap_or(""), c["schedule"].as_str().unwrap_or(""), c["device"].as_str().unwrap_or(""));
if let Some(t) = c.get("target") {
out += &format!("target {} {}{}\n", t["name"].as_str().unwrap_or(""), t["hash"].as_str().unwrap_or(""), t.pointer("/calibration/id").and_then(Value::as_str).map(|id| format!(", calibration {id}")).unwrap_or_default());
}
out
}
/// A check as a reader takes it in.
pub fn report_check(c: &Value) -> String {
let mut out = format!("check {}\n", if c["verified"] == true { "VERIFIED" } else { "NOT VERIFIED" });
out += &format!("signature {}\nprogram {}\nreproduced {}\n", c["signature"], c["program"], c["reproduced"]);
if let Some(d) = c["differences"].as_array().filter(|d| !d.is_empty()) {
out += &format!("differs {}\n", d.iter().filter_map(Value::as_str).collect::<Vec<_>>().join(", "));
}
out += &format!("density given {}, here {}\n", c["density_hash"]["given"].as_str().unwrap_or("none"), c["density_hash"]["recomputed"].as_str().unwrap_or("none"));
out += &format!("{}\n", c["statement"].as_str().unwrap_or(""));
out
}
pub fn main(cmd: Cmd) {
let r = (|| -> Result<bool, String> {
let (op, req, out, width) = match &cmd {
Cmd::Schedule { file, pulse, width, out } => {
let mut req = json!({ "qasm": super::read_text(file.as_deref())? });
if let Some(p) = pulse {
req["pulse"] = p.clone();
}
("schedule", req, out, *width)
}
Cmd::Check { file, out } => {
let text = super::read_text(file.as_deref())?;
let v: Value = serde_json::from_str(&text).map_err(|e| format!("a pulse job is JSON, as `run --method pulse --json` writes it: {e}"))?;
("check", v, out, 0)
}
};
let result = pulse::call(op, &req)?;
let verified = op != "check" || result["verified"] == true;
let k = super::load_key(&out.key);
let body = pulse::seal(&req, result, &k.key);
if out.json {
println!("{body}");
return Ok(verified);
}
print!("{}", if op == "schedule" { report_schedule(&body["result"], width) } else { report_check(&body["result"]) });
let ok = job::verify(&body).map(|(ok, _)| ok).unwrap_or(false);
let signer = body["result"]["receipt"]["key"].as_str().and_then(job::did_of).unwrap_or_default();
println!("receipt {}, signed by {signer}", if ok { "verifies" } else { "DOES NOT VERIFY" });
Ok(verified)
})();
match r {
Ok(true) => {}
Ok(false) => std::process::exit(1),
Err(e) => super::die(&e),
}
}
#[cfg(test)]
mod tests {
use super::*;
use clap::Parser;
const XM: &str = "OPENQASM 3.0;\ndefcalgrammar \"openpulse\";\ncal {\n extern port d0;\n extern port a0;\n frame q0 = newframe(d0, 5.0e9, 0);\n frame c0 = newframe(a0, 5.0e9, 0);\n}\ndefcal x $0 { play(q0, gaussian(0.4, 64dt, 16dt)); }\ndefcal measure $0 -> bit { return capture_v2(c0, 100dt); }\nbit c;\nx $0;\nc = measure $0;\n";
#[test]
fn a_schedule_is_drawn_and_a_pulse_run_checks() {
let key = wai_quantum::ed25519_dalek::SigningKey::from_bytes(&[5; 32]);
let s = pulse::call("schedule", &json!({ "qasm": XM })).unwrap();
let text = report_schedule(&s, 40);
assert!(text.contains("164 samples") && text.contains("call x $0 samples 0–64") && text.contains("█") && text.contains("▒"), "{text}");
// The run, as `run --method pulse` makes it, and its check.
let body = job::run(&json!({ "qasm": XM, "method": "pulse", "shots": 100 }), 1 << 30, &key, json!({ "unmetered": "a test" })).unwrap();
let printed = report_run(&body["result"]);
assert!(printed.contains("capture $0 on c0 at sample 64 into c") && printed.contains("binds program"), "{printed}");
let c = pulse::call("check", &body).unwrap();
assert_eq!(c["verified"], true, "{c}");
assert!(report_check(&c).starts_with("check VERIFIED"));
// The flags read.
let cli = super::super::Cli::try_parse_from(["wai-quantum", "pulse", "schedule", "p.qasm", "--pulse", "{\"dt_ns\": 0.25}", "--width", "30"]).unwrap();
assert!(matches!(cli.command, super::super::Command::Pulse { cmd: Cmd::Schedule { width: 30, .. } }));
assert!(options_arg("[1]").is_err() && options_arg("{\"levels\": 3}").is_ok());
}
}

View file

@ -358,6 +358,20 @@ fn qec_answer(op: &str, args: &str, key: &SigningKey) -> String {
read_request(args).and_then(|req| wai_quantum::engine::qec::call(op, &req).map(|r| wai_quantum::engine::qec::seal(&req, r, key))).map(|v| v.to_string()).unwrap_or_else(error_json)
}
/// A pulse call (`op`: schedule or check, with its arguments as JSON), its
/// answer sealed with this user's key, off the window's thread. A pulse run
/// is a job: `run_job` with `"method": "pulse"`.
#[tauri::command]
async fn pulse(studio: tauri::State<'_, Studio>, op: String, args: String) -> Result<String, String> {
let key = studio.key.clone();
tauri::async_runtime::spawn_blocking(move || pulse_answer(&op, &args, &key)).await.map_err(|e| e.to_string())
}
/// [`pulse`]'s answer, as JSON text: the sealed answer, or `{"error"}`.
fn pulse_answer(op: &str, args: &str, key: &SigningKey) -> String {
read_request(args).and_then(|req| wai_quantum::engine::pulse::call(op, &req).map(|r| wai_quantum::engine::pulse::seal(&req, r, key))).map(|v| v.to_string()).unwrap_or_else(error_json)
}
/// An estimate's seal, every receipt, and its request re-run here, off the
/// window's thread.
#[tauri::command]
@ -950,7 +964,7 @@ fn main() {
.plugin(tauri_plugin_updater::Builder::new().build())
.manage(Studio { key, job_bytes: job_bytes(), running: Arc::new(Mutex::new(None)), cancelled: Arc::new(Mutex::new(false)) })
.manage(PendingUpdate(Mutex::new(None)))
.invoke_handler(tauri::generate_handler![run_job, cancel, plan_job, methods, lint, critique, equivalent, variants, shortcuts, compile, verdict, repair, course, course_lesson, course_check, course_hint, estimate, estimate_verify, qec, draw, layout, debug, visual_edit, picture, verify, signer, save_job, save_program, chat, models, discover_start, discover_submit, discover_search, discover_evaluate, discover_novelty, instantiate, history, locks, novelty, rewrite, taste, converge, study, providers_list, provider_estimate, run_on_provider, targets, target, target_place, answer, telemetry, update_check, update_install])
.invoke_handler(tauri::generate_handler![run_job, cancel, plan_job, methods, lint, critique, equivalent, variants, shortcuts, compile, verdict, repair, course, course_lesson, course_check, course_hint, estimate, estimate_verify, qec, pulse, draw, layout, debug, visual_edit, picture, verify, signer, save_job, save_program, chat, models, discover_start, discover_submit, discover_search, discover_evaluate, discover_novelty, instantiate, history, locks, novelty, rewrite, taste, converge, study, providers_list, provider_estimate, run_on_provider, targets, target, target_place, answer, telemetry, update_check, update_install])
.run(tauri::generate_context!())
.expect("the window opens");
}
@ -1197,6 +1211,17 @@ mod tests {
}
}
#[test]
fn a_pulse_schedule_is_sealed_with_the_users_key() {
let key = SigningKey::from_bytes(&[4; 32]);
let src = "OPENQASM 3.0;\ncal { extern port d0; frame q0 = newframe(d0, 5.0e9, 0); }\ndefcal x $0 { play(q0, gaussian(0.4, 64dt, 16dt)); }\nx $0;\n";
let out: Value = serde_json::from_str(&pulse_answer("schedule", &json!({ "qasm": src }).to_string(), &key)).unwrap();
assert!(job::verify(&out).unwrap().0, "{out}");
assert_eq!(out["result"]["duration"], 64, "{out}");
let bad: Value = serde_json::from_str(&pulse_answer("schedule", "{\"qasm\": \"OPENQASM 3.0;\\nqubit q;\\n\"}", &key)).unwrap();
assert!(bad["error"].as_str().unwrap().ends_with("[Q460]"), "{bad}");
}
#[test]
fn a_model_server_is_reached_by_http_only() {
assert_eq!(model_url("http://127.0.0.1:8080/v1/", "/models").unwrap(), "http://127.0.0.1:8080/v1/models");

View file

@ -23,10 +23,10 @@
"id": "getting-started-1",
"metadata": {
"execution": {
"iopub.execute_input": "2026-10-28T08:01:08.252068Z",
"iopub.status.busy": "2026-10-28T08:01:08.251991Z",
"iopub.status.idle": "2026-10-28T08:01:08.262307Z",
"shell.execute_reply": "2026-10-28T08:01:08.262058Z"
"iopub.execute_input": "2026-10-30T18:17:32.853588Z",
"iopub.status.busy": "2026-10-30T18:17:32.853375Z",
"iopub.status.idle": "2026-10-30T18:17:32.871318Z",
"shell.execute_reply": "2026-10-30T18:17:32.870355Z"
}
},
"outputs": [
@ -52,10 +52,10 @@
"id": "getting-started-2",
"metadata": {
"execution": {
"iopub.execute_input": "2026-10-28T08:01:08.263274Z",
"iopub.status.busy": "2026-10-28T08:01:08.263203Z",
"iopub.status.idle": "2026-10-28T08:01:08.264838Z",
"shell.execute_reply": "2026-10-28T08:01:08.264640Z"
"iopub.execute_input": "2026-10-30T18:17:32.873536Z",
"iopub.status.busy": "2026-10-30T18:17:32.873343Z",
"iopub.status.idle": "2026-10-30T18:17:32.877116Z",
"shell.execute_reply": "2026-10-30T18:17:32.876420Z"
}
},
"outputs": [
@ -95,10 +95,10 @@
"id": "getting-started-4",
"metadata": {
"execution": {
"iopub.execute_input": "2026-10-28T08:01:08.265673Z",
"iopub.status.busy": "2026-10-28T08:01:08.265609Z",
"iopub.status.idle": "2026-10-28T08:01:08.267553Z",
"shell.execute_reply": "2026-10-28T08:01:08.267344Z"
"iopub.execute_input": "2026-10-30T18:17:32.879466Z",
"iopub.status.busy": "2026-10-30T18:17:32.879270Z",
"iopub.status.idle": "2026-10-30T18:17:32.883948Z",
"shell.execute_reply": "2026-10-30T18:17:32.883150Z"
}
},
"outputs": [
@ -142,10 +142,10 @@
"id": "getting-started-6",
"metadata": {
"execution": {
"iopub.execute_input": "2026-10-28T08:01:08.268230Z",
"iopub.status.busy": "2026-10-28T08:01:08.268142Z",
"iopub.status.idle": "2026-10-28T08:01:08.270293Z",
"shell.execute_reply": "2026-10-28T08:01:08.270115Z"
"iopub.execute_input": "2026-10-30T18:17:32.885820Z",
"iopub.status.busy": "2026-10-30T18:17:32.885633Z",
"iopub.status.idle": "2026-10-30T18:17:32.889717Z",
"shell.execute_reply": "2026-10-30T18:17:32.889212Z"
}
},
"outputs": [
@ -206,10 +206,10 @@
"id": "getting-started-8",
"metadata": {
"execution": {
"iopub.execute_input": "2026-10-28T08:01:08.271106Z",
"iopub.status.busy": "2026-10-28T08:01:08.271041Z",
"iopub.status.idle": "2026-10-28T08:01:08.272679Z",
"shell.execute_reply": "2026-10-28T08:01:08.272473Z"
"iopub.execute_input": "2026-10-30T18:17:32.890804Z",
"iopub.status.busy": "2026-10-30T18:17:32.890727Z",
"iopub.status.idle": "2026-10-30T18:17:32.892852Z",
"shell.execute_reply": "2026-10-30T18:17:32.892507Z"
}
},
"outputs": [
@ -245,10 +245,10 @@
"id": "getting-started-10",
"metadata": {
"execution": {
"iopub.execute_input": "2026-10-28T08:01:08.273369Z",
"iopub.status.busy": "2026-10-28T08:01:08.273300Z",
"iopub.status.idle": "2026-10-28T08:01:08.274741Z",
"shell.execute_reply": "2026-10-28T08:01:08.274539Z"
"iopub.execute_input": "2026-10-30T18:17:32.893855Z",
"iopub.status.busy": "2026-10-30T18:17:32.893793Z",
"iopub.status.idle": "2026-10-30T18:17:32.895781Z",
"shell.execute_reply": "2026-10-30T18:17:32.895369Z"
}
},
"outputs": [
@ -277,17 +277,17 @@
"id": "getting-started-11",
"metadata": {
"execution": {
"iopub.execute_input": "2026-10-28T08:01:08.275420Z",
"iopub.status.busy": "2026-10-28T08:01:08.275364Z",
"iopub.status.idle": "2026-10-28T08:01:08.313903Z",
"shell.execute_reply": "2026-10-28T08:01:08.313547Z"
"iopub.execute_input": "2026-10-30T18:17:32.896679Z",
"iopub.status.busy": "2026-10-30T18:17:32.896620Z",
"iopub.status.idle": "2026-10-30T18:17:32.937071Z",
"shell.execute_reply": "2026-10-30T18:17:32.936536Z"
}
},
"outputs": [
{
"data": {
"application/vnd.jupyter.widget-view+json": {
"model_id": "98c4cc1a28394e16862b3cfa5d240dac",
"model_id": "7c992051b33243fa9a5585cc2c0cfcf3",
"version_major": 2,
"version_minor": 1
},
@ -316,10 +316,10 @@
"id": "getting-started-12",
"metadata": {
"execution": {
"iopub.execute_input": "2026-10-28T08:01:08.315388Z",
"iopub.status.busy": "2026-10-28T08:01:08.315309Z",
"iopub.status.idle": "2026-10-28T08:01:08.317133Z",
"shell.execute_reply": "2026-10-28T08:01:08.316863Z"
"iopub.execute_input": "2026-10-30T18:17:32.938195Z",
"iopub.status.busy": "2026-10-30T18:17:32.938101Z",
"iopub.status.idle": "2026-10-30T18:17:32.940581Z",
"shell.execute_reply": "2026-10-30T18:17:32.940034Z"
}
},
"outputs": [
@ -355,10 +355,10 @@
"id": "getting-started-14",
"metadata": {
"execution": {
"iopub.execute_input": "2026-10-28T08:01:08.317804Z",
"iopub.status.busy": "2026-10-28T08:01:08.317745Z",
"iopub.status.idle": "2026-10-28T08:01:08.319238Z",
"shell.execute_reply": "2026-10-28T08:01:08.318955Z"
"iopub.execute_input": "2026-10-30T18:17:32.941942Z",
"iopub.status.busy": "2026-10-30T18:17:32.941847Z",
"iopub.status.idle": "2026-10-30T18:17:32.943927Z",
"shell.execute_reply": "2026-10-30T18:17:32.943548Z"
}
},
"outputs": [
@ -376,7 +376,8 @@
"cutrank exact; single when only a single-precision table fits\n",
"cutrank_single single\n",
"density_matrix exact\n",
"trajectories estimate\n"
"trajectories estimate\n",
"pulse bounded\n"
]
}
],
@ -391,10 +392,10 @@
"id": "getting-started-15",
"metadata": {
"execution": {
"iopub.execute_input": "2026-10-28T08:01:08.319876Z",
"iopub.status.busy": "2026-10-28T08:01:08.319820Z",
"iopub.status.idle": "2026-10-28T08:01:08.321721Z",
"shell.execute_reply": "2026-10-28T08:01:08.321462Z"
"iopub.execute_input": "2026-10-30T18:17:32.944906Z",
"iopub.status.busy": "2026-10-30T18:17:32.944835Z",
"iopub.status.idle": "2026-10-30T18:17:32.947177Z",
"shell.execute_reply": "2026-10-30T18:17:32.946844Z"
}
},
"outputs": [
@ -432,17 +433,17 @@
"id": "getting-started-17",
"metadata": {
"execution": {
"iopub.execute_input": "2026-10-28T08:01:08.322331Z",
"iopub.status.busy": "2026-10-28T08:01:08.322276Z",
"iopub.status.idle": "2026-10-28T08:01:08.324933Z",
"shell.execute_reply": "2026-10-28T08:01:08.324626Z"
"iopub.execute_input": "2026-10-30T18:17:32.948407Z",
"iopub.status.busy": "2026-10-30T18:17:32.948327Z",
"iopub.status.idle": "2026-10-30T18:17:32.951943Z",
"shell.execute_reply": "2026-10-30T18:17:32.951464Z"
}
},
"outputs": [
{
"data": {
"application/vnd.jupyter.widget-view+json": {
"model_id": "71e7e316cf1c4d6993bd73c703188142",
"model_id": "0578d81f40d747c585ebeaccf83dfb4a",
"version_major": 2,
"version_minor": 1
},
@ -479,7 +480,7 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.12.15"
"version": "3.12.14"
}
},
"nbformat": 4,

View file

@ -156,9 +156,11 @@ Counts are always seeded samples.
| `POST /v1/qec/code` | `{"code", "certify"?, "bound"?, "certificate"?}`: an error-correcting code (`surface:D`, `toric:L`, `steane`, `color:D`, `bb:72` to `bb:288`, `gross`), its checks, `k`, layout and decoders, and its distance certificate, or a given certificate checked against it |
| `POST /v1/qec/decode` | a code's `syndrome` or `error` decoded at code capacity, or a `memory` experiment's, a `circuit`'s or a `model`'s fired `detectors` decoded (or its `shots` sampled and decoded), with `decoder` |
| `POST /v1/qec/threshold` | `{"family", "distances", "p", "shots", "decoder"?}`: logical failure rates with their standard errors, the code model fitted at circuit level, and whether each larger distance failed less |
| `POST /v1/pulse/schedule` | `{"qasm", "pulse"?}`: a pulse program's schedule on frames, every play, delay, phase and frequency change, capture and barrier at its sample, each waveform's samples, the gate calls, and the digests a pulse run's receipt binds |
| `POST /v1/pulse/check` | `{"request", "result"}`: a pulse job checked by running it again here: its receipt, its program's digest, and its result held to the run field by field |
| `POST /mcp` | the Model Context Protocol endpoint (streamable HTTP, answered as JSON, no key): the tools, resources and prompts below |
The three error-correction calls answer in a job's form, sealed with the service's key, so `POST /v1/verify` checks them ([the error-correction page](../wai-quantum/docs/qec.md)). A job may also give `mitigate` beside `noise` and `observables` (`"zne"`, `"pea"`, `"pec"`, `"cdr"`, `"vd"`, `"readout"`, or `{"method", ...}` with its options): its result then carries each value raw and mitigated with its error bar, the method and its assumptions, inside the receipt.
The three error-correction calls answer in a job's form, sealed with the service's key, so `POST /v1/verify` checks them ([the error-correction page](../wai-quantum/docs/qec.md)). So do the two pulse calls; a pulse run itself is a job of the method `pulse` (`"method": "pulse"`, its options under `"pulse"`), queued, run and sealed as every job is ([the pulse page](../wai-quantum/docs/pulse.md)). A job may also give `mitigate` beside `noise` and `observables` (`"zne"`, `"pea"`, `"pec"`, `"cdr"`, `"vd"`, `"readout"`, or `{"method", ...}` with its options): its result then carries each value raw and mitigated with its error bar, the method and its assumptions, inside the receipt.
A job may give a `target`: a built-in target's name, or a target document (`wai.quantum.target/1`, its schema in `GET /v1/openapi.json`). The program is compiled to the target's gates, placed on the qubits and routes most likely to succeed, and simulated under the noise its calibration gives. The result's `target` names the layout, its expected success, the noise and the target's hash, inside the receipt. The service reads the document it is given and fetches nothing.
@ -188,10 +190,11 @@ The six checks answer at once, in the service's own process. Each counts against
- a compile of more than 20,000 gates, or with more than 256 rotations to approximate, or a budget that leaves a rotation less than 10⁻¹² (`wai-quantum compile`). A job that asks to be compiled first is held to the same bounds;
- an error-correction call past its bounds here (`wai-quantum qec`): a decode of more than 200,000 shots, of a memory experiment past distance 7, or of more than 2 MiB of circuit or model text; a threshold of more than 16 points or 200,000 shots in all, or at circuit level past distance 7; the distance certificate of `bb:288`;
- an estimate of more than 200,000 gates, or with more than 256 distinct rotation angles to synthesise, or a budget that leaves a rotation less than 10⁻¹² (`wai-quantum estimate`). Verifying an estimate re-runs it, within the same bounds.
- a pulse check whose run takes more than 2·10¹⁰ of the simulator's multiply-adds, or a pulse call on more than 1 MiB of program (`wai-quantum pulse check`). A pulse job itself is held to the limits every job is.
## For agents
`POST /mcp` serves the service to an agent as Model Context Protocol tools: `run_quantum_circuit`, `run_quantum_set` (a set or a sweep), `dry_run_quantum_job` (the plan, running nothing), `get_quantum_job`, `verify_quantum_receipt`, `critique_qasm`, `check_equivalent`, `improve_qasm`, `classical_shortcuts`, `compile_qasm`, `estimate_resources`, `repair_qasm`, `quantum_service_info`, `qasm_diagnostics`, `list_targets`, `show_target`, `run_on_target`, `quantum_circuit_examples`, `qec_code`, `qec_decode` and `qec_threshold`. A run it makes asks for its verdict, so the request its receipt seals is the job's own `request`; every answer about a program carries its verdict, every refusal its codes, and every tool a versioned output schema. `verify_quantum_receipt` re-runs an estimate. It also serves the engine's resources and prompts, the same as the command line's own server (`wai-quantum mcp`):
`POST /mcp` serves the service to an agent as Model Context Protocol tools: `run_quantum_circuit`, `run_quantum_set` (a set or a sweep), `dry_run_quantum_job` (the plan, running nothing), `get_quantum_job`, `verify_quantum_receipt`, `critique_qasm`, `check_equivalent`, `improve_qasm`, `classical_shortcuts`, `compile_qasm`, `estimate_resources`, `repair_qasm`, `quantum_service_info`, `qasm_diagnostics`, `list_targets`, `show_target`, `run_on_target`, `quantum_circuit_examples`, `qec_code`, `qec_decode`, `qec_threshold`, `pulse_schedule` and `pulse_check`. A run it makes asks for its verdict, so the request its receipt seals is the job's own `request`; every answer about a program carries its verdict, every refusal its codes, and every tool a versioned output schema. `verify_quantum_receipt` re-runs an estimate. It also serves the engine's resources and prompts, the same as the command line's own server (`wai-quantum mcp`):
- resources: the language the engine reads (`wai-quantum://docs/language`), its diagnostics, the methods with their accuracy classes (their reach in a job's memory here), and worked examples, each with what it must do;
- prompts: writing and verifying a circuit for an intent, explaining a result and its receipt, and repairing a program from its diagnostics. Each names this server's own tools.

View file

@ -226,7 +226,7 @@ mcp '{"jsonrpc":"2.0","id":2,"method":"tools/list"}' > "$tmp/mcp-tools.json"
py 'import json,sys
i=json.load(open(sys.argv[1]))["result"]; t=json.load(open(sys.argv[2]))["result"]["tools"]
assert i["protocolVersion"]=="2025-06-18" and "tools" in i["capabilities"], i
names={x["name"] for x in t}; assert {"run_quantum_circuit","run_quantum_set","dry_run_quantum_job","get_quantum_job","verify_quantum_receipt","quantum_service_info","quantum_circuit_examples","critique_qasm","check_equivalent","improve_qasm","classical_shortcuts","compile_qasm","estimate_resources","qec_code","qec_decode","qec_threshold"} <= names, names
names={x["name"] for x in t}; assert {"run_quantum_circuit","run_quantum_set","dry_run_quantum_job","get_quantum_job","verify_quantum_receipt","quantum_service_info","quantum_circuit_examples","critique_qasm","check_equivalent","improve_qasm","classical_shortcuts","compile_qasm","estimate_resources","qec_code","qec_decode","qec_threshold","pulse_schedule","pulse_check"} <= names, names
assert sys.argv[3]=="202", sys.argv[3]
print("mcp: ok, initialized,", len(t), "tools")' "$tmp/mcp-init.json" "$tmp/mcp-tools.json" "$code"
py 'import json,sys
@ -440,9 +440,9 @@ post /v1/repair "$tmp/proposal.json" | py 'import json,sys; d=json.load(sys.stdi
py 'import json; print(json.dumps({"qasm": "OPENQASM 3.0;\nqubit[1] q;\nrx(0.3) q[0];\n", "method": "stabilizer"}))' > "$tmp/refused.json"
post /v1/run "$tmp/refused.json" | py 'import json,sys; d=json.load(sys.stdin); assert d["error"].endswith("[J011]"), d; print("refusal: ok, ends with its code")'
# The whole script makes 28 submissions (29 on a host with a GPU), within the
# The whole script makes 29 submissions (30 on a host with a GPU), within the
# 30 a client may make in a window: a check added anywhere must take the place
# of one. What follows makes four.
# of one. What follows makes five.
#
# A dry run: the plan, its estimates with their basis, the bounds, and whether
# the service would take it; nothing runs.
@ -456,19 +456,29 @@ print("dry run: ok,", d["method"], "%.1f MiB," % (d["memory_bytes"] / 2**20), "t
# A set of programs: one result and receipt per item, one receipt over the
# Merkle root of them all, which POST /v1/verify checks; a changed item fails.
py 'import json; print(json.dumps({"programs": ["OPENQASM 3.0; qubit[2] q; bit[2] c; h q[0]; cx q[0], q[1]; c = measure q;", {"qasm": "OPENQASM 3.0; qubit q; rx(0.3) q;", "observables": ["Z0"], "shots": 0}], "shots": 100, "seed": 5}))' > "$tmp/set.json"
# Its third item is a pulse program run as a job of the method pulse (its
# error bounded, its receipt binding its program), which POST /v1/pulse/check
# then runs again and holds to its result.
py 'import json
pulse = "OPENQASM 3.0;\ndefcalgrammar \"openpulse\";\ncal {\n extern port d0;\n extern port a0;\n frame q0 = newframe(d0, 5.0e9, 0);\n frame c0 = newframe(a0, 5.0e9, 0);\n}\ndefcal x $0 { play(q0, gaussian(0.4, 64dt, 16dt)); }\ndefcal measure $0 -> bit { return capture_v2(c0, 100dt); }\nbit c;\nx $0;\nc = measure $0;\n"
print(json.dumps({"programs": ["OPENQASM 3.0; qubit[2] q; bit[2] c; h q[0]; cx q[0], q[1]; c = measure q;", {"qasm": "OPENQASM 3.0; qubit q; rx(0.3) q;", "observables": ["Z0"], "shots": 0}, {"qasm": pulse, "method": "pulse"}], "shots": 100, "seed": 5}))' > "$tmp/set.json"
run "$tmp/set.json" "$tmp/set.out"
py 'import json,sys
d=json.load(open(sys.argv[1])); r=d["result"]
assert d["status"]=="done" and d["method"]=="set" and r["format"]=="wai.quantum.set/1" and (r["count"], r["done"])==(2, 2), d
assert d["status"]=="done" and d["method"]=="set" and r["format"]=="wai.quantum.set/1" and (r["count"], r["done"])==(3, 3), d
p=r["items"][2]; a=p["result"]["accuracy"]
assert p["method"]=="pulse" and a["class"]=="bounded" and a["bound"] <= 1e-6 and len(p["result"]["pulse"]["calibration"]["program"])==64, p
json.dump({"request": p["request"], "result": p["result"]}, open(sys.argv[4],"w"))
assert r["items"][0]["method"]=="stabilizer" and abs(r["items"][1]["result"]["expectations"][0]["value"] - 0.955336489125606) < 1e-12, r["items"]
assert r["items"][0]["result"]["provenance"]["software"]["host"].startswith("wai-quantum-service"), r["items"][0]["result"]
json.dump({"request": d["request"], "result": r}, open(sys.argv[2],"w"))
r["items"][0]["result"]["counts"]["00"] = r["items"][0]["result"]["counts"].get("00", 0) + 1
json.dump({"request": d["request"], "result": r}, open(sys.argv[3],"w"))
print("set: ok,", r["count"], "items, root", r["root"][:16]+"…")' "$tmp/set.out" "$tmp/vset.json" "$tmp/tset.json"
post /v1/verify "$tmp/vset.json" | py 'import json,sys; d=json.load(sys.stdin); assert d["valid"] is True and d["items"]==2, d; print("set receipt: verifies, every item sealed")'
print("set: ok,", r["count"], "items, root", r["root"][:16]+"…, the pulse item P(1)", round(p["result"]["pulse"]["distribution"]["1"], 6), "within", a["bound"])' "$tmp/set.out" "$tmp/vset.json" "$tmp/tset.json" "$tmp/pulse-check.json"
post /v1/verify "$tmp/vset.json" | py 'import json,sys; d=json.load(sys.stdin); assert d["valid"] is True and d["items"]==3, d; print("set receipt: verifies, every item sealed")'
post /v1/verify "$tmp/tset.json" | py 'import json,sys; d=json.load(sys.stdin); assert d["valid"] is False, d; print("set receipt: a changed item does not verify")'
post /v1/pulse/check "$tmp/pulse-check.json" > "$tmp/pulse-check.out"
py 'import json,sys; r=json.load(open(sys.argv[1]))["result"]; assert r["verified"] is True and r["reproduced"] is True and r["receipt"], r; print("pulse check: ok, the set item run again here, bit for bit")' "$tmp/pulse-check.out"
# A job's status as server-sent events, to its end; and a poll held until it changes.
id=$(py 'import json,sys; print(json.load(open(sys.argv[1]))["id"])' "$tmp/set.out")

View file

@ -210,6 +210,37 @@ pub fn qec_bounds(op: &str, body: &Value) -> Result<(), (u16, String)> {
Ok(())
}
/// The most work a pulse check runs here: the pulse run it repeats, in the
/// simulator's multiply-adds.
pub const PULSE_MAX_WORK: u64 = 20_000_000_000;
/// The most bytes of program a pulse call reads here.
pub const PULSE_MAX_TEXT: usize = 1 << 20;
/// Whether the service takes the pulse call `op` (`schedule` or `check`)
/// on `body`: the reason it does not, with the status to answer, or
/// nothing. A check runs its job again, so it is held to the work a call
/// here takes; past it the reason names the command that runs it on the
/// client's machine, and the job service runs pulse jobs themselves
/// (method pulse) within its jobs' limits.
pub fn pulse_bounds(op: &str, body: &Value) -> Result<(), (u16, String)> {
let far = |m: String| Err((422, tag(refusal::TOO_LARGE_HERE, m)));
let text = match op {
"check" => body.pointer("/request/qasm"),
_ => body.get("qasm"),
};
if text.and_then(Value::as_str).is_some_and(|t| t.len() > PULSE_MAX_TEXT) {
return far(format!("a program longer than {PULSE_MAX_TEXT} bytes; run `wai-quantum pulse {op}` on your machine"));
}
if op == "check" {
let request = body.get("request").unwrap_or(&Value::Null);
let plan = wai_quantum::engine::pulse::plan(request, u64::MAX).map_err(|e| (422, e))?;
if plan.work > PULSE_MAX_WORK {
return far(format!("checking this pulse job runs it again, {} multiply-adds, more than a call here takes ({PULSE_MAX_WORK}); run `wai-quantum pulse check` on your machine", plan.work));
}
}
Ok(())
}
/// `work` for `who` within the bounds above (counted, one of [`AT_ONCE`],
/// off the request's thread): its answer, or 422 with its refusal.
pub async fn bounded<F>(app: &App, who: &str, work: F) -> Result<Value, (u16, String)>

View file

@ -115,7 +115,7 @@ pub fn openapi(server: &str) -> Value {
"required": ["qasm"],
"properties": {
"qasm": { "type": "string", "description": "The program: OpenQASM 3, or OpenQASM 2. Registers, standard gates (h, x, y, z, s, sdg, t, tdg, sx, rx, ry, rz, p, u, cx, cy, cz, ch, swap, iswap, cp, crx, cry, crz, ccx, ccz, cswap, rxx, ryy, rzz), the control modifiers ctrl @, ctrl(n) @ and negctrl @ on x y z h s sdg t tdg p and the controlled names, user gate definitions, measurement anywhere, reset anywhere, and `if (c[0]) …` / `if (c == 3) { … } else { … }` on measured bits (OpenQASM 2's `if(c==n) gate;` too). A program that measures in the middle, resets after a gate or acts on measured bits runs on statevector or stabilizer: counts split their shots at each measurement, expectation values are the exact mixture over the branches, of the state before the final measurements. Loops, inv @ and pow @ are refused, with the line." },
"method": { "type": "string", "enum": ["auto", "exact", "statevector", "statevector_single", "stabilizer", "pauli", "mps", "tensor_network", "cutrank", "cutrank_single", "density_matrix", "trajectories"], "default": "auto" },
"method": { "type": "string", "enum": ["auto", "exact", "statevector", "statevector_single", "stabilizer", "pauli", "mps", "tensor_network", "cutrank", "cutrank_single", "density_matrix", "trajectories", "pulse"], "default": "auto", "description": "pulse: a pulse program (OpenPulse calibrations on physical qubits) integrated under its pulse options, its error bounded (POST /v1/pulse/schedule draws its schedule)." },
"shots": { "type": "integer", "minimum": 0, "maximum": 1000000, "description": "Measurement shots (default 1024 when nothing else is asked for)." },
"seed": { "type": "integer", "default": 1, "description": "The sampling seed: the same job and seed give the same counts." },
"observables": { "type": "array", "items": pauli },
@ -141,7 +141,8 @@ pub fn openapi(server: &str) -> Value {
"repair": { "description": "true, or the choices {\"max_iterations\", \"dyadic_bits\", \"sizes\"}: the program is repaired first on the lines its diagnostics flag (as POST /v1/repair does), and the result's `repaired` holds the program that ran and each repair." },
"programs": { "type": "array", "minItems": 1, "maxItems": wai_quantum::engine::sets::MAX_ITEMS, "items": { "oneOf": [{ "type": "string" }, { "type": "object", "required": ["qasm"] }] }, "description": "A set: in place of qasm, the programs to run as one job, each a program's text or a job whose own fields win over the set's. Every other field applies to each item. The result is a set (SetResult)." },
"sweep": { "type": "object", "additionalProperties": { "type": "array", "minItems": 1, "items": { "type": "number" } }, "description": "A set over qasm's inputs: each input's values, every combination run (the names in sorted order, the first varying slowest). An integer input (`input uint n;`) takes whole numbers and the program is written out at each; a float or angle input (`input float theta;`) is bound to the double given. Every input the program declares is given values." },
"dry_run": { "type": "boolean", "description": "true: run nothing, and answer 200 with the plan (DryRun): the method, memory, estimated time and energy with their basis, the bounds, and whether the service would take it now." }
"dry_run": { "type": "boolean", "description": "true: run nothing, and answer 200 with the plan (DryRun): the method, memory, estimated time and energy with their basis, the bounds, and whether the service would take it now." },
"pulse": wai_quantum::engine::pulse::options_schema()
}
});
let dry_run = json!({
@ -325,12 +326,29 @@ pub fn openapi(server: &str) -> Value {
"/mcp": { "post": { "summary": "The Model Context Protocol endpoint (streamable HTTP, JSON responses): tools to run circuits and sets, plan a job without running it, read jobs, verify receipts, list examples, and critique, compare, improve, price, compile, estimate and repair programs", "responses": { "200": { "description": "A JSON-RPC response" } } } }
}
});
for (path, item) in qec_paths() {
for (path, item) in qec_paths().into_iter().chain(pulse_paths()) {
doc["paths"][path] = item;
}
doc
}
/// The pulse calls' paths.
fn pulse_paths() -> Vec<(&'static str, Value)> {
let responses = |ok: &str| json!({ "200": { "description": ok }, "400": { "description": "A body that does not read" }, "422": { "description": "A program the pulse reader refuses (its Q code), a request the engine refuses (J040, J041), or a check larger than the service takes (J031, naming the local command)" }, "429": { "description": "Past the per-client limits" }, "503": { "description": "Every check is running" } });
vec![
("/v1/pulse/schedule", json!({ "post": {
"summary": "A pulse program's schedule on frames: every play, delay, phase and frequency change, capture and barrier at its sample, each waveform's samples, the gate calls, and the digests a pulse run's receipt binds; sealed with the service's key",
"requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "required": ["qasm"], "properties": { "qasm": { "type": "string" }, "pulse": wai_quantum::engine::pulse::options_schema() } } } } },
"responses": responses("{\"request\", \"result\": {\"frames\", \"instructions\", \"waveforms\", \"calls\", \"calibration\", \"receipt\"}} (wai.quantum.pulse/1)")
} })),
("/v1/pulse/check", json!({ "post": {
"summary": "A pulse job checked by running it again: its receipt's signature, its program's digest against its request, and its result held to the run here field by field; sealed with the service's key",
"requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "required": ["request", "result"], "properties": { "request": { "type": "object" }, "result": { "type": "object" } } } } } },
"responses": responses("{\"request\", \"result\": {\"verified\", \"signature\", \"program\", \"reproduced\", \"differences\", \"statement\", \"receipt\"}} (wai.quantum.pulse/1)")
} })),
]
}
/// The error-correction calls' paths, apart, so the document's macro stays
/// within its depth.
fn qec_paths() -> Vec<(&'static str, Value)> {
@ -430,7 +448,7 @@ mod tests {
let d = openapi("https://example.org/q");
for p in ["/v1", "/v1/methods", "/v1/examples", "/v1/targets", "/v1/run", "/v1/jobs", "/v1/jobs/{id}", "/v1/jobs/{id}/events", "/.well-known/wai-quantum-receipt-keys", "/v1/verify", "/v1/compile", "/v1/estimate", "/v1/qec/code", "/v1/qec/decode", "/v1/qec/threshold", "/mcp"] {
for p in ["/v1", "/v1/methods", "/v1/examples", "/v1/targets", "/v1/run", "/v1/jobs", "/v1/jobs/{id}", "/v1/jobs/{id}/events", "/.well-known/wai-quantum-receipt-keys", "/v1/verify", "/v1/compile", "/v1/estimate", "/v1/qec/code", "/v1/qec/decode", "/v1/qec/threshold", "/v1/pulse/schedule", "/v1/pulse/check", "/mcp"] {
assert!(d["paths"].get(p).is_some(), "{p}");
}
assert_eq!(d["openapi"], "3.1.0");

View file

@ -33,6 +33,13 @@
//! POST /v1/qec/decode a syndrome, or a circuit's detectors, decoded
//! POST /v1/qec/threshold logical failure rates over distances and rates,
//! each answer sealed with the service's key
//! POST /v1/pulse/schedule a pulse program's schedule on frames: every
//! instruction at its sample, the waveforms and
//! the gate calls, with the digests a run binds
//! POST /v1/pulse/check a pulse job checked by running it again: its
//! receipt, its program's digest, its result bit
//! for bit (a pulse job itself is a job: method
//! pulse, through /v1/jobs and /v1/run)
//! GET /v1/examples ready-to-run example jobs
//! GET /v1/targets the built-in device targets (a job may also give its own)
//! GET /v1/openapi.json this API as an OpenAPI 3.1 document
@ -577,6 +584,29 @@ pub(crate) async fn qec(app: &App, who: &str, op: &'static str, request: Value)
Ok(json!({ "request": request, "result": result }))
}
/// A pulse call (`schedule` or `check`), its answer sealed with the
/// service's key in a job's form, so `POST /v1/verify` checks it; within the
/// bounds of [`crate::analysis::pulse_bounds`].
pub(crate) async fn pulse(app: &App, who: &str, op: &'static str, request: Value) -> Result<Value, (u16, String)> {
if let Some(at) = crate::receipt::inexact_number(&request) {
return Err((400, format!("`{at}`: a whole number here must be within ±9007199254740991 (2^53 − 1), so that every JSON reader reads it alike and the receipt binds it")));
}
crate::analysis::pulse_bounds(op, &request)?;
let req = request.clone();
let mut result = crate::analysis::bounded(app, who, move || wai_quantum::engine::pulse::call(op, &req)).await?;
result["receipt"] = app.signer.seal(&request, &result);
Ok(json!({ "request": request, "result": result }))
}
async fn pulse_route(app: Shared, peer: SocketAddr, headers: HeaderMap, body: Result<Json<Value>, axum::extract::rejection::JsonRejection>, op: &'static str) -> Response {
let Ok(Json(v)) = body else { return error(400, "the body is not JSON") };
let who = client(&app, peer, &headers);
match pulse(&app, &who, op, v).await {
Ok(sealed) => reply(200, sealed),
Err((code, why)) => error(code, &why),
}
}
async fn qec_route(app: Shared, peer: SocketAddr, headers: HeaderMap, body: Result<Json<Value>, axum::extract::rejection::JsonRejection>, op: &'static str) -> Response {
let Ok(Json(v)) = body else { return error(400, "the body is not JSON") };
let who = client(&app, peer, &headers);
@ -740,6 +770,8 @@ pub fn router(app: Shared) -> Router {
.route("/v1/qec/code", post(|State(a): State<Shared>, ConnectInfo(p): ConnectInfo<SocketAddr>, h: HeaderMap, b| qec_route(a, p, h, b, "code")))
.route("/v1/qec/decode", post(|State(a): State<Shared>, ConnectInfo(p): ConnectInfo<SocketAddr>, h: HeaderMap, b| qec_route(a, p, h, b, "decode")))
.route("/v1/qec/threshold", post(|State(a): State<Shared>, ConnectInfo(p): ConnectInfo<SocketAddr>, h: HeaderMap, b| qec_route(a, p, h, b, "threshold")))
.route("/v1/pulse/schedule", post(|State(a): State<Shared>, ConnectInfo(p): ConnectInfo<SocketAddr>, h: HeaderMap, b| pulse_route(a, p, h, b, "schedule")))
.route("/v1/pulse/check", post(|State(a): State<Shared>, ConnectInfo(p): ConnectInfo<SocketAddr>, h: HeaderMap, b| pulse_route(a, p, h, b, "check")))
.route("/v1/estimate", post(estimate_route))
.route("/v1/repair", post(|State(a): State<Shared>, ConnectInfo(p): ConnectInfo<SocketAddr>, h: HeaderMap, b| analysis(a, p, h, b, crate::analysis::Check::Repair)))
.route("/v1/examples", get(examples))

View file

@ -75,6 +75,7 @@ fn output_of(tool: &str) -> Option<&'static str> {
"show_target" => "tools.v1.json#/$defs/target_summary",
"qasm_diagnostics" => "service-tools.v1.json#/$defs/diagnostics",
"qec_code" | "qec_decode" | "qec_threshold" => "qec.v1.json",
"pulse_schedule" | "pulse_check" => "pulse.v1.json",
_ => return None,
})
}
@ -220,14 +221,15 @@ fn tool_list() -> Value {
"noise": { "type": "object", "description": "Optional noise, each a probability: depolarizing_1q, depolarizing_2q, amplitude_damping, dephasing (after each gate on its qubits), readout (each measured bit).", "properties": {
"depolarizing_1q": { "type": "number" }, "depolarizing_2q": { "type": "number" }, "amplitude_damping": { "type": "number" }, "dephasing": { "type": "number" }, "readout": { "type": "number" }
} },
"method": { "type": "string", "enum": ["auto", "exact", "statevector", "statevector_single", "stabilizer", "pauli", "mps", "tensor_network", "cutrank", "cutrank_single", "density_matrix", "trajectories"], "description": "Leave as auto unless you need a particular method." },
"method": { "type": "string", "enum": ["auto", "exact", "statevector", "statevector_single", "stabilizer", "pauli", "mps", "tensor_network", "cutrank", "cutrank_single", "density_matrix", "trajectories", "pulse"], "description": "Leave as auto unless you need a particular method; pulse runs a pulse program (OpenPulse calibrations on physical qubits, see pulse_schedule) under the pulse options, its error bounded." },
"seed": { "type": "integer", "description": "Sampling seed (default 1); the same job and seed give the same counts." },
"options": { "type": "object", "description": "Advanced, per method: max_bond (mps), threshold and max_terms (pauli), fusion (statevector), trajectories." },
"compile": { "type": "object", "description": "Compile the program first (as compile_qasm does) and run the compiled circuit: with basis clifford_t it runs bit-exact, and the result's `compiled` states its distance from the program.", "properties": compile_props() },
"target": { "description": "A device target: a built-in target's name (list_targets) or a target document (wai.quantum.target/1). The program is then compiled to its gates, placed where it is most likely to succeed and simulated under its noise, its hash in the receipt; not with noise or amplitudes. The service reads the document given and fetches nothing." },
"intent": intent_schema(),
"repair": { "description": "Repair the program first, on the lines its diagnostics flag alone (as repair_qasm does): true, or the choices {\"max_iterations\": N, \"dyadic_bits\": K, \"sizes\": {\"q\": 3}}. The result's `repaired` holds the program that ran and each repair." },
"mitigate": wai_quantum::engine::mitigate::schema()
"mitigate": wai_quantum::engine::mitigate::schema(),
"pulse": wai_quantum::engine::pulse::options_schema()
});
let mut on_target = json!({ "type": "object", "required": ["qasm", "target"], "properties": job_props.clone() });
on_target["properties"].as_object_mut().map(|o| o.remove("method"));
@ -376,6 +378,7 @@ fn tool_list() -> Value {
]);
if let Some(a) = list.as_array_mut() {
a.extend(wai_quantum::engine::qec::tools("the service's key").as_array().cloned().unwrap_or_default());
a.extend(wai_quantum::engine::pulse::tools("the service's key").as_array().cloned().unwrap_or_default());
}
list
}
@ -682,6 +685,20 @@ async fn call(app: &App, who: &str, name: &str, args: Value, base: &str) -> Valu
}
Err((_, why)) => tool_result(format!("refused: {why}"), json!({ "error": why }), true),
},
name if wai_quantum::engine::pulse::op_of_tool(name).is_some() => {
let op = wai_quantum::engine::pulse::op_of_tool(name).expect("a pulse tool");
match crate::http::pulse(app, who, op, args).await {
Ok(body) => {
let r = &body["result"];
let text = match op {
"schedule" => format!("a schedule of {} samples ({} ns) on {} frames: {} instructions, {} gate calls; sealed (verify_quantum_receipt checks it)", r["duration"], r["duration_ns"], r["frames"].as_array().map_or(0, Vec::len), r["instructions"].as_array().map_or(0, Vec::len), r["calls"].as_array().map_or(0, Vec::len)),
_ => format!("{}: {}; sealed", if r["verified"] == true { "verified" } else { "not verified" }, r["statement"].as_str().unwrap_or("")),
};
tool_result(text, body, false)
}
Err((_, why)) => tool_result(format!("refused: {why}"), json!({ "error": why }), true),
}
}
name if wai_quantum::engine::qec::op_of_tool(name).is_some() => {
let op = wai_quantum::engine::qec::op_of_tool(name).expect("an error-correction tool");
match crate::http::qec(app, who, op, args).await {
@ -760,6 +777,9 @@ fn dry_run_answer(app: &App, who: &str, args: &Value) -> Value {
#[cfg(test)]
mod tests {
/// A pulse program: an `x` as a gaussian and a measurement as a capture.
const PULSE: &str = "OPENQASM 3.0;\ndefcalgrammar \"openpulse\";\ncal {\n extern port d0;\n extern port a0;\n frame q0 = newframe(d0, 5.0e9, 0);\n frame c0 = newframe(a0, 5.0e9, 0);\n}\ndefcal x $0 { play(q0, gaussian(0.4, 64dt, 16dt)); }\ndefcal measure $0 -> bit { return capture_v2(c0, 100dt); }\nbit c;\nx $0;\nc = measure $0;\n";
use super::*;
use wai_quantum::engine::views::compact_bits;
@ -787,7 +807,7 @@ mod tests {
#[test]
fn the_tools_are_these() {
let names: Vec<String> = tools().as_array().unwrap().iter().map(|t| t["name"].as_str().unwrap().to_string()).collect();
assert_eq!(names, ["run_quantum_circuit", "run_quantum_set", "dry_run_quantum_job", "get_quantum_job", "verify_quantum_receipt", "critique_qasm", "check_equivalent", "improve_qasm", "classical_shortcuts", "compile_qasm", "estimate_resources", "repair_qasm", "quantum_service_info", "qasm_diagnostics", "list_targets", "show_target", "run_on_target", "quantum_circuit_examples", "qec_code", "qec_decode", "qec_threshold"]);
assert_eq!(names, ["run_quantum_circuit", "run_quantum_set", "dry_run_quantum_job", "get_quantum_job", "verify_quantum_receipt", "critique_qasm", "check_equivalent", "improve_qasm", "classical_shortcuts", "compile_qasm", "estimate_resources", "repair_qasm", "quantum_service_info", "qasm_diagnostics", "list_targets", "show_target", "run_on_target", "quantum_circuit_examples", "qec_code", "qec_decode", "qec_threshold", "pulse_schedule", "pulse_check"]);
let run = &tools()[0]["inputSchema"]["properties"]["compile"]["properties"];
assert!(run.get("epsilon").is_some() && run.get("basis").is_some() && run.get("target").is_none(), "the compile's gate set is its basis; target is a device: {run}");
}
@ -802,7 +822,7 @@ mod tests {
}
let all = tools();
let names: Vec<&str> = all.as_array().unwrap().iter().map(|t| t["name"].as_str().unwrap()).collect();
assert_eq!(names, ["run_quantum_circuit", "run_quantum_set", "dry_run_quantum_job", "get_quantum_job", "verify_quantum_receipt", "critique_qasm", "check_equivalent", "improve_qasm", "classical_shortcuts", "compile_qasm", "estimate_resources", "repair_qasm", "quantum_service_info", "qasm_diagnostics", "list_targets", "show_target", "run_on_target", "quantum_circuit_examples", "qec_code", "qec_decode", "qec_threshold"]);
assert_eq!(names, ["run_quantum_circuit", "run_quantum_set", "dry_run_quantum_job", "get_quantum_job", "verify_quantum_receipt", "critique_qasm", "check_equivalent", "improve_qasm", "classical_shortcuts", "compile_qasm", "estimate_resources", "repair_qasm", "quantum_service_info", "qasm_diagnostics", "list_targets", "show_target", "run_on_target", "quantum_circuit_examples", "qec_code", "qec_decode", "qec_threshold", "pulse_schedule", "pulse_check"]);
// Each declares its output, versioned; a tool that reads a program requires its verdict.
for t in all.as_array().unwrap() {
let (name, o) = (t["name"].as_str().unwrap(), &t["outputSchema"]);
@ -964,6 +984,11 @@ mod tests {
("qec_decode", json!({ "memory": { "distance": 3, "rounds": 3, "p": 0.002 }, "decoder": "correlated", "detectors": [0, 1] }), false),
("qec_threshold", json!({ "family": "color", "distances": [3, 5], "p": [0.03], "shots": 200 }), false),
("qec_decode", json!({ "code": "bb:72", "decoder": "union_find", "error": [0] }), true),
// A pulse program's schedule, its run as a job, and a refusal.
("pulse_schedule", json!({ "qasm": PULSE }), false),
("pulse_schedule", json!({ "qasm": "OPENQASM 3.0;\ncal { play(f, constant(0.1, 1dt)); }\n" }), true),
("run_quantum_circuit", json!({ "qasm": PULSE, "method": "pulse", "shots": 50 }), false),
("pulse_check", json!({ "request": {}, "result": {} }), true),
] {
let r = rpc("tools/call", json!({ "name": name, "arguments": args }));
assert_eq!(r["result"]["isError"], refused, "{name}: {r}");
@ -974,6 +999,16 @@ mod tests {
}
called.insert(name.to_string());
}
// A pulse job sealed as a host seals one, checked here by running it again.
let pulse_key = wai_quantum::ed25519_dalek::SigningKey::from_bytes(&[3; 32]);
let job = wai_quantum::engine::job::run(&json!({ "qasm": PULSE, "method": "pulse", "shots": 20 }), 1 << 30, &pulse_key, json!({ "unmetered": "a test" })).unwrap();
let c = rpc("tools/call", json!({ "name": "pulse_check", "arguments": { "request": job["request"], "result": job["result"] } }));
assert_eq!(c["result"]["structuredContent"]["result"]["verified"], true, "{c}");
let schema = &all.as_array().unwrap().iter().find(|t| t["name"] == "pulse_check").unwrap()["outputSchema"];
let errs = Registry::empty().check_value(schema, &c["result"]["structuredContent"]);
if !errs.is_empty() {
problems.push(format!("pulse_check: {}\n the answer: {}", errs.join("\n "), c["result"]["structuredContent"]));
}
// An estimate's receipts, checked and re-run here.
let est = rpc("tools/call", json!({ "name": "estimate_resources", "arguments": { "counts": { "numQubits": 4, "tCount": 8, "measurementCount": 4 } } }));
let body = &est["result"]["structuredContent"];

View file

@ -87,6 +87,11 @@ quantum_cutrank = ["quantum_tableau", "quantum_sv"]
# for every surface, each refusal and warning with a stable diagnostic code
# (src/qasm.rs, src/qasm/). The bit-exact circuit's toolchain reads through it.
quantum_lang = ["quantum"]
# Pulse programs: OpenPulse read into a schedule on frames (src/qasm/pulse.rs)
# and integrated under a Lindblad master equation by a fixed-step integrator
# whose result is the same bits everywhere and whose error is bounded and
# stated (src/quantum_pulse.rs).
quantum_pulse = ["quantum_lang"]
# The reader's program lowered to each method's input (src/qasm.rs).
quantum_qasm = ["quantum_lang", "quantum_sv", "quantum_spd", "quantum_tableau"]
quantum_mpsim = ["quantum_sv", "quantum_mps"]
@ -97,7 +102,7 @@ quantum_mpsim = ["quantum_sv", "quantum_mps"]
# gates (quantum_toolchain). The resource model and the decoders' memory
# experiment come with it: a program's estimate (engine::estimate) is
# calibrated from the decoders' own threshold fits.
quantum_engine = ["quantum_qasm", "quantum_tn", "quantum_cutrank", "quantum_mpsim", "quantum_noisy", "quantum_toolchain", "quantum_resource", "quantum_frame", "dep:serde", "serde_json/float_roundtrip"]
quantum_engine = ["quantum_qasm", "quantum_tn", "quantum_cutrank", "quantum_mpsim", "quantum_noisy", "quantum_toolchain", "quantum_resource", "quantum_frame", "quantum_pulse", "dep:serde", "serde_json/float_roundtrip"]
quantum_pauli = ["quantum"]
# Sparse Pauli dynamics at utility scale: arbitrary-angle Pauli rotations on up
# to 1024 qubits, reproducible bit for bit, with a rigorous truncation bound.
@ -228,7 +233,7 @@ quantum_phasor_meter = ["_signing"]
# Everything.
full = [
"quantum", "quantum_toolchain", "quantum_mps", "quantum_stabilizer", "quantum_tableau", "quantum_cutrank", "quantum_lang", "quantum_qasm", "quantum_engine", "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_lang", "quantum_pulse", "quantum_qasm", "quantum_engine", "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

@ -90,6 +90,7 @@ never creates information.
| `quantum_receipt` | signed, energy-accounted receipt over a reconstruction |
| `quantum_ops` | the operations/attestation layer |
| `quantum_cal`, `quantum_control`, `quantum_noise` | calibration, filter-function robust control, DD noise spectroscopy, Cycle-Benchmarking noise learning |
| `quantum_pulse` | **pulse programs**: OpenPulse calibrations read into a schedule on frames (`qasm::pulse`: ports, frames, the waveform templates and explicit samples, plays, delays, phase and frequency changes, captures, barriers) and integrated under a Lindblad master equation (relaxation and dephasing from T1 and T2, two or three levels, static couplings) by a fixed-step series whose result is the same bits on every platform and whose error is bounded and stated; in a job, the method `pulse` (`engine::pulse`), its receipt binding the control program, its calibrations, the device and target snapshot and the integrator ([docs/pulse.md](docs/pulse.md)) |
| `quantum_mitigate` | zero-noise extrapolation, readout M3, classical shadows; in a job, `engine::mitigate` (with `quantum_noise`): zero-noise extrapolation, error amplification and cancellation from a learned noise model, Clifford data regression, virtual distillation and readout inversion, each value raw and mitigated with its error bar and the method's assumptions ([docs/qec.md](docs/qec.md)) |
| `quantum_qec` | CSS codes (distance verified by exhaustive search, not asserted) — **rotated surface** `[[d²,1,d]]`, toric, bivariate-bicycle, and the **`[[7,1,3]]` colour code**, whose self-duality buys a *transversal* logical Hadamard — one round, no ancillas, no surgery — verified on the simulator, with the surface code shown failing the same move — and the **triangular 6.6.6 colour codes** (`quantum_color`, decoded by restriction to matching), each code's **distance certified** by an exhaustive search over connected supports (`quantum_distance`) — with decoders: union-find (weighted matching, single-shot, space-time with faulty measurement, **and circuit-level with hook errors**; **imports and exports** Stim-syntax detector error models, so other tools' noise models decode here and ours decode there) and Relay-BP (for qLDPC) |
| `quantum_frame` | **noisy Clifford circuits at QEC scale**: reads the ecosystem's circuit text (`quantum_qec_text`: tags, Pauli-target observables, coordinates, and the error-model text with its `^` decompositions, read as written and written back; [conformance vectors](../quantum-conformance/qec-text/)); samples detection events by bit-packed Pauli-frame simulation (64 shots per word); derives the detector error model by backward sensitivity, refusing non-deterministic detectors; generates the rotated surface-code memory experiment byte for byte as the reference layout; and decodes by **exact minimum-weight perfect matching** (Edmonds' blossom on integer weights). Measures logical error rates and fits the code model resource estimates assume. Correlated matching through a model's decomposed faults (`quantum_match`), and sliding-window decoding as the syndrome arrives (`quantum_window`) |
@ -481,6 +482,20 @@ test: every error below half the distance corrects to a trivial residual
What each decoder guarantees, the window's latency budget, and each
mitigation method's assumptions are on [docs/qec.md](docs/qec.md).
## Pulse and dynamics
A pulse program gives its gates as the control signals that make them:
OpenPulse calibrations on physical qubits. `wai-quantum pulse schedule`
draws its schedule; `wai-quantum run` with `--method pulse` integrates it
under a master equation, the result bounded and the same bits on every
platform; `wai-quantum pulse check` holds a sealed pulse job to its request
by running it again. The same run is a job on every surface (both MCP
servers' run tools, the job service, the page), and both MCP servers serve
`pulse_schedule` and `pulse_check`. Its receipt binds the control program,
its calibrations, the device and the target snapshot it ran on, and the
integrator and its step. The model, the bound and the options are on
[docs/pulse.md](docs/pulse.md).
## State vector
```sh runnable

View file

@ -73,6 +73,7 @@ wai-quantum lab qutrit # d = 3,不只是量子比特
| `quantum_receipt` | 针对一次重构的、带能耗核算的签名回执 |
| `quantum_ops` | 操作/证明(operations/attestation)层 |
| `quantum_cal`, `quantum_control`, `quantum_noise` | 校准、滤波函数鲁棒控制、DD 噪声谱学、Cycle-Benchmarking 噪声学习 |
| `quantum_pulse` | **脉冲程序**:把 OpenPulse 校准读入为帧上的时序(`qasm::pulse`:端口、帧、波形模板与显式采样、播放、延迟、相位与频率的改变、采集、屏障),并在 Lindblad 主方程下积分(由 T1 和 T2 给出弛豫与退相,二能级或三能级,静态耦合),所用的定步长级数在每个平台上结果比特一致,误差有界并予以说明;在作业中即方法 `pulse`(`engine::pulse`),其回执绑定控制程序、其校准、设备与目标快照以及积分器([docs/pulse.md](docs/pulse.md)) |
| `quantum_mitigate` | 零噪声外推、读出 M3、经典影子 |
| `quantum_qec` | CSS 码(码距通过穷举搜索验证,而非仅凭声称)——**旋转表面码** `[[d²,1,d]]`、环面码、双变量自行车码,以及 **`[[7,1,3]]` 色码**,其自对偶性带来一个*横向*(transversal)逻辑 Hadamard——一轮完成,无需辅助比特,无需手术——已在模拟器上验证,并展示了表面码在同样操作下的失败——配有**两个解码器**:并查集(加权匹配、单次(single-shot)、带错误测量的时空解码,**以及带钩子错误(hook error)的线路级解码**;可**导入和导出** Stim 语法的探测器误差模型,因此其他工具的噪声模型可以在这里解码,我们的也可以在那里解码)以及 Relay-BP(用于 qLDPC) |
| `quantum_frame` | **QEC 规模的含噪声 Clifford 线路**:读取生态系统通用的线路文本;通过按位打包的 Pauli 帧模拟(每个字 64 次采样)对探测事件采样;通过反向灵敏度推导探测器误差模型,拒绝非确定性的探测器;按参考布局逐字节生成旋转表面码存储实验;并通过**精确的最小权完美匹配**(基于整数权重的 Edmonds 开花算法)解码。测量逻辑错误率,并拟合资源估算所假定的码模型 |
@ -307,6 +308,10 @@ wai-quantum qec threshold --distances 3,5,7 --p 0.002,0.004 --shots 20000
`engine::qec` 是命令行、两个 MCP 服务器(`qec_code`、`qec_decode`、`qec_threshold`)、作业服务(`POST /v1/qec/{code,decode,threshold}`)、Studio 的纠错面板、桌面应用和 Python(`wai_quantum.qec_code` 等)共用的同一个调用,每个回答都以作业的形式封存。一个码附带**距离证书**:在每个扇区中,找到的逻辑算符的最小权重、该权重的一个见证,以及在连通支撑上经穷举搜索排除的每个更轻的支撑,可以逐个节点地重新检查。每个解码器都经受同样的穷举测试:低于距离一半的每个错误都纠正为平凡残差(在线路级模型上,则是低于图距离一半的每个边集)。每个解码器保证什么、窗口的延迟预算,以及每种缓解方法的假设,见 [docs/qec.md](docs/qec.md)。
## 脉冲与动力学
脉冲程序以产生各个门的控制信号来给出这些门:作用于物理量子比特的 OpenPulse 校准。`wai-quantum pulse schedule` 画出它的时序;`wai-quantum run` 加上 `--method pulse` 在主方程下对它积分,结果有界,并且在每个平台上比特一致;`wai-quantum pulse check` 通过重新运行,把一个已封存的脉冲作业与其请求对照检查。同样的运行在每个入口上都是作业(两个 MCP 服务器的运行工具、作业服务、页面),两个 MCP 服务器都提供 `pulse_schedule` 和 `pulse_check`。其回执绑定控制程序、其校准、它所运行的设备与目标快照,以及积分器及其步长。模型、误差上界和选项见 [docs/pulse.md](docs/pulse.md)。
## 态矢量
```sh runnable

View file

@ -19,7 +19,7 @@ with the engine:
- with the command line given: a run's job, a set's, the versions it reports, and the
answer of each of its MCP server's tools that needs no file (the verdict,
the repair, the compile, the estimate and its check, the debugger, the
device targets, error correction and mitigation among them), called over
device targets, error correction, mitigation and pulse runs among them), called over
standard input and output, each follow their schema.
Exit status 0 when everything holds; 1 with the first failure otherwise.
@ -150,6 +150,10 @@ def main() -> int:
"c[0] = measure q[0];\nc[1] = measure q[1];\nif (c[1]) x q[2];\nif (c[0]) z q[2];\nc[2] = measure q[2];\n")
pair = "OPENQASM 3.0;\nqubit[2] q;\nbit[2] c;\nh q[0];\ncx q[0], q[1];\nc = measure q;\n"
rot = "OPENQASM 3.0;\nqubit[3] q;\nh q;\nccx q[0], q[1], q[2];\nrz(0.3) q[0];\n"
pulse = ("OPENQASM 3.0;\ndefcalgrammar \"openpulse\";\ncal {\n extern port d0;\n extern port a0;\n"
" frame q0 = newframe(d0, 5.0e9, 0);\n frame c0 = newframe(a0, 5.0e9, 0);\n}\n"
"defcal x $0 { play(q0, gaussian(0.4, 64dt, 16dt)); }\ndefcal measure $0 -> bit { return capture_v2(c0, 100dt); }\n"
"bit c;\nx $0;\nc = measure $0;\n")
calls = [
("run_quantum_circuit", {"qasm": bell, "shots": 20}),
("run_quantum_circuit", {"qasm": pair, "shots": 64, "repair": True, "intent": [
@ -190,6 +194,9 @@ def main() -> int:
("qec_threshold", {"family": "color", "distances": [3, 5], "p": [0.03], "shots": 200}),
("run_quantum_circuit", {"qasm": rot, "observables": ["Z0 Z1"], "noise": {"depolarizing_2q": 0.02}, "mitigate": "zne"}),
("compile_qasm", {"qasm": bell, "basis": "standard", "twirl": {"seed": 3}, "dd": "xy4"}),
("pulse_schedule", {"qasm": pulse}),
("run_quantum_circuit", {"qasm": pulse, "method": "pulse", "shots": 50}),
("plan_quantum_circuit", {"qasm": pulse, "method": "pulse"}),
]
local = lists["wai-quantum-cli/docs/local-mcp.json"]
@ -212,7 +219,11 @@ def main() -> int:
# An estimate's check takes the estimate this server wrote.
est = got[[n for n, _ in calls].index("estimate_resources")]
session([("verify_quantum_result", {"job": est})])
print(f"{binary}: run --json, version --json, initialize and {len(calls) + 1} tool answers follow their schemas")
# A pulse job's check takes the pulse job this server ran.
ran = got[[i for i, (n, a) in enumerate(calls) if a.get("method") == "pulse" and n == "run_quantum_circuit"][0]]
checked = session([("pulse_check", {"request": ran["request"], "result": ran["result"]})])[0]
assert checked["result"]["verified"] is True, checked
print(f"{binary}: run --json, version --json, initialize and {len(calls) + 2} tool answers follow their schemas")
return 0

View file

@ -35,6 +35,9 @@ run one says why.
- [The job layer's refusal codes](job-refusals.md).
- [Error correction and mitigation](qec.md): codes with their certified
distances, the decoders, thresholds, and mitigation in a job.
- [Pulse and dynamics](pulse.md): pulse programs read into schedules on
frames, integrated under a master equation with their error bounded and
stated, the same bits everywhere, and what their receipts bind.
## Other surfaces

View file

@ -22,6 +22,7 @@
- [读取器的诊断](qasm-diagnostics.md),以及[它如何处理每种构造](qasm-feature-matrix.md)。
- [作业层的拒绝代码](job-refusals.md)。
- [纠错与误差缓解](qec.md):码及其经过认证的距离、解码器、阈值,以及作业中的误差缓解。
- [脉冲与动力学](pulse.md):读入为帧上时序的脉冲程序,在主方程下积分并给出误差上界,在每个平台上比特一致,以及其回执所绑定的内容。
## 其他入口

View file

@ -71,6 +71,8 @@ browser engine. A test holds this page to the code's table.
| `J031` | refused | A job or a check larger than this host takes: run it on your own machine, with the command line or its MCP server. |
| `J032` | retry | The host is full (its queue, or the checks running at once): send it again later. |
| `J033` | refused | A host that takes requests only with one of its API keys: give yours as `Authorization: Bearer <key>`; a key it does not hold is refused the same way. |
| `J040` | refused | A pulse run that does not read or cannot be done: a field it does not take, options or device parameters that do not read, a target without the qubit, or a schedule outside the simulator's model (two tones on one qubit at once, a play after its qubit's capture, more levels and qubits than it holds, a frame far from its qubit); the message says which. |
| `J041` | refused | A pulse run whose integration error cannot be stated within its tolerance: a step longer than the generator's norm allows, or a schedule so long that the bound or the rounding passes the tolerance; the message names the steps that would meet it, or says that none does. |
| `J100` | dropped | A line of an answer that is not a statement (`name = View(...)`), a program (a fenced `qasm` block with its name), a comment or a marker: left out. |
| `J101` | dropped | A view the answer layer does not have: left out (the views are listed in the layer's specification). |
| `J102` | dropped | An `@` call the engine does not have: the statement is left out. |

View file

@ -0,0 +1,145 @@
# Pulse and dynamics
A pulse program is OpenQASM 3 whose gates are given as the control signals
that make them: OpenPulse calibrations (`defcal`) on physical qubits, played
on frames of ports. The engine reads one into a schedule, integrates the
schedule under a Lindblad master equation, and answers as a job of the
method `pulse`: the result is the same bits on every platform, its error is
bounded and stated, and its receipt binds the control program, its
calibrations, the device and target snapshot it ran on, and the integrator
and its step. A pulse result can be checked by anyone by running its request
again.
This is a simulation. The engine reaches no device's pulse interface.
## A program
```qasm file=x.qasm
OPENQASM 3.0;
defcalgrammar "openpulse";
cal {
extern port d0;
extern port a0;
frame q0 = newframe(d0, 5.0e9, 0);
frame c0 = newframe(a0, 5.0e9, 0);
}
defcal x $0 { play(q0, gaussian(0.4, 64dt, 16dt)); }
defcal measure $0 -> bit { return capture_v2(c0, 100dt); }
bit c;
x $0;
c = measure $0;
```
```sh runnable
wai-quantum pulse schedule x.qasm --demo-key # each frame's timeline, the gate calls, the waveforms
wai-quantum run x.qasm --method pulse --shots 1000 --demo-key # integrated: the capture's reading, the bound, what the receipt binds
wai-quantum run x.qasm --method pulse --shots 1000 --demo-key --json > job.json
wai-quantum pulse check job.json --demo-key # the receipt, the program's digest, and the run again, bit for bit
wai-quantum run x.qasm --method pulse --target synthetic-line-5 --demo-key # the qubit's frequency, T1 and T2 from a target's calibration
wai-quantum run x.qasm --method pulse --pulse '{"levels": 3, "qubits": {"0": {"anharmonicity_mhz": -220}}}' --demo-key # three levels: the leakage shown
wai-quantum run x.qasm --method pulse --pulse '{"tolerance": 1e-15}' --demo-key # exits 1
```
The last run is refused with `J041`: the rounding of a schedule this long in
double precision is estimated above the tolerance asked, and the message says
so rather than giving a number it cannot stand behind.
## What the reader takes
`qasm::pulse` runs the program in order, each calibration's body at its call:
- **Ports** reach a qubit by name: `d3` drives qubit 3, `m3` is its
measurement port and `a3` its acquisition port. The job's `pulse.ports`
maps other names (`{"u0": {"kind": "drive", "qubit": 1}}`).
- **Frames** are made in `cal` blocks with `newframe(port, frequency, phase)`
(hertz and radians). Each has a time of its own. A play starts at its
frame's time and moves it on by the waveform's length; `delay[d] f` moves
it on; `barrier f, g` moves both to the later. A gate starts when its
qubits are free, all the frames its calibration names together.
- **Waveforms**: `constant`, `gaussian`, `sech`, `gaussian_square`, `drag`
and `sine`, and explicit samples `{s0, s1, …}`, real or complex. A
template's sample k is taken at t = k − (l − 1)/2 samples, centred and not
lifted; the exponential and the sine are the engine's own, so every sample
is the same bits everywhere.
- **Frame changes**: `shift_phase`, `set_phase`, `shift_frequency` and
`set_frequency`. A frame's carrier runs on continuously across a frequency
change.
- **Captures**: `capture` and `capture_v0` to `capture_v4`, each read as a
discriminated bit, written to the program's bit when a measurement's
calibration returns it.
- **Time** is counted in samples of `dt` (`pulse.dt_ns`, 0.5 ns unless
given). A duration in nanoseconds that is not a whole number of samples is
refused (`Q458`).
What it does not take is refused with a code and its place: `Q454` to
`Q461`, listed in [the diagnostic codes](qasm-diagnostics.md). The circuit
methods keep the same calibrations verbatim and run none of them (`Q450`,
`Q451`); the pulse reader runs them (`Q453`).
## The model and the bound
`quantum_pulse` holds each qubit as a ladder of two or three levels in the
frame rotating at its frequency, under the rotating-wave approximation, the
envelope held for each sample. The drive is what the qubit's drive frames
play, at their carriers' phases; at phase 0 a real envelope turns the qubit
about +X. Relaxation and pure dephasing come from T1 and T2, and pairs of
qubits may have a static ZZ coupling. A capture reads its qubit's state at
its first sample and ends that qubit's schedule, since the readout's
back-action is not modelled; plays on measurement ports are scheduled and
timed and drive nothing.
Within a sample each driven qubit hears one tone, so in the frame of that
tone the generator is constant. Each sample is a fixed number of steps of a
Taylor series of order 12, then a diagonal phase back to the qubits' frames.
Only `+ − × ÷` and `sqrt` are used, in a fixed order, so the density matrix
is the same bits on x86_64, arm64 and in wasm32; CI pins its digest on each.
The bound: with λ an upper bound on the generator's norm, each step leaves
out at most (hλ)^13/13! / (1 − hλ/14) of the state, and the exact evolution
is a contraction in the trace norm, so the final state is within T(1 + T) of
the exact solution of the model's equation, T the sum over every step. Every
probability the result gives is within that, stated as
`accuracy.truncation_bound`. Rounding is stated beside it as an estimate
(`accuracy.rounding_estimate`), not a proof; `accuracy.bound` is their sum.
A step longer than 1/λ, or a schedule whose bound and rounding would pass
the tolerance, is refused (`J041`), naming the steps that would meet it.
The bound is the distance from the model's exact solution; it says nothing
of how far the model is from a device. The tests hold the integrator to
closed forms within the bound it states: a Rabi turn, the axis a quarter
turn of phase gives, a detuned drive, T1 decay and T2 dephasing.
## What a receipt binds
A pulse run's result holds, inside what its receipt seals:
| Field | What it is |
|---|---|
| `pulse.calibration.program` | the BLAKE3 of the control program's text |
| `pulse.calibration.calibrations` | the BLAKE3 of its `cal`, `defcal` and `defcalgrammar` statements as written |
| `pulse.calibration.schedule` | the BLAKE3 of the schedule's JSON |
| `pulse.calibration.device` | the BLAKE3 of the device's JSON (`pulse.device`, each parameter with where it came from) |
| `pulse.calibration.target` | the target snapshot's name, hash and calibration id, when one is named |
| `pulse.integrator` | the integrator, its order, its step and the steps taken |
| `pulse.density_hash` | the BLAKE3 of the final density matrix's bits |
`wai-quantum pulse check`, the `pulse_check` tool on both MCP servers and
`POST /v1/pulse/check` verify the receipt, hold the program's digest to the
request's text, and run the request again here, naming every field whose
value differs. A pulse result that does not reproduce was not made from that
request by this engine.
## The options
The job's `pulse` object (every field optional): `dt_ns`, `levels` (2 or 3),
`tolerance` (1e-6 unless given), `substeps` (chosen so each step times λ is
at most 0.5 unless given), `qubits` (by index: `frequency_ghz`, `rabi_mhz`,
`anharmonicity_mhz`, `t1_us`, `t2_us`), `couplings`
(`[{"qubits": [0, 1], "zz_khz": 50}]`) and `ports`. A qubit's parameters come
from `pulse.qubits`, then the target's calibration, then the defaults: 5 GHz
plus 100 MHz per index, a Rabi frequency of 50 MHz at full amplitude, an
anharmonicity of −300 MHz, and no decay.
The simulator holds a space of up to 16 (four qubits at two levels, two at
three), a frame within 1 GHz of its qubit, one tone per qubit at a time, and
schedules of up to 2²² samples.

View file

@ -125,6 +125,16 @@ engine. A test holds this page to the code's table.
| `Q450` | preserved | `defcal`: kept verbatim for export, never executed; the gate's matrix comes from its `gate` definition or the standard library. |
| `Q451` | preserved | `cal { … }`: kept verbatim for export, never executed. |
| `Q452` | preserved | `defcalgrammar`: kept verbatim for export. |
| `Q453` | lowered | In a pulse run, a `cal` or `defcal` body read as OpenPulse and lowered to the schedule on frames (plays, delays, phase and frequency changes, captures and barriers) that the pulse simulator integrates. |
| `Q454` | refused | In a pulse run, a `defcalgrammar` other than `"openpulse"`: the pulse reader reads OpenPulse alone. |
| `Q455` | refused | A statement in a calibration that the pulse reader does not take: classical control, a frame's attributes or `get_phase`, an `extern` function, a frame made inside a `defcal`, a nested calibration, or a `defcal` over qubit arguments (the message names it and what to write instead). |
| `Q456` | refused | A waveform that does not read: a template the reader does not know, arguments of the wrong number or kind, a length or a width that is not positive, a sample that is not a number, or more samples than it holds. |
| `Q457` | refused | A frame or a port that does not read: a play, a delay or a phase or frequency change on a name that is not a frame, `newframe` on a name that is not a port, a frequency that is not a number, or a port that reaches no qubit. |
| `Q458` | refused | A duration in a calibration that is not a whole, non-negative number of samples at the sample period (`dt`), or a schedule longer than the reader holds. |
| `Q459` | refused | In a pulse run, a gate or a measurement on physical qubits that no `defcal` calibrates for those qubits and those parameters. |
| `Q460` | refused | In a pulse run, a statement outside the calibrations that the pulse reader does not take: declared qubits (a pulse program names physical qubits, `$0`), a modifier, `reset`, classical control, a subroutine, or a measurement into something other than a bit. |
| `Q461` | refused | A capture that does not read: not on a frame of an acquisition port, without a duration or a waveform, or its result put somewhere other than a bit. |
| `Q462` | warned | A play on a measurement port: scheduled and timed, its effect on the readout not simulated (a capture reads the qubit's state). |
| `Q500` | refused | An angle that is not a dyadic multiple of 2π (2πm/2^k, k ≤ 32): the bit-exact method refuses it, never rounds it; the methods in double precision take it. |
| `Q501` | refused | More than 255 qubits for the bit-exact circuit. |
| `Q502` | refused | A program that measures a qubit and then acts on it, resets one after its first gate, or acts on measured bits: the bit-exact circuit takes programs whose measurements all come at the end. |

View file

@ -162,9 +162,15 @@ and in the browser build.
| Construct | Status | Codes |
|---|---|---|
| `defcalgrammar "openpulse";` | preserved | Q452 |
| `cal { … }` | preserved, never run | Q451 |
| `defcal …` | preserved, never run; the gate's matrix comes from its definition or the library | Q450, Q126 |
| `defcalgrammar "openpulse";` | preserved; in a pulse run, read (another grammar is refused) | Q452, Q454 |
| `cal { … }` | preserved, never run by the circuit methods; in a pulse run, read as OpenPulse and run in order | Q451, Q453 |
| `defcal …` | preserved, never run by the circuit methods; the gate's matrix comes from its definition or the library. In a pulse run, read as OpenPulse and run at each call on its physical qubits | Q450, Q126, Q453, Q459 |
| Ports, frames, waveforms (`constant`, `gaussian`, `sech`, `gaussian_square`, `drag`, `sine`, explicit samples) | in a pulse run, lowered to the schedule | Q453, Q455 to Q458 |
| `play`, `delay`, `shift_phase`, `set_phase`, `shift_frequency`, `set_frequency`, `capture_v0` to `capture_v4`, `barrier` | in a pulse run, lowered to the schedule on frames | Q453, Q457, Q458, Q461 |
| Plays on a measurement port | in a pulse run, scheduled and timed; the readout is not simulated | Q462 |
A pulse run is the job method `pulse`; [the pulse page](pulse.md) gives the
model, the bound and the options.
## The bit-exact method

View file

@ -35,7 +35,7 @@
},
"method": {
"type": "string",
"description": "auto (the default: the strongest method the program admits), or a method by its name as the methods list names it."
"description": "auto (the default: the strongest method the program admits), or a method by its name as the methods list names it; pulse runs a pulse program (OpenPulse calibrations) under the pulse options."
},
"observables": {
"type": "array",
@ -235,6 +235,121 @@
]
}
]
},
"pulse": {
"type": "object",
"description": "A pulse run's options (method \"pulse\"): the sample period, the levels kept, the tolerance on the stated error, the steps a sample is divided into, each qubit's parameters, static couplings, and ports beyond the names d0, m0, a0.",
"additionalProperties": false,
"properties": {
"dt_ns": {
"type": "number",
"exclusiveMinimum": 0,
"maximum": 1000,
"description": "The sample period, ns (default 0.5)."
},
"levels": {
"type": "integer",
"enum": [
2,
3
],
"description": "Levels kept per qubit (default 2; 3 shows leakage)."
},
"tolerance": {
"type": "number",
"exclusiveMinimum": 0,
"exclusiveMaximum": 1,
"description": "The most the stated error may be (default 1e-6): a run whose bound and rounding estimate pass it is refused, naming the steps that would meet it."
},
"substeps": {
"type": "integer",
"minimum": 1,
"maximum": 4096,
"description": "Steps per sample (default: chosen so each step times the generator's norm is at most 0.5)."
},
"qubits": {
"type": "object",
"description": "Each qubit's parameters by its index, over the target's and the defaults.",
"additionalProperties": {
"type": "object",
"additionalProperties": false,
"properties": {
"frequency_ghz": {
"type": "number",
"description": "Its frequency, GHz."
},
"rabi_mhz": {
"type": "number",
"description": "Its Rabi frequency at full amplitude, MHz."
},
"anharmonicity_mhz": {
"type": "number",
"description": "Its anharmonicity, MHz."
},
"t1_us": {
"type": "number",
"description": "Its relaxation time, µs."
},
"t2_us": {
"type": "number",
"description": "Its coherence time, µs (at most 2·T1)."
}
}
}
},
"couplings": {
"type": "array",
"description": "Static ZZ couplings.",
"items": {
"type": "object",
"required": [
"qubits",
"zz_khz"
],
"additionalProperties": false,
"properties": {
"qubits": {
"type": "array",
"items": {
"type": "integer",
"minimum": 0
},
"minItems": 2,
"maxItems": 2
},
"zz_khz": {
"type": "number",
"description": "The coupling's rate, kHz."
}
}
}
},
"ports": {
"type": "object",
"description": "Ports by name: what each is for and the qubit it reaches.",
"additionalProperties": {
"type": "object",
"required": [
"kind",
"qubit"
],
"additionalProperties": false,
"properties": {
"kind": {
"enum": [
"drive",
"measure",
"acquire"
]
},
"qubit": {
"type": "integer",
"minimum": 0
}
}
}
}
}
}
},
"$defs": {

View file

@ -398,6 +398,237 @@
},
"mitigation": {
"$ref": "mitigation.v1.json"
},
"pulse": {
"type": "object",
"description": "A pulse run (method pulse, engine::pulse): what was read and its distribution, the final populations, each capture's reading, the final density matrix's digest, the schedule's length, the integrator and its step, the device and where each parameter came from, and what the receipt binds (calibration).",
"required": [
"read",
"outcome_bits",
"populations",
"simulated_qubits",
"density_hash",
"integrator",
"device",
"calibration"
],
"properties": {
"read": {
"type": "string",
"description": "What the distribution reads: the captures together (into the program's bits when each writes one), or the final state."
},
"outcome_bits": {
"type": "array",
"items": {
"type": "string"
},
"description": "What each character of an outcome is, left to right: a program bit (c[0]) or a qubit ($0)."
},
"distribution": {
"type": [
"object",
"null"
],
"additionalProperties": {
"type": "number"
},
"description": "The probability of each outcome, which the counts sample; null when the captures start at different samples."
},
"populations": {
"type": "object",
"additionalProperties": {
"type": "number"
},
"description": "The final state read on every simulated qubit, the highest qubit first, a level above 0 read as 1."
},
"simulated_qubits": {
"type": "array",
"items": {
"type": "integer"
}
},
"captures": {
"type": "array",
"items": {
"type": "object",
"properties": {
"frame": {
"type": "string"
},
"qubit": {
"type": "integer"
},
"start": {
"type": "integer"
},
"start_ns": {
"type": "number"
},
"bit": {
"type": [
"string",
"null"
]
},
"p1": {
"type": "number"
},
"p2": {
"type": "number"
}
}
}
},
"leakage": {
"type": "array",
"items": {
"type": "object",
"properties": {
"qubit": {
"type": "integer"
},
"p2": {
"type": "number"
}
}
},
"description": "With three levels: each qubit's final probability of level 2."
},
"trace": {
"type": "number"
},
"density_hash": {
"type": "string",
"description": "The BLAKE3 of the final density matrix's bits: the same on every platform."
},
"duration": {
"type": "integer"
},
"duration_ns": {
"type": "number"
},
"integrator": {
"type": "object",
"properties": {
"name": {
"type": "string"
},
"order": {
"type": "integer"
},
"dt_ns": {
"type": "number"
},
"substeps": {
"type": "integer"
},
"step_ns": {
"type": "number"
},
"steps": {
"type": "integer"
},
"norm_max": {
"type": "number"
},
"frame": {
"type": "string"
}
}
},
"device": {
"type": "object",
"properties": {
"levels": {
"type": "integer"
},
"dt_ns": {
"type": "number"
},
"qubits": {
"type": "array",
"items": {
"type": "object",
"properties": {
"qubit": {
"type": "integer"
},
"frequency_hz": {
"type": "number"
},
"rabi_hz": {
"type": "number"
},
"anharmonicity_hz": {
"type": "number"
},
"t1_ns": {
"type": "number"
},
"t2_ns": {
"type": "number"
},
"from": {
"type": "object",
"additionalProperties": {
"enum": [
"pulse",
"target",
"default",
"none"
]
}
}
}
}
},
"couplings": {
"type": "array"
},
"model": {
"type": "string"
}
}
},
"calibration": {
"type": "object",
"description": "What the receipt binds: the program's, its calibrations', the schedule's and the device's digests, and the target snapshot.",
"properties": {
"program": {
"type": "string"
},
"calibrations": {
"type": "string"
},
"schedule": {
"type": "string"
},
"device": {
"type": "string"
},
"target": {
"type": "object",
"properties": {
"name": {
"type": "string"
},
"hash": {
"type": "string"
},
"calibration": {
"type": "object"
}
}
}
}
},
"not_modelled": {
"type": "array",
"items": {
"type": "string"
}
}
}
}
},
"$defs": {
@ -473,6 +704,24 @@
},
"batch": {
"type": "integer"
},
"bound": {
"type": "number",
"description": "bounded (a pulse run): every probability within this of the exact solution of the model's master equation, the truncation bound and the rounding estimate together."
},
"truncation_bound": {
"type": "number",
"description": "A pulse run: the proved bound, in the trace norm, on what the integrator's series leaves out."
},
"rounding_estimate": {
"type": "number",
"description": "A pulse run: an estimate of floating-point rounding beside the bound, not a proof."
},
"tolerance": {
"type": "number"
},
"norm": {
"type": "string"
}
}
},

View file

@ -0,0 +1,413 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://git.transaction.science/Transaction-Science/open-standards/raw/branch/main/wai/wai-quantum/schemas/pulse.v1.json",
"title": "wai.quantum.pulse/1",
"description": "A pulse answer as every host gives it (engine::pulse: pulse_schedule and pulse_check on both servers, wai-quantum pulse, POST /v1/pulse/*, the Studio): the request, and the result sealed in a job's form. A pulse program's schedule on frames, or a pulse job checked by running it again. A pulse run itself is a job (method pulse) and follows job-result.v1.json. A result names the engine that made it and this schema (engine and schema). docs/pulse.md explains each.",
"type": "object",
"required": [
"request",
"result"
],
"properties": {
"request": {
"type": "object",
"description": "The call's request, as given."
},
"result": {
"$ref": "#/$defs/result"
}
},
"$defs": {
"result": {
"description": "A schedule or a check, with its receipt.",
"anyOf": [
{
"$ref": "#/$defs/schedule"
},
{
"$ref": "#/$defs/check"
}
]
},
"schedule": {
"type": "object",
"description": "A pulse program's schedule (pulse_schedule, wai-quantum pulse schedule, POST /v1/pulse/schedule): its frames and ports, every instruction at its sample, each waveform's samples, the gate calls, the reader's notes, and the digests a run binds.",
"required": [
"answer",
"dt_ns",
"duration",
"frames",
"waveforms",
"instructions",
"calls",
"calibration"
],
"properties": {
"engine": {
"type": "object",
"description": "The engine that made the result: the wai-quantum crate's name and version.",
"required": [
"name",
"version"
],
"properties": {
"name": {
"const": "wai-quantum"
},
"version": {
"type": "string"
}
}
},
"schema": {
"const": "wai.quantum.pulse/1"
},
"answer": {
"const": "schedule"
},
"dt_ns": {
"type": "number",
"description": "The sample period, ns."
},
"duration": {
"type": "integer",
"description": "The latest time of any frame, samples."
},
"duration_ns": {
"type": "number"
},
"grammar": {
"type": [
"string",
"null"
]
},
"ports": {
"type": "array",
"items": {
"type": "object",
"properties": {
"name": {
"type": "string"
},
"kind": {
"enum": [
"drive",
"measure",
"acquire"
]
},
"qubit": {
"type": "integer"
}
}
}
},
"frames": {
"type": "array",
"items": {
"type": "object",
"properties": {
"name": {
"type": "string"
},
"port": {
"type": "string"
},
"qubit": {
"type": "integer"
},
"kind": {
"enum": [
"drive",
"measure",
"acquire"
]
},
"frequency_hz": {
"type": "number"
},
"phase": {
"type": "number"
}
}
}
},
"waveforms": {
"type": "array",
"items": {
"type": "object",
"properties": {
"name": {
"type": "string"
},
"length": {
"type": "integer"
},
"samples": {
"type": "array",
"items": {
"type": "array",
"items": {
"type": "number"
},
"minItems": 2,
"maxItems": 2
}
},
"samples_every": {
"type": "integer",
"description": "Each k-th sample is drawn, when the schedule holds more than the view writes out."
}
}
}
},
"instructions": {
"type": "array",
"items": {
"type": "object",
"required": [
"frame",
"op",
"start",
"length"
],
"properties": {
"frame": {
"type": "integer"
},
"op": {
"enum": [
"play",
"delay",
"shift_phase",
"set_phase",
"shift_frequency",
"set_frequency",
"capture",
"barrier"
]
},
"start": {
"type": "integer"
},
"length": {
"type": "integer"
},
"line": {
"type": "integer"
},
"waveform": {
"type": "integer"
},
"phase": {
"type": "number"
},
"frequency_hz": {
"type": "number"
},
"carrier": {
"type": "number",
"description": "The carrier's phase at the play's first sample, cycles in [0, 1)."
},
"value": {
"type": "number"
},
"bit": {
"type": [
"string",
"null"
]
}
}
}
},
"calls": {
"type": "array",
"items": {
"type": "object",
"properties": {
"name": {
"type": "string"
},
"qubits": {
"type": "array",
"items": {
"type": "integer"
}
},
"params": {
"type": "array",
"items": {
"type": "number"
}
},
"start": {
"type": "integer"
},
"end": {
"type": "integer"
},
"line": {
"type": "integer"
}
}
}
},
"bits": {
"type": "array",
"items": {
"type": "string"
}
},
"qubits": {
"type": "array",
"items": {
"type": "integer"
}
},
"decimated": {
"type": "string"
},
"diagnostics": {
"type": "array",
"items": {
"type": "object",
"description": "A reader's note or warning (qasm::diag).",
"properties": {
"code": {
"type": "string"
},
"outcome": {
"type": "string"
},
"message": {
"type": "string"
},
"line": {
"type": "integer"
},
"column": {
"type": "integer"
},
"length": {
"type": "integer"
}
}
}
},
"calibration": {
"type": "object",
"description": "What a pulse run's receipt binds of the program: the BLAKE3 of the control program's text, of its calibrations as written (every cal, defcal and defcalgrammar, joined by newlines), and of the schedule's JSON; a run adds the device's and the target snapshot's.",
"required": [
"program",
"calibrations",
"schedule"
],
"properties": {
"program": {
"type": "string"
},
"calibrations": {
"type": "string"
},
"schedule": {
"type": "string"
},
"device": {
"type": "string"
},
"target": {
"type": "object",
"properties": {
"name": {
"type": "string"
},
"hash": {
"type": "string"
},
"calibration": {
"type": "object"
}
}
}
}
},
"receipt": {
"$ref": "receipt.v2.json"
}
}
},
"check": {
"type": "object",
"description": "A pulse job checked (pulse_check, wai-quantum pulse check, POST /v1/pulse/check): its receipt's signature, its program's digest, and the request run again with the result held to the one given.",
"required": [
"answer",
"verified",
"signature",
"program",
"reproduced",
"differences",
"statement"
],
"properties": {
"engine": {
"type": "object",
"description": "The engine that made the result: the wai-quantum crate's name and version.",
"required": [
"name",
"version"
],
"properties": {
"name": {
"const": "wai-quantum"
},
"version": {
"type": "string"
}
}
},
"schema": {
"const": "wai.quantum.pulse/1"
},
"answer": {
"const": "check"
},
"verified": {
"type": "boolean"
},
"signature": {
"type": "boolean"
},
"key": {},
"program": {
"type": "boolean"
},
"reproduced": {
"type": "boolean"
},
"differences": {
"type": "array",
"items": {
"type": "string"
},
"description": "Each field (or field.member) whose value differs from the run here."
},
"density_hash": {
"type": "object",
"properties": {
"given": {},
"recomputed": {}
}
},
"calibration": {},
"statement": {
"type": "string"
},
"receipt": {
"$ref": "receipt.v2.json"
}
}
}
}
}

View file

@ -118,6 +118,7 @@ pub fn nominal_rate(method: &str) -> Option<f64> {
"mps" => Some(MPS_MAX_WORK as f64 / 600.0),
"cutrank" | "cutrank_single" => Some(CUTRANK_MAX_WORK as f64 / 600.0),
"density_matrix" => Some(DENSITY_MAX_WORK as f64 / 180.0),
"pulse" => Some(1e9),
_ => None,
}
}
@ -148,6 +149,7 @@ pub fn describe(job_bytes: u64) -> Value {
{ "method": "cutrank_single", "accuracy": "single", "circuits": "as for cutrank; the table on a GPU, its arithmetic in single precision", "max_qubits": CUTRANK_MAX_QUBITS, "max_cut": gpu_cutrank_max_cut().unwrap_or(0), "available": gpu_cutrank_max_cut().is_ok(), "answers": ["counts", "amplitudes"] },
{ "method": "density_matrix", "accuracy": "exact", "circuits": "any, with noise", "max_qubits": density_max_qubits(job_bytes), "answers": ["counts", "expectations"] },
{ "method": "trajectories", "accuracy": "estimate", "circuits": "any, with noise", "max_qubits": TRAJECTORY_MAX_QUBITS.min(statevector_max_qubits(job_bytes)), "answers": ["counts", "expectations"], "options": { "trajectories": "how many (default: the shots, or 1000 for expectations alone); shots that take the same errors share one state" } },
super::pulse::describe(),
])
}
@ -866,6 +868,12 @@ pub fn trajectory_count(r: &Request) -> u64 {
/// The plan for a job's JSON, `job_bytes` the memory one job may take.
pub fn plan(v: &Value, job_bytes: u64) -> Result<Plan, String> {
// A pulse run is read by the pulse reader and integrated by the pulse
// simulator, a device target giving its qubits' parameters
// (engine::pulse).
if super::pulse::names_pulse(v) {
return super::pulse::plan(v, job_bytes);
}
// A job that names a device target is compiled, placed and simulated as
// the device would run it (engine::targets::job).
#[cfg(feature = "quantum_receipt")]

View file

@ -128,6 +128,14 @@
//! verdict, and its constraints measured; what went wrong and the hints
//! come from the diagnostics, the verdict and the critique, never from the
//! solution.
//! - [`pulse`]: pulse runs. A pulse program (OpenPulse calibrations on
//! physical qubits, `crate::qasm::pulse`) integrated by the pulse
//! simulator (`crate::quantum_pulse`) as a job of the method `pulse`, its
//! device from a target and the options, its result bounded and the same
//! bits everywhere, its receipt binding the control program, its
//! calibrations, the device and target snapshot, and the integrator and
//! its step; its schedule as a view; and a pulse job checked by running
//! it again.
//! - [`refusal`]: the job layer's refusal codes (`J…`), beside the reader's
//! (`Q…`): the request, the method, an intent, a repair and the host.
//! - [`energy`]: what the CPU cores spent for one process, as the kernel counts
@ -204,6 +212,7 @@ pub mod novelty;
pub mod provider;
#[cfg(feature = "quantum_receipt")]
pub mod receipt;
pub mod pulse;
pub mod refusal;
pub mod repair;
#[cfg(feature = "quantum_receipt")]

View file

@ -0,0 +1,757 @@
//! Pulse runs as every host serves them: a pulse program read by the pulse
//! reader ([`crate::qasm::pulse`]), its device resolved, and integrated by
//! the pulse simulator ([`crate::quantum_pulse`]), as a job of the method
//! `pulse`; its schedule as a view; and a pulse job checked by running it
//! again.
//!
//! A job:
//!
//! ```json
//! { "qasm": "OPENQASM 3.0; defcalgrammar \"openpulse\"; cal { … } defcal x $0 { … } x $0; …",
//! "method": "pulse", "shots": 1000, "seed": 1,
//! "pulse": { "dt_ns": 0.5, "levels": 2, "tolerance": 1e-6,
//! "qubits": { "0": { "frequency_ghz": 5.0, "rabi_mhz": 50, "t1_us": 100, "t2_us": 80 } },
//! "couplings": [{ "qubits": [0, 1], "zz_khz": 50 }],
//! "ports": { "u0": { "kind": "drive", "qubit": 1 } } },
//! "target": "synthetic-line-5" }
//! ```
//!
//! - **The device.** Each qubit the schedule reaches is at 5 GHz plus 100 MHz
//! per index, with a Rabi frequency of 50 MHz at full amplitude, an
//! anharmonicity of −300 MHz (which matters at three levels) and no decay,
//! unless a `target` gives its frequency, T1 and T2 from its calibration
//! snapshot, or `pulse.qubits` gives them; the result's `pulse.device`
//! says where each number came from.
//! - **The answer.** The final state's populations, each capture's reading,
//! counts sampled (seeded) from the captures' joint reading when every
//! capture starts at one sample (written to the program's bits when each
//! writes one), or from the final state when there is no capture, and the
//! accuracy class `bounded`: every probability within `accuracy.bound` of
//! the exact solution of the pulse model's master equation.
//! - **What the receipt binds.** `pulse.calibration` holds the control
//! program's digest (its text), the digest of its calibrations (every
//! `cal`, `defcal` and `defcalgrammar` as written), the schedule's and the
//! device's, and the target snapshot's name, hash and calibration id when
//! one is named; `pulse.integrator` the integrator, its order and its
//! step; `pulse.density_hash` the final state's bits. The job's receipt
//! seals the result with all of them, and [`check`] runs the request again
//! and holds the result to it.
//!
//! Every refusal of a pulse run ends with `[J040]` (or `[J041]`, when the
//! error could not be stated within its tolerance); the reader's own end
//! with its `Q` code.
use super::catalog::Plan;
use super::refusal::{self, tag};
use crate::qasm::pulse::{self as ir, Op, Options, Port, PortKind, Reading, Schedule};
use crate::quantum_pulse::{self as sim, Device, Qubit, Refusal, Settings};
use serde_json::{Map, Value, json};
use std::collections::BTreeMap;
/// The method's name.
pub const METHOD: &str = "pulse";
/// A qubit's frequency when nothing gives it: this, plus
/// [`DEFAULT_FREQUENCY_STEP_GHZ`] per index.
pub const DEFAULT_FREQUENCY_GHZ: f64 = 5.0;
pub const DEFAULT_FREQUENCY_STEP_GHZ: f64 = 0.1;
/// The Rabi frequency at full amplitude when nothing gives it, MHz.
pub const DEFAULT_RABI_MHZ: f64 = 50.0;
/// The anharmonicity when nothing gives it, MHz.
pub const DEFAULT_ANHARMONICITY_MHZ: f64 = -300.0;
/// The most samples a schedule's view writes out in full.
pub const MAX_VIEW_SAMPLES: usize = 65_536;
const KEYS: [&str; 8] = ["qasm", "method", "shots", "seed", "pulse", "target", "dry_run", "verdict"];
const OPTIONS: [&str; 7] = ["dt_ns", "levels", "tolerance", "substeps", "qubits", "couplings", "ports"];
const QUBIT_KEYS: [&str; 5] = ["frequency_ghz", "rabi_mhz", "anharmonicity_mhz", "t1_us", "t2_us"];
fn perr(m: impl Into<String>) -> String {
tag(refusal::PULSE, m)
}
/// Whether a request is a pulse run: its method is `pulse`.
pub fn names_pulse(v: &Value) -> bool {
v.get("method").and_then(Value::as_str) == Some(METHOD)
}
/// The JSON Schema of a request's `pulse` options, for every host's tool
/// and API description.
pub fn options_schema() -> Value {
let num = |d: &str| json!({ "type": "number", "description": d });
json!({
"type": "object",
"description": "A pulse run's options (method \"pulse\"): the sample period, the levels kept, the tolerance on the stated error, the steps a sample is divided into, each qubit's parameters, static couplings, and ports beyond the names d0, m0, a0.",
"additionalProperties": false,
"properties": {
"dt_ns": { "type": "number", "exclusiveMinimum": 0, "maximum": 1000, "description": "The sample period, ns (default 0.5)." },
"levels": { "type": "integer", "enum": [2, 3], "description": "Levels kept per qubit (default 2; 3 shows leakage)." },
"tolerance": { "type": "number", "exclusiveMinimum": 0, "exclusiveMaximum": 1, "description": "The most the stated error may be (default 1e-6): a run whose bound and rounding estimate pass it is refused, naming the steps that would meet it." },
"substeps": { "type": "integer", "minimum": 1, "maximum": sim::MAX_SUBSTEPS, "description": "Steps per sample (default: chosen so each step times the generator's norm is at most 0.5)." },
"qubits": { "type": "object", "description": "Each qubit's parameters by its index, over the target's and the defaults.", "additionalProperties": {
"type": "object", "additionalProperties": false, "properties": {
"frequency_ghz": num("Its frequency, GHz."),
"rabi_mhz": num("Its Rabi frequency at full amplitude, MHz."),
"anharmonicity_mhz": num("Its anharmonicity, MHz."),
"t1_us": num("Its relaxation time, µs."),
"t2_us": num("Its coherence time, µs (at most 2·T1).")
} } },
"couplings": { "type": "array", "description": "Static ZZ couplings.", "items": { "type": "object", "required": ["qubits", "zz_khz"], "additionalProperties": false, "properties": {
"qubits": { "type": "array", "items": { "type": "integer", "minimum": 0 }, "minItems": 2, "maxItems": 2 },
"zz_khz": num("The coupling's rate, kHz.")
} } },
"ports": { "type": "object", "description": "Ports by name: what each is for and the qubit it reaches.", "additionalProperties": {
"type": "object", "required": ["kind", "qubit"], "additionalProperties": false, "properties": {
"kind": { "enum": ["drive", "measure", "acquire"] },
"qubit": { "type": "integer", "minimum": 0 }
} } }
}
})
}
/// A pulse run's options, read.
struct Opts {
dt_ns: f64,
levels: usize,
tolerance: f64,
substeps: Option<u32>,
ports: Vec<Port>,
qubits: BTreeMap<u32, Map<String, Value>>,
couplings: Vec<(u32, u32, f64)>,
}
fn number(v: &Value, at: &str) -> Result<f64, String> {
v.as_f64().filter(|x| x.is_finite()).ok_or_else(|| perr(format!("{at} is a number")))
}
fn whole(v: &Value, at: &str, max: u64) -> Result<u64, String> {
v.as_u64().filter(|&x| x <= max).ok_or_else(|| perr(format!("{at} is a whole number from 0 to {max}")))
}
fn opts(v: &Value) -> Result<Opts, String> {
let o = match v.get("pulse") {
None | Some(Value::Null) => Map::new(),
Some(Value::Object(o)) => o.clone(),
Some(_) => return Err(perr("\"pulse\" is an object: the run's options")),
};
if let Some(k) = o.keys().find(|k| !OPTIONS.contains(&k.as_str())) {
return Err(perr(format!("pulse.{k}: no such option; the options are {}", OPTIONS.join(", "))));
}
let dt_ns = match o.get("dt_ns") {
None => ir::DEFAULT_DT_NS,
Some(x) => {
let x = number(x, "pulse.dt_ns")?;
if !(x > 0.0 && x <= 1000.0) {
return Err(perr(format!("pulse.dt_ns is {x}; the sample period is more than 0 and at most 1000 ns")));
}
x
}
};
let levels = match o.get("levels") {
None => 2,
Some(x) => match x.as_u64() {
Some(l @ (2 | 3)) => l as usize,
_ => return Err(perr("pulse.levels is 2 or 3")),
},
};
let tolerance = match o.get("tolerance") {
None => sim::DEFAULT_TOLERANCE,
Some(x) => {
let x = number(x, "pulse.tolerance")?;
if !(x > 0.0 && x < 1.0) {
return Err(perr(format!("pulse.tolerance is {x}; it is between 0 and 1")));
}
x
}
};
let substeps = match o.get("substeps") {
None | Some(Value::Null) => None,
Some(x) => {
let n = whole(x, "pulse.substeps", sim::MAX_SUBSTEPS as u64)?;
if n == 0 {
return Err(perr("pulse.substeps is at least 1"));
}
Some(n as u32)
}
};
let mut ports = Vec::new();
match o.get("ports") {
None | Some(Value::Null) => {}
Some(Value::Object(m)) => {
for (name, p) in m {
let at = format!("pulse.ports.{name}");
let po = p.as_object().ok_or_else(|| perr(format!("{at} is {{\"kind\", \"qubit\"}}")))?;
if let Some(k) = po.keys().find(|k| !matches!(k.as_str(), "kind" | "qubit")) {
return Err(perr(format!("{at}.{k}: a port gives its kind and its qubit")));
}
let kind = po.get("kind").and_then(Value::as_str).and_then(PortKind::from_name).ok_or_else(|| perr(format!("{at}.kind is drive, measure or acquire")))?;
let qubit = whole(po.get("qubit").unwrap_or(&Value::Null), &format!("{at}.qubit"), 65_535)? as u32;
ports.push(Port::new(name.clone(), kind, qubit));
}
}
Some(_) => return Err(perr("pulse.ports is an object: each port's name and {\"kind\", \"qubit\"}")),
}
let mut qubits = BTreeMap::new();
match o.get("qubits") {
None | Some(Value::Null) => {}
Some(Value::Object(m)) => {
for (k, p) in m {
let at = format!("pulse.qubits.{k}");
let q: u32 = k.parse().ok().filter(|_| k.bytes().all(|b| b.is_ascii_digit()) && k.len() <= 5).ok_or_else(|| perr(format!("{at}: a qubit is named by its index, as \"0\"")))?;
let po = p.as_object().ok_or_else(|| perr(format!("{at} is an object of the qubit's parameters")))?;
if let Some(f) = po.keys().find(|f| !QUBIT_KEYS.contains(&f.as_str())) {
return Err(perr(format!("{at}.{f}: no such parameter; the parameters are {}", QUBIT_KEYS.join(", "))));
}
for (f, x) in po {
let x = number(x, &format!("{at}.{f}"))?;
let ok = match f.as_str() {
"frequency_ghz" => x > 0.0 && x <= 1000.0,
"rabi_mhz" => (0.0..=100_000.0).contains(&x),
"anharmonicity_mhz" => x.abs() <= 100_000.0,
_ => x > 0.0 && x <= 1e9,
};
if !ok {
return Err(perr(format!("{at}.{f} is {x}, outside what it can be")));
}
}
qubits.insert(q, po.clone());
}
}
Some(_) => return Err(perr("pulse.qubits is an object: each qubit's index and its parameters")),
}
let mut couplings = Vec::new();
match o.get("couplings") {
None | Some(Value::Null) => {}
Some(Value::Array(a)) => {
for (i, c) in a.iter().enumerate() {
let at = format!("pulse.couplings[{i}]");
let co = c.as_object().ok_or_else(|| perr(format!("{at} is {{\"qubits\": [a, b], \"zz_khz\": x}}")))?;
if let Some(k) = co.keys().find(|k| !matches!(k.as_str(), "qubits" | "zz_khz")) {
return Err(perr(format!("{at}.{k}: a coupling gives its qubits and zz_khz")));
}
let qs: Vec<u32> = co.get("qubits").and_then(Value::as_array).map(|a| a.iter().filter_map(Value::as_u64).filter(|&q| q <= 65_535).map(|q| q as u32).collect()).unwrap_or_default();
let [a, b] = qs[..] else { return Err(perr(format!("{at}.qubits is two qubits' indices"))) };
if a == b {
return Err(perr(format!("{at}: a coupling is between two qubits")));
}
let khz = number(co.get("zz_khz").unwrap_or(&Value::Null), &format!("{at}.zz_khz"))?;
couplings.push((a.min(b), a.max(b), khz * 1e3));
}
}
Some(_) => return Err(perr("pulse.couplings is a list of {\"qubits\": [a, b], \"zz_khz\": x}")),
}
Ok(Opts { dt_ns, levels, tolerance, substeps, ports, qubits, couplings })
}
fn reader_options(o: &Opts) -> Options {
let mut r = Options::new(o.dt_ns);
for p in &o.ports {
r = r.with_port(p.clone());
}
r
}
fn hash_json(v: &Value) -> String {
super::compile::hash_of_text(&v.to_string())
}
/// What a schedule's digests are: the control program's text, its
/// calibrations as written, and the schedule.
fn digests(src: &str, s: &Schedule) -> Value {
json!({
"program": super::compile::hash_of_text(src),
"calibrations": super::compile::hash_of_text(&s.calibrations.join("\n")),
"schedule": hash_json(&s.to_json()),
})
}
/// A pulse job read: its schedule, its device and where each parameter came
/// from, the target it names, and the integration planned.
struct Prepared {
reading: Reading,
device: Device,
device_json: Value,
target: Option<Value>,
plan: sim::Plan,
shots: u64,
seed: u64,
}
fn read_program(v: &Value, o: &Opts) -> Result<(String, Reading), String> {
let src = v.get("qasm").and_then(Value::as_str).ok_or_else(|| tag(refusal::NOT_A_JOB, "a job needs \"qasm\": a pulse program (OpenPulse calibrations on physical qubits)"))?;
let reading = ir::read(src, &reader_options(o)).map_err(|d| d.to_string())?;
Ok((src.to_string(), reading))
}
/// The device the schedule runs on: each qubit's parameters from the
/// options, else the target, else the defaults, and the JSON that says
/// which.
#[cfg_attr(not(feature = "quantum_receipt"), allow(unused_variables))]
fn device(s: &Schedule, o: &Opts, v: &Value) -> Result<(Device, Value, Option<Value>), String> {
let qs = s.qubits();
#[cfg(feature = "quantum_receipt")]
let target = match v.get("target") {
None | Some(Value::Null) => None,
Some(t) => Some(super::targets::job::target_of(t).map_err(perr)?),
};
#[cfg(not(feature = "quantum_receipt"))]
if v.get("target").is_some_and(|t| !t.is_null()) {
return Err(perr("this build has no device targets (the engine's quantum_receipt feature)"));
}
for q in o.qubits.keys() {
if !qs.contains(q) {
return Err(perr(format!("pulse.qubits.{q}: the schedule does not act on qubit {q}; it acts on {}", list(&qs))));
}
}
for &(a, b, _) in &o.couplings {
if !qs.contains(&a) || !qs.contains(&b) {
return Err(perr(format!("a coupling of {a} and {b}: the schedule acts on {}", list(&qs))));
}
}
let mut d = Device::new(o.levels);
let mut rows = Vec::new();
for &q in &qs {
let given = o.qubits.get(&q);
let g = |k: &str| given.and_then(|m| m.get(k)).and_then(Value::as_f64);
#[cfg(feature = "quantum_receipt")]
let tq = match &target {
Some(t) => Some(t.qubits.get(q as usize).ok_or_else(|| perr(format!("qubit {q} is not on the target {}, which has {} qubits", t.name, t.n())))?),
None => None,
};
#[cfg(not(feature = "quantum_receipt"))]
let tq: Option<&()> = None;
let pick = |given: Option<f64>, from_target: Option<f64>, default: Option<f64>| -> (Option<f64>, &'static str) {
match (given, from_target, default) {
(Some(x), _, _) => (Some(x), "pulse"),
(None, Some(x), _) => (Some(x), "target"),
(None, None, Some(x)) => (Some(x), "default"),
_ => (None, "none"),
}
};
#[cfg(feature = "quantum_receipt")]
let (tf, t1t, t2t) = (tq.and_then(|t| t.frequency_ghz), tq.and_then(|t| t.t1_us), tq.and_then(|t| t.t2_us));
#[cfg(not(feature = "quantum_receipt"))]
let (tf, t1t, t2t): (Option<f64>, Option<f64>, Option<f64>) = { let _ = tq; (None, None, None) };
let (f, f_from) = pick(g("frequency_ghz"), tf, Some(DEFAULT_FREQUENCY_GHZ + DEFAULT_FREQUENCY_STEP_GHZ * q as f64));
let (rabi, r_from) = pick(g("rabi_mhz"), None, Some(DEFAULT_RABI_MHZ));
let (anh, a_from) = pick(g("anharmonicity_mhz"), None, Some(DEFAULT_ANHARMONICITY_MHZ));
let (t1, t1_from) = pick(g("t1_us"), t1t, None);
let (t2, t2_from) = pick(g("t2_us"), t2t, None);
let (f, rabi, anh) = (f.unwrap_or(DEFAULT_FREQUENCY_GHZ) * 1e9, rabi.unwrap_or(DEFAULT_RABI_MHZ) * 1e6, anh.unwrap_or(0.0) * 1e6);
let (t1, t2) = (t1.map(|t| t * 1e3), t2.map(|t| t * 1e3));
d = d.with_qubit(q, Qubit::new(f, rabi).with_anharmonicity(anh).with_times(t1, t2));
let mut row = json!({ "qubit": q, "frequency_hz": f, "rabi_hz": rabi, "anharmonicity_hz": anh, "t1_ns": t1, "t2_ns": t2,
"from": { "frequency": f_from, "rabi": r_from, "anharmonicity": a_from, "t1": t1_from, "t2": t2_from } });
if let Some(r) = row.as_object_mut() {
r.retain(|_, x| !x.is_null());
}
rows.push(row);
}
for &(a, b, hz) in &o.couplings {
d = d.with_coupling(a, b, hz);
}
let dj = json!({
"levels": o.levels,
"dt_ns": o.dt_ns,
"qubits": rows,
"couplings": o.couplings.iter().map(|&(a, b, hz)| json!({ "qubits": [a, b], "zz_hz": hz })).collect::<Vec<_>>(),
"model": "each qubit a ladder of its levels in the frame rotating at its frequency, under the rotating-wave approximation, the envelope held for each sample; relaxation and pure dephasing from T1 and T2; static ZZ couplings",
});
#[cfg(feature = "quantum_receipt")]
let tj = target.as_ref().map(|t| {
let c = t.to_json()["calibration"].clone();
json!({ "name": t.name, "hash": t.hash(), "calibration": c })
});
#[cfg(not(feature = "quantum_receipt"))]
let tj = None;
Ok((d, dj, tj))
}
fn list(qs: &[u32]) -> String {
if qs.is_empty() {
return "no qubit".into();
}
qs.iter().map(|q| format!("${q}")).collect::<Vec<_>>().join(", ")
}
fn refused(r: Refusal) -> String {
match r {
Refusal::Bound(m) => tag(refusal::PULSE_BOUND, m),
other => perr(other.to_string()),
}
}
fn prepare(v: &Value) -> Result<(String, Prepared), String> {
let o = v.as_object().ok_or_else(|| tag(refusal::NOT_A_JOB, "a job is a JSON object"))?;
if let Some(k) = o.keys().find(|k| !KEYS.contains(&k.as_str())) {
return Err(perr(format!(
"\"{k}\": a pulse run takes qasm, shots, seed, pulse and target; observables, amplitudes, noise, compile, repair, intent and mitigate belong to the circuit methods (its decay comes from T1 and T2)"
)));
}
// A pulse run always states its verdict; asking for it changes nothing.
if v.get("verdict").is_some_and(|x| !x.is_boolean()) {
return Err(perr("\"verdict\" is true or false; a pulse run always carries its verdict"));
}
let opts = opts(v)?;
let (src, reading) = read_program(v, &opts)?;
if reading.schedule.instructions.is_empty() {
return Err(perr("the schedule plays nothing: a pulse run needs a calibration that plays, delays or captures on a frame"));
}
let (device, device_json, target) = device(&reading.schedule, &opts, v)?;
let plan = sim::plan(&reading.schedule, &device, &Settings::new(opts.tolerance, opts.substeps)).map_err(refused)?;
let shots = match v.get("shots") {
None | Some(Value::Null) => 1024,
Some(x) => x.as_u64().filter(|&s| s <= super::catalog::MAX_SHOTS).ok_or_else(|| tag(refusal::LIMIT, format!("shots is a whole number of at most {}", super::catalog::MAX_SHOTS)))?,
};
let seed = match v.get("seed") {
None | Some(Value::Null) => 1,
Some(x) => x.as_u64().ok_or_else(|| perr("seed is a whole number"))?,
};
Ok((src, Prepared { reading, device, device_json, target, plan, shots, seed }))
}
/// A pulse program as the pulse reader reads it under a request's options:
/// `{"ok": true}` with its notes and warnings (`diagnostics`), or `{"ok":
/// false}` with the problem, its code and its place, as a verdict's
/// `parsed` holds it.
pub fn lint(request: &Value) -> Value {
let o = match opts(request) {
Ok(o) => o,
Err(e) => return json!({ "ok": false, "text": e }),
};
match request.get("qasm").and_then(Value::as_str) {
None => json!({ "ok": false, "text": tag(refusal::NOT_A_JOB, "a job needs \"qasm\": a pulse program") }),
Some(src) => match ir::read(src, &reader_options(&o)) {
Ok(r) => json!({ "ok": true, "diagnostics": r.diagnostics.iter().map(|d| d.to_json()).collect::<Vec<_>>() }),
Err(d) => {
let mut v = d.to_json();
v["ok"] = json!(false);
v["text"] = json!(d.to_string());
v
}
},
}
}
/// The plan of a pulse job within `job_bytes`: the qubits simulated, the
/// memory and the work, and why.
pub fn plan(v: &Value, job_bytes: u64) -> Result<Plan, String> {
let (_, p) = prepare(v)?;
let s = &p.reading.schedule;
let held: usize = s.waveforms.iter().map(|w| w.samples.len()).sum();
let memory = (32u64 << 20) + 16 * held as u64 + 96 * s.instructions.len() as u64 + 64 * (p.plan.dim * p.plan.dim) as u64 + 64 * p.plan.samples;
if memory > job_bytes {
return Err(tag(refusal::LIMIT, format!("the pulse run takes about {} MiB; a job here takes at most {} MiB", memory >> 20, job_bytes >> 20)));
}
let ns = p.plan.samples as f64 * p.plan.dt_ns;
Ok(Plan {
method: METHOD.into(),
why: format!(
"a pulse schedule of {} samples ({ns} ns) on {} at {} levels: {} steps of {} ns, each a series of order {}, the error within {:.3e} with rounding estimated at {:.3e}",
p.plan.samples,
list(&p.plan.qubits),
p.plan.levels,
p.plan.steps,
p.plan.step_ns,
p.plan.order,
p.plan.truncation_bound,
p.plan.rounding_estimate
),
memory_bytes: memory,
work: p.plan.work,
qubits: p.plan.qubits.len() as u32,
target: None,
})
}
/// The bits of `x` as a key `width` wide, the highest first.
fn key(x: usize, width: usize) -> String {
(0..width).rev().map(|j| if x >> j & 1 == 1 { '1' } else { '0' }).collect()
}
/// Runs a pulse job planned by [`plan`]: see the module documentation for
/// the answer.
pub fn run(v: &Value, plan: &Plan) -> Result<Value, String> {
if plan.method != METHOD {
return Err(perr(format!("a pulse run's plan has the method pulse, not {}", plan.method)));
}
let (src, p) = prepare(v)?;
let s = &p.reading.schedule;
let out = sim::run(s, &p.device, &p.plan).map_err(refused)?;
let sq = &out.plan.qubits;
let n = sq.len();
// What is read: the captures together, or the final state.
let caps = &out.captures;
let bit_of = |c: &sim::Capture| match s.instructions[c.instruction].op {
Op::Capture { bit, .. } => bit,
_ => None,
};
let linked: Option<Vec<usize>> = caps.iter().map(bit_of).collect();
let linked = linked.filter(|b| {
let mut u = b.clone();
u.sort_unstable();
u.dedup();
!b.is_empty() && u.len() == b.len()
});
let (read, labels, dist): (&str, Vec<String>, Option<Vec<(String, f64)>>) = if caps.is_empty() {
let labels = sq.iter().rev().map(|q| format!("${q}")).collect();
("the final state, each simulated qubit read at the schedule's end", labels, Some(out.populations.iter().enumerate().map(|(x, &pr)| (key(x, n), pr)).collect()))
} else {
match (&out.joint, &linked) {
(Some(j), Some(bits)) => {
let width = s.bits.len();
let mut m: BTreeMap<String, f64> = BTreeMap::new();
for (x, &pr) in j.iter().enumerate() {
let mut k = vec!['0'; width];
for (c, &b) in bits.iter().enumerate() {
if x >> c & 1 == 1 {
k[width - 1 - b] = '1';
}
}
*m.entry(k.into_iter().collect()).or_insert(0.0) += pr;
}
("the captures, at their one first sample, into the program's bits", s.bits.iter().rev().cloned().collect(), Some(m.into_iter().collect()))
}
(Some(j), None) => {
let labels = caps.iter().rev().map(|c| format!("${}", c.qubit)).collect();
("the captures, at their one first sample", labels, Some(j.iter().enumerate().map(|(x, &pr)| (key(x, caps.len()), pr)).collect()))
}
(None, _) => ("the captures, each at its own sample: their joint reading needs the readout's back-action, which is not modelled", caps.iter().rev().map(|c| format!("${}", c.qubit)).collect(), None),
}
};
let bound = out.plan.truncation_bound + out.plan.rounding_estimate;
let mut r = json!({
"method": METHOD,
"qubits": n,
"gates": s.calls.len(),
"accuracy": {
"class": "bounded",
"bound": bound,
"truncation_bound": out.plan.truncation_bound,
"rounding_estimate": out.plan.rounding_estimate,
"tolerance": out.plan.tolerance,
"norm": "trace",
"note": "every probability is within bound of the exact solution of the pulse model's master equation: the series' truncation proved, the rounding an estimate; the model itself (its levels, the rotating frame and the rotating-wave approximation, the envelope held for each sample) is what is solved",
},
});
if let (Some(d), true) = (&dist, p.shots > 0) {
let pr: Vec<f64> = d.iter().map(|x| x.1.max(0.0)).collect();
let mut map = std::collections::HashMap::new();
for i in super::run::sample_distribution(&pr, p.shots, p.seed) {
*map.entry(d[i as usize].0.clone()).or_insert(0u64) += 1;
}
super::run::merge(&mut r, super::run::counts(map));
r["shots"] = json!(p.shots);
}
if !p.reading.diagnostics.is_empty() {
r["diagnostics"] = Value::Array(p.reading.diagnostics.iter().map(|d| d.to_json()).collect());
}
let mut calibration = digests(&src, s);
calibration["device"] = json!(hash_json(&p.device_json));
if let Some(t) = &p.target {
calibration["target"] = t.clone();
}
let mut not_modelled = vec![json!("the readout: a capture reads its qubit's state at its first sample, a level above 0 as 1")];
if s.instructions.iter().any(|i| s.frame_kind(i.frame) == PortKind::Measure && matches!(i.op, Op::Play { .. })) {
not_modelled.push(json!("plays on measurement ports: scheduled and timed, driving nothing simulated"));
}
let frame_name = |i: usize| s.frames[s.instructions[i].frame].name.clone();
r["pulse"] = json!({
"read": read,
"outcome_bits": labels,
"distribution": dist.as_ref().map(|d| d.iter().map(|(k, pr)| (k.clone(), json!(pr))).collect::<Map<_, _>>()),
"populations": out.populations.iter().enumerate().map(|(x, &pr)| (key(x, n), json!(pr))).collect::<Map<_, _>>(),
"simulated_qubits": sq,
"captures": caps.iter().map(|c| json!({ "frame": frame_name(c.instruction), "qubit": c.qubit, "start": c.start, "start_ns": c.start as f64 * s.dt_ns, "bit": bit_of(c).map(|b| s.bits[b].clone()), "p1": c.p1, "p2": c.p2 })).collect::<Vec<_>>(),
"trace": out.trace,
"density_hash": out.density_hash(),
"duration": s.duration,
"duration_ns": s.duration as f64 * s.dt_ns,
"integrator": {
"name": "taylor",
"order": out.plan.order,
"dt_ns": out.plan.dt_ns,
"substeps": out.plan.substeps,
"step_ns": out.plan.step_ns,
"steps": out.plan.steps,
"norm_max": out.plan.norm_max,
"frame": "each sample in the frame of each qubit's tone, the generator constant over it, then a diagonal phase back",
},
"device": p.device_json,
"calibration": calibration,
"not_modelled": not_modelled,
});
if p.device.levels == 3 {
r["pulse"]["leakage"] = Value::Array(sq.iter().zip(&out.leakage).map(|(q, &l)| json!({ "qubit": q, "p2": l })).collect());
}
r["verdict"] = super::verdict::of_pulse(&r);
Ok(r)
}
/// A pulse program's schedule, for a view: every frame and its port, every
/// instruction at its sample, the waveforms' samples (each kept to at most
/// 1024 points, every k-th, when the schedule holds more than
/// [`MAX_VIEW_SAMPLES`]), the gate calls, the reader's notes, and the
/// digests a pulse run's receipt binds. Takes `{"qasm", "pulse"}` (only the
/// sample period and the ports matter here).
pub fn schedule(v: &Value) -> Result<Value, String> {
if let Some(k) = v.as_object().and_then(|o| o.keys().find(|k| !matches!(k.as_str(), "qasm" | "pulse" | "method"))) {
return Err(perr(format!("\"{k}\": a schedule takes qasm and pulse")));
}
let o = opts(v)?;
let (src, reading) = read_program(v, &o)?;
let s = &reading.schedule;
let mut j = s.to_json();
let held: usize = s.waveforms.iter().map(|w| w.samples.len()).sum();
if held > MAX_VIEW_SAMPLES {
for w in j["waveforms"].as_array_mut().into_iter().flatten() {
let all: Vec<Value> = w["samples"].as_array().cloned().unwrap_or_default();
let every = all.len().div_ceil(1024).max(1);
w["samples"] = Value::Array(all.into_iter().step_by(every).collect());
w["samples_every"] = json!(every);
}
j["decimated"] = json!(format!("the waveforms hold {held} samples, more than {MAX_VIEW_SAMPLES}: each is drawn from every k-th sample (samples_every), which a run does not do"));
}
j["answer"] = json!("schedule");
j["diagnostics"] = Value::Array(reading.diagnostics.iter().map(|d| d.to_json()).collect());
j["calibration"] = digests(&src, s);
Ok(j)
}
/// A pulse job as a host returned it (`{"request", "result"}`, the result
/// with its receipt), checked: its receipt's signature, its program's digest
/// against its request's text, and the request run again here, its result
/// held to the one given (every field but the receipt, the energy, the
/// provenance and the engine's version), each difference named.
#[cfg(feature = "quantum_receipt")]
pub fn check(job: &Value) -> Result<Value, String> {
let (Some(request), Some(result)) = (job.get("request"), job.get("result")) else {
return Err(perr("expected a pulse job as a host returns it: {\"request\", \"result\"}, the result with its receipt"));
};
if !names_pulse(request) {
return Err(perr("the job is not a pulse run: its request's method is not \"pulse\""));
}
let (signature, key) = super::job::verify(job)?;
let program = request.get("qasm").and_then(Value::as_str).map(super::compile::hash_of_text);
let program_ok = program.is_some() && result.pointer("/pulse/calibration/program").and_then(Value::as_str) == program.as_deref();
let p = plan(request, u64::MAX)?;
let fresh = super::run::run(request, &p)?;
let canon = super::receipt::canonical;
let mut differences = Vec::new();
let skip = ["receipt", "energy", "provenance", "engine", "elapsed_ms"];
let keys: std::collections::BTreeSet<&String> = result.as_object().into_iter().flat_map(|o| o.keys()).chain(fresh.as_object().into_iter().flat_map(|o| o.keys())).collect();
for k in keys {
if skip.contains(&k.as_str()) {
continue;
}
let (a, b) = (result.get(k).unwrap_or(&Value::Null), fresh.get(k).unwrap_or(&Value::Null));
if canon(a) == canon(b) {
continue;
}
match (a.as_object(), b.as_object()) {
(Some(x), Some(y)) => {
let inner: std::collections::BTreeSet<&String> = x.keys().chain(y.keys()).collect();
for i in inner {
if canon(x.get(i).unwrap_or(&Value::Null)) != canon(y.get(i).unwrap_or(&Value::Null)) {
differences.push(format!("{k}.{i}"));
}
}
}
_ => differences.push(k.clone()),
}
}
let reproduced = differences.is_empty();
let statement = match (signature, program_ok, reproduced) {
(true, true, true) => "the receipt verifies, the program is the one its digest names, and running the request again here gives the same result, bit for bit".to_string(),
(false, _, _) => "the receipt does not verify: the result is not the one its key sealed".to_string(),
(true, false, _) => "the result's program digest is not its request's text".to_string(),
(true, true, false) => format!("the receipt verifies, but running the request again here gives another result: {}", differences.join(", ")),
};
Ok(json!({
"answer": "check",
"verified": signature && program_ok && reproduced,
"signature": signature,
"key": key,
"program": program_ok,
"reproduced": reproduced,
"differences": differences,
"density_hash": { "given": result.pointer("/pulse/density_hash"), "recomputed": fresh.pointer("/pulse/density_hash") },
"calibration": fresh.pointer("/pulse/calibration"),
"statement": statement,
}))
}
/// `{"request", "result"}` with the result sealed by `key`, in the form every
/// job's receipt takes, so [`super::job::verify`] checks it.
#[cfg(feature = "quantum_receipt")]
pub fn seal(request: &Value, mut result: Value, key: &ed25519_dalek::SigningKey) -> Value {
let r = super::receipt::seal(key, request, &result);
result["receipt"] = r;
json!({ "request": request, "result": result })
}
/// The method's row in the methods list.
pub fn describe() -> Value {
json!({
"method": METHOD,
"accuracy": "bounded",
"circuits": "pulse programs: OpenPulse calibrations (cal, defcal) on physical qubits, every gate called with its calibration",
"max_qubits": sim::MAX_DIM.ilog2(),
"answers": ["counts", "populations", "captures"],
"options": { "pulse": "dt_ns, levels, tolerance, substeps, qubits, couplings, ports (the request's \"pulse\")" },
})
}
/// The MCP tools both servers serve, in one wording: `sealer` names whose
/// key seals the answer ("this user's key", "the service's key").
pub fn tools(sealer: &str) -> Value {
let qasm = json!({ "type": "string", "description": "A pulse program: OpenQASM 3 with `defcalgrammar \"openpulse\";`, `cal` blocks declaring ports (d0 drives qubit 0, m0 is its measurement port, a0 its acquisition port), frames (newframe(port, frequency Hz, phase)) and waveforms, a `defcal` for every gate it calls on physical qubits ($0), and the calls." });
let out = |extra: Value| -> Value {
let mut p = extra;
p["receipt"] = json!({ "type": "object" });
p
};
json!([
{
"name": "pulse_schedule",
"title": "Read a pulse program into its schedule",
"description": format!("Read a pulse program (OpenPulse calibrations: ports, frames, waveforms, play, delay, shift_phase, set_phase, shift_frequency, set_frequency, captures and barriers) into its schedule on frames: every instruction at its sample, each waveform's samples (gaussian, drag, gaussian_square, constant, sech, sine, or explicit), each gate call with the samples its calibration spans, and the digests a pulse run's receipt binds (the program, its calibrations, the schedule). Durations are whole samples of dt (pulse.dt_ns, default 0.5 ns) or refused. A construct the reader does not take is refused with a Q code and its line. Sealed with {sealer}. To run it, give the program to run_quantum_circuit with method \"pulse\"."),
"inputSchema": { "type": "object", "required": ["qasm"], "properties": { "qasm": qasm, "pulse": options_schema() } },
"outputSchema": super::verdict::output_schema("pulse_schedule", false, out(json!({ "request": { "type": "object" }, "result": { "type": "object", "properties": { "frames": { "type": "array" }, "instructions": { "type": "array" }, "waveforms": { "type": "array" }, "calls": { "type": "array" }, "calibration": { "type": "object" } } } }))),
"annotations": { "readOnlyHint": true, "idempotentHint": true, "openWorldHint": false }
},
{
"name": "pulse_check",
"title": "Check a pulse run by running it again",
"description": format!("Check a pulse job as a host returned it ({{\"request\", \"result\"}}, the result with its receipt): the receipt's signature, the program's digest against the request's text, and the request run again here with the result held to the one given field by field (the density matrix's digest among them), each difference named. A pulse run is the same bits on every platform, so a result that does not reproduce was not made from that request by this engine. Sealed with {sealer}. Quote `verified` and the statement."),
"inputSchema": { "type": "object", "required": ["request", "result"], "properties": { "request": { "type": "object", "description": "The pulse job's request." }, "result": { "type": "object", "description": "Its result, with its receipt." } } },
"outputSchema": super::verdict::output_schema("pulse_check", false, out(json!({ "request": { "type": "object" }, "result": { "type": "object", "properties": { "verified": { "type": "boolean" }, "reproduced": { "type": "boolean" }, "differences": { "type": "array" }, "statement": { "type": "string" } } } }))),
"annotations": { "readOnlyHint": true, "idempotentHint": true, "openWorldHint": false }
}
])
}
/// The call a tool is, `schedule` or `check`, if it is one of these.
pub fn op_of_tool(name: &str) -> Option<&'static str> {
match name {
"pulse_schedule" => Some("schedule"),
"pulse_check" => Some("check"),
_ => None,
}
}
/// The calls by name (`schedule`, `check`), as the hosts route them: the
/// answer names the engine and its schema (`wai.quantum.pulse/1`,
/// [`super::schema::PULSE`]).
pub fn call(op: &str, request: &Value) -> Result<Value, String> {
let mut out = match op {
"schedule" => schedule(request),
#[cfg(feature = "quantum_receipt")]
"check" => check(request),
other => Err(perr(format!("no pulse call {other}: schedule or check"))),
}?;
super::schema::stamp_as(&mut out, super::schema::PULSE);
Ok(out)
}
#[cfg(test)]
mod tests;

View file

@ -0,0 +1,195 @@
use super::*;
use crate::engine::schema::Registry;
use crate::engine::{catalog, job};
use ed25519_dalek::SigningKey;
/// A pulse program: an `x` as a gaussian π pulse and a measurement as a
/// readout tone and a capture.
pub(crate) const XM: &str = "OPENQASM 3.0;\ndefcalgrammar \"openpulse\";\ncal {\n extern port d0;\n extern port m0;\n extern port a0;\n frame q0 = newframe(d0, 5.0e9, 0);\n frame r0 = newframe(m0, 7.1e9, 0);\n frame c0 = newframe(a0, 7.1e9, 0);\n}\ndefcal x $0 { play(q0, gaussian(0.4, 64dt, 16dt)); }\ndefcal measure $0 -> bit { play(r0, constant(0.1, 200dt)); return capture_v2(c0, 200dt); }\nbit[1] c;\nx $0;\nc[0] = measure $0;\n";
fn key() -> SigningKey {
SigningKey::from_bytes(&[7; 32])
}
fn ok(errs: Vec<String>, what: &str) {
assert!(errs.is_empty(), "{what}: {errs:?}");
}
fn job_of(extra: Value) -> Value {
let mut v = json!({ "qasm": XM, "method": "pulse", "shots": 500, "seed": 3 });
if let (Some(o), Some(e)) = (v.as_object_mut(), extra.as_object()) {
o.extend(e.clone());
}
v
}
fn refusal_of(v: &Value) -> String {
match catalog::plan(v, 1 << 30) {
Ok(p) => panic!("planned, expected a refusal: {p:?}"),
Err(e) => e,
}
}
#[test]
fn a_pulse_job_runs_end_to_end_and_its_answer_follows_its_schemas() {
let r = Registry::engine();
let req = job_of(json!({}));
let plan = catalog::plan(&req, 1 << 30).unwrap();
assert_eq!(plan.method, "pulse");
ok(r.check("plan.v1.json", &serde_json::to_value(&plan).unwrap()), "the plan");
ok(r.check("job-request.v1.json", &req), "the request");
let body = job::run(&req, 1 << 30, &key(), json!({ "unmetered": "a test" })).unwrap();
ok(r.check("job.v1.json", &body), "the job");
let res = &body["result"];
ok(r.check("job-result.v1.json", res), "the result");
assert!(job::verify(&body).unwrap().0, "its receipt verifies");
assert_eq!((res["method"].as_str(), res["accuracy"]["class"].as_str(), res["schema"].as_str()), (Some("pulse"), Some("bounded"), Some(crate::engine::schema::JOB_RESULT)));
// On resonance with a real envelope the drive commutes with itself, so
// the pulse turns the qubit by its area: Ω·Σ samples·dt.
let area: f64 = (0..64).map(|k| 0.4 * (-((k as f64 - 31.5).powi(2)) / 512.0).exp()).sum::<f64>() * 0.5;
let theta = TAU_TEST * 50e6 * 1e-9 * area;
let want = (theta / 2.0).sin().powi(2);
let p1 = res["pulse"]["distribution"]["1"].as_f64().unwrap();
let room = res["accuracy"]["bound"].as_f64().unwrap() + 1e-12;
assert!((p1 - want).abs() <= room, "P(1) {p1} vs the closed form {want}");
assert_eq!(res["pulse"]["outcome_bits"], json!(["c[0]"]));
assert_eq!(res["counts"]["1"].as_u64().unwrap() + res["counts"].get("0").and_then(Value::as_u64).unwrap_or(0), 500);
assert_eq!(res["pulse"]["captures"][0]["bit"], "c[0]");
assert_eq!(res["pulse"]["captures"][0]["start"], 64);
assert_eq!(res["gates"], 2);
// The verdict: executed on the pulse method, bounded.
assert_eq!(res["verdict"]["level"], "executed", "{}", res["verdict"]);
// What the receipt binds.
let cal = &res["pulse"]["calibration"];
assert_eq!(cal["program"].as_str(), Some(crate::engine::compile::hash_of_text(XM).as_str()));
for k in ["calibrations", "schedule", "device"] {
assert_eq!(cal[k].as_str().map(str::len), Some(64), "{k}: {cal}");
}
assert_eq!(res["pulse"]["integrator"]["order"], crate::quantum_pulse::ORDER);
assert!(res["accuracy"]["bound"].as_f64().unwrap() <= 1e-6);
// The plan and the run say the same of it, and a second run is the same bits.
let again = job::run(&req, 1 << 30, &key(), json!({ "unmetered": "a test" })).unwrap();
assert_eq!(crate::engine::receipt::canonical(&again["result"]), crate::engine::receipt::canonical(res));
}
const TAU_TEST: f64 = std::f64::consts::TAU;
#[test]
fn a_target_gives_the_qubits_their_frequency_and_times_and_its_hash_is_bound() {
let t = crate::engine::targets::library::get("synthetic-line-5").unwrap();
let q0 = &t.qubits[0];
let req = job_of(json!({ "target": "synthetic-line-5", "pulse": { "qubits": { "0": { "rabi_mhz": 40 } } } }));
let body = job::run(&req, 1 << 30, &key(), json!({ "unmetered": "a test" })).unwrap();
let res = &body["result"];
let row = &res["pulse"]["device"]["qubits"][0];
assert_eq!(row["from"]["frequency"], if q0.frequency_ghz.is_some() { "target" } else { "default" });
assert_eq!(row["from"]["t1"], if q0.t1_us.is_some() { "target" } else { "none" });
assert_eq!(row["from"]["rabi"], "pulse");
assert_eq!(row["rabi_hz"], 40e6);
if let Some(t1) = q0.t1_us {
assert_eq!(row["t1_ns"].as_f64(), Some(t1 * 1e3));
}
let bound = &res["pulse"]["calibration"]["target"];
assert_eq!(bound["hash"].as_str(), Some(t.hash().as_str()));
assert_eq!(bound["name"], "synthetic-line-5");
ok(Registry::engine().check("job-result.v1.json", res), "the result");
// A qubit the target does not have is refused.
let far = XM.replace("d0", "d9").replace("$0", "$9").replace("m0", "m9").replace("a0", "a9");
let e = refusal_of(&json!({ "qasm": far, "method": "pulse", "target": "synthetic-line-5" }));
assert!(e.contains("not on the target") && e.ends_with("[J040]"), "{e}");
}
#[test]
fn what_a_pulse_job_cannot_be_is_refused_with_its_code() {
let cases: Vec<(Value, &str)> = vec![
(job_of(json!({ "observables": ["Z0"] })), "[J040]"),
(job_of(json!({ "noise": { "dephasing": 0.1 } })), "[J040]"),
(job_of(json!({ "pulse": { "dt": 1.0 } })), "[J040]"),
(job_of(json!({ "pulse": { "levels": 4 } })), "[J040]"),
(job_of(json!({ "pulse": { "qubits": { "0": { "t1_us": 10, "t2_us": 30 } } } })), "[J040]"),
(job_of(json!({ "pulse": { "qubits": { "3": { "t1_us": 10 } } } })), "[J040]"),
(job_of(json!({ "pulse": { "substeps": 1, "qubits": { "0": { "rabi_mhz": 20000 } } } })), "[J041]"),
(job_of(json!({ "pulse": { "tolerance": 1e-15 } })), "[J041]"),
(json!({ "qasm": XM.replace("64dt", "32ns"), "method": "pulse", "pulse": { "dt_ns": 0.3 } }), "[Q458]"),
(json!({ "qasm": "OPENQASM 3.0;\nqubit q;\nx q;\n", "method": "pulse" }), "[Q460]"),
(json!({ "qasm": "OPENQASM 3.0;\ncal { extern port d0; }\n", "method": "pulse" }), "[J040]"),
(json!({ "method": "pulse" }), "[J001]"),
(job_of(json!({ "shots": 2_000_000 })), "[J005]"),
];
for (req, code) in cases {
let e = refusal_of(&req);
assert!(e.ends_with(code), "{req}: {e}");
}
}
#[test]
fn a_pulse_job_is_checked_by_running_it_again() {
let req = job_of(json!({}));
let body = job::run(&req, 1 << 30, &key(), json!({ "unmetered": "a test" })).unwrap();
let c = call("check", &body).unwrap();
assert_eq!((c["verified"].as_bool(), c["reproduced"].as_bool(), c["signature"].as_bool()), (Some(true), Some(true), Some(true)), "{c}");
assert_eq!(c["schema"], crate::engine::schema::PULSE);
// A result sealed anew with a changed number: the receipt verifies, the
// run does not reproduce it, and the check says where.
let mut forged = body["result"].clone();
forged.as_object_mut().unwrap().remove("receipt");
forged["pulse"]["populations"]["1"] = json!(0.5);
let resealed = seal(&req, forged.clone(), &key());
let c = check(&resealed).unwrap();
assert_eq!((c["signature"].as_bool(), c["reproduced"].as_bool(), c["verified"].as_bool()), (Some(true), Some(false), Some(false)));
assert_eq!(c["differences"], json!(["pulse.populations"]));
// A changed result under the old receipt does not verify.
let mut tampered = body.clone();
tampered["result"]["pulse"]["density_hash"] = json!("0".repeat(64));
let c = check(&tampered).unwrap();
assert_eq!(c["signature"], false);
// A circuit job is not a pulse run.
let circuit = job::run(&json!({ "qasm": "qubit q; h q;", "shots": 10 }), 1 << 30, &key(), Value::Null).unwrap();
assert!(check(&circuit).unwrap_err().ends_with("[J040]"));
ok(Registry::engine().check("pulse.v1.json", &seal(&body, call("check", &body).unwrap(), &key())), "the check");
}
#[test]
fn a_schedule_is_drawn_with_its_digests() {
let req = json!({ "qasm": XM, "pulse": { "dt_ns": 0.5 } });
let s = call("schedule", &req).unwrap();
assert_eq!(s["answer"], "schedule");
assert_eq!(s["schema"], crate::engine::schema::PULSE);
assert_eq!(s["frames"].as_array().map(Vec::len), Some(3));
assert_eq!(s["duration"], 264);
assert_eq!(s["calibration"]["program"].as_str(), Some(crate::engine::compile::hash_of_text(XM).as_str()));
let ops: Vec<&str> = s["instructions"].as_array().unwrap().iter().map(|i| i["op"].as_str().unwrap()).collect();
assert_eq!(ops, ["play", "play", "capture"]);
ok(Registry::engine().check("pulse.v1.json", &seal(&req, s.clone(), &key())), "the schedule");
// A schedule past the view's samples is drawn from every k-th sample, and says so.
let long = "OPENQASM 3.0;\ncal { extern port d0; frame f = newframe(d0, 5e9, 0); play(f, gaussian(0.1, 70000dt, 9000dt)); }\n";
let s = schedule(&json!({ "qasm": long })).unwrap();
assert!(s["decimated"].is_string() && s["waveforms"][0]["samples"].as_array().unwrap().len() <= 1024, "{}", s["waveforms"][0]["samples_every"]);
// Its refusals are the reader's.
let e = schedule(&json!({ "qasm": "OPENQASM 3.0;\ncal { play(f, constant(0.1, 1dt)); }\n" })).unwrap_err();
assert!(e.ends_with("[Q102]"), "{e}");
}
#[test]
fn the_request_schema_holds_the_options_the_engine_reads() {
let file: Value = serde_json::from_str(include_str!("../../../schemas/job-request.v1.json")).unwrap();
assert_eq!(file["properties"]["pulse"], options_schema(), "schemas/job-request.v1.json's pulse property is engine::pulse::options_schema()");
// Every tool has its output schema, standing alone.
for t in tools("this user's key").as_array().unwrap() {
assert!(t["outputSchema"]["type"] == "object" && t["inputSchema"]["type"] == "object", "{t}");
}
assert_eq!(op_of_tool("pulse_schedule"), Some("schedule"));
assert!(catalog::describe(1 << 30).as_array().unwrap().iter().any(|m| m["method"] == "pulse"));
}
/// The final density matrix's digest for [`XM`] at the defaults: what every
/// platform gives, natively and in the browser (wai-web's wasm32 leg).
pub(crate) const XM_DENSITY: &str = "03c0c48a8a3568fdd86e792633c5b4ea98eca3d09dc40d028bb50e13c489828b";
#[test]
fn a_pulse_job_is_the_same_bits_on_every_platform() {
let req = job_of(json!({}));
let p = catalog::plan(&req, 1 << 30).unwrap();
let r = crate::engine::run::run(&req, &p).unwrap();
assert_eq!(r["pulse"]["density_hash"].as_str(), Some(XM_DENSITY), "the bits differ from the pinned ones: {}", r["pulse"]["density_hash"]);
}

View file

@ -99,6 +99,10 @@ table! {
BUSY = "J032", Retry, "The host is full (its queue, or the checks running at once): send it again later.";
KEY_REQUIRED = "J033", Refused, "A host that takes requests only with one of its API keys: give yours as `Authorization: Bearer <key>`; a key it does not hold is refused the same way.";
// Pulse runs (engine::pulse).
PULSE = "J040", Refused, "A pulse run that does not read or cannot be done: a field it does not take, options or device parameters that do not read, a target without the qubit, or a schedule outside the simulator's model (two tones on one qubit at once, a play after its qubit's capture, more levels and qubits than it holds, a frame far from its qubit); the message says which.";
PULSE_BOUND = "J041", Refused, "A pulse run whose integration error cannot be stated within its tolerance: a step longer than the generator's norm allows, or a schedule so long that the bound or the rounding passes the tolerance; the message names the steps that would meet it, or says that none does.";
// An answer's view statements (the answer view layer).
ANSWER_PARSE = "J100", Dropped, "A line of an answer that is not a statement (`name = View(...)`), a program (a fenced `qasm` block with its name), a comment or a marker: left out.";
ANSWER_VIEW = "J101", Dropped, "A view the answer layer does not have: left out (the views are listed in the layer's specification).";

View file

@ -121,7 +121,8 @@ pub fn run(request: &Value, plan: &Plan) -> Result<Value, String> {
fn run_noting(request: &Value, plan: &Plan, passes: &mut Option<Value>) -> Result<Value, String> {
let mut out = run_job(request, plan, passes)?;
if crate::engine::verdict::asked(request) {
// A pulse run carries its own verdict (engine::pulse).
if crate::engine::verdict::asked(request) && !crate::engine::pulse::names_pulse(request) {
out["verdict"] = crate::engine::verdict::of_run(request, &out, plan.memory_bytes)?;
}
crate::engine::schema::stamp(&mut out);
@ -198,6 +199,10 @@ pub fn provenance(request: &Value, plan: &Plan, result: &Value, passes: Option<V
}
fn run_job(request: &Value, plan: &Plan, passes: &mut Option<Value>) -> Result<Value, String> {
// A pulse run is integrated by the pulse simulator (engine::pulse).
if crate::engine::pulse::names_pulse(request) {
return crate::engine::pulse::run(request, plan);
}
// A job that names a device target runs as the device would run it.
#[cfg(feature = "quantum_receipt")]
if crate::engine::targets::job::names_target(request) {

View file

@ -69,6 +69,9 @@ pub const SET: &str = "wai.quantum.set/1";
pub const PROVENANCE: &str = super::run::PROVENANCE;
/// An error-correction answer: a code, a decode or a threshold (`engine::qec`).
pub const QEC: &str = "wai.quantum.qec/1";
/// A pulse answer: a pulse program's schedule, or a pulse job checked by
/// running it again (`engine::pulse`). A pulse run is a job.
pub const PULSE: &str = "wai.quantum.pulse/1";
/// A code's distance certificate (`quantum_distance`).
pub const DISTANCE: &str = "wai.quantum.distance/1";
/// A job's mitigation, in its result (`engine::mitigate`).
@ -96,7 +99,7 @@ pub const DEPRECATIONS: &str = "https://git.transaction.science/Transaction-Scie
pub const ID_BASE: &str = "https://git.transaction.science/Transaction-Science/open-standards/raw/branch/main/wai/wai-quantum/schemas/";
/// This crate's schemas: each file's name and its text.
pub const FILES: [(&str, &str); 22] = [
pub const FILES: [(&str, &str); 23] = [
("job-request.v1.json", include_str!("../../schemas/job-request.v1.json")),
("job-result.v1.json", include_str!("../../schemas/job-result.v1.json")),
("job.v1.json", include_str!("../../schemas/job.v1.json")),
@ -111,6 +114,7 @@ pub const FILES: [(&str, &str); 22] = [
("set.v1.json", include_str!("../../schemas/set.v1.json")),
("provenance.v1.json", include_str!("../../schemas/provenance.v1.json")),
("qec.v1.json", include_str!("../../schemas/qec.v1.json")),
("pulse.v1.json", include_str!("../../schemas/pulse.v1.json")),
("distance.v1.json", include_str!("../../schemas/distance.v1.json")),
("mitigation.v1.json", include_str!("../../schemas/mitigation.v1.json")),
("answer.v1.json", include_str!("../../schemas/answer.v1.json")),
@ -145,6 +149,7 @@ pub fn names() -> Value {
"set": SET,
"provenance": PROVENANCE,
"qec": QEC,
"pulse": PULSE,
"distance": DISTANCE,
"mitigation": MITIGATION,
"answer": ANSWER,

View file

@ -158,9 +158,11 @@ pub fn for_tool(tool: &str, args: &Value, answer: &Value) -> Option<Value> {
let req = answer.get("request").unwrap_or(args);
match answer.get("error").and_then(Value::as_str) {
Some(e) => Some(of_refusal(req, e)),
None if super::pulse::names_pulse(req) => Some(of_pulse_text(req)),
None => req.get("qasm").and_then(Value::as_str).map(of_text),
}
}),
"plan_quantum_circuit" if super::pulse::names_pulse(args) => Some(of_pulse_text(args)),
"repair_qasm" => answer.get("verdict").cloned(),
"check_equivalent" => Some(of_equivalence(args.get("a").and_then(Value::as_str).unwrap_or(""), args.get("b").and_then(Value::as_str).unwrap_or(""), answer)),
"compile_qasm" => Some(of_compile(qasm(), answer.get("result").unwrap_or(answer))),
@ -676,7 +678,9 @@ pub fn of_refusal(request: &Value, error: &str) -> Value {
repairs = r["repairs"].clone();
src = r["qasm"].as_str().map(str::to_string);
}
let pulse = super::pulse::names_pulse(request);
let p = match &src {
Some(_) if pulse => super::pulse::lint(request),
Some(s) => parsed(s).0,
None => json!({ "ok": false, "text": error }),
};
@ -684,6 +688,7 @@ pub fn of_refusal(request: &Value, error: &str) -> Value {
v["repairs"] = repairs;
if let Some(s) = &src
&& v["parsed"]["ok"] == true
&& !pulse
{
v["critique"] = smells(s);
}
@ -692,6 +697,25 @@ pub fn of_refusal(request: &Value, error: &str) -> Value {
v
}
/// The verdict of a pulse run ([`super::pulse`]): what the pulse reader
/// said of the program (the result's diagnostics), and the run's method and
/// accuracy. A pulse run checks no intent.
pub fn of_pulse(result: &Value) -> Value {
let parsed = json!({ "ok": true, "diagnostics": result.get("diagnostics").cloned().unwrap_or_else(|| json!([])) });
let mut v = base(parsed);
v["executed"] = json!({ "method": result["method"], "accuracy": result.pointer("/accuracy/class").cloned().unwrap_or(Value::Null) });
conclude(&mut v);
v
}
/// The verdict of a pulse job not yet run: what the pulse reader says of
/// its program under its options.
pub fn of_pulse_text(request: &Value) -> Value {
let mut v = base(super::pulse::lint(request));
conclude(&mut v);
v
}
/// The verdict of a program read and nothing more: what the lint, the
/// critique, the drawing and the classical shortcuts answer about.
pub fn of_text(src: &str) -> Value {

View file

@ -52,6 +52,11 @@ pub mod gates;
#[cfg(feature = "quantum_lang")]
pub mod qasm;
// Pulse programs integrated under a master equation, the same bits everywhere,
// the error bounded and stated.
#[cfg(feature = "quantum_pulse")]
pub mod quantum_pulse;
// The job service's engine: plan, run and answer a job, the same on every host.
#[cfg(feature = "quantum_engine")]
pub mod engine;

View file

@ -45,6 +45,9 @@ pub mod parse;
pub mod value;
pub(crate) mod elab;
mod unroll;
/// OpenPulse: the calibrations read into a schedule on frames, for the pulse
/// simulator (`quantum_pulse`).
pub mod pulse;
pub use diag::{Diagnostic, Outcome, Span};

View file

@ -255,6 +255,16 @@ table! {
DEFCAL = "Q450", Preserved, "`defcal`: kept verbatim for export, never executed; the gate's matrix comes from its `gate` definition or the standard library.";
CAL = "Q451", Preserved, "`cal { … }`: kept verbatim for export, never executed.";
DEFCALGRAMMAR = "Q452", Preserved, "`defcalgrammar`: kept verbatim for export.";
PULSE_LOWERED = "Q453", Lowered, "In a pulse run, a `cal` or `defcal` body read as OpenPulse and lowered to the schedule on frames (plays, delays, phase and frequency changes, captures and barriers) that the pulse simulator integrates.";
PULSE_GRAMMAR = "Q454", Refused, "In a pulse run, a `defcalgrammar` other than `\"openpulse\"`: the pulse reader reads OpenPulse alone.";
PULSE_STATEMENT = "Q455", Refused, "A statement in a calibration that the pulse reader does not take: classical control, a frame's attributes or `get_phase`, an `extern` function, a frame made inside a `defcal`, a nested calibration, or a `defcal` over qubit arguments (the message names it and what to write instead).";
PULSE_WAVEFORM = "Q456", Refused, "A waveform that does not read: a template the reader does not know, arguments of the wrong number or kind, a length or a width that is not positive, a sample that is not a number, or more samples than it holds.";
PULSE_FRAME = "Q457", Refused, "A frame or a port that does not read: a play, a delay or a phase or frequency change on a name that is not a frame, `newframe` on a name that is not a port, a frequency that is not a number, or a port that reaches no qubit.";
PULSE_TIMING = "Q458", Refused, "A duration in a calibration that is not a whole, non-negative number of samples at the sample period (`dt`), or a schedule longer than the reader holds.";
PULSE_NO_CALIBRATION = "Q459", Refused, "In a pulse run, a gate or a measurement on physical qubits that no `defcal` calibrates for those qubits and those parameters.";
PULSE_PROGRAM = "Q460", Refused, "In a pulse run, a statement outside the calibrations that the pulse reader does not take: declared qubits (a pulse program names physical qubits, `$0`), a modifier, `reset`, classical control, a subroutine, or a measurement into something other than a bit.";
PULSE_CAPTURE = "Q461", Refused, "A capture that does not read: not on a frame of an acquisition port, without a duration or a waveform, or its result put somewhere other than a bit.";
PULSE_NOT_DRIVEN = "Q462", Warned, "A play on a measurement port: scheduled and timed, its effect on the readout not simulated (a capture reads the qubit's state).";
// The bit-exact method.
NOT_DYADIC = "Q500", Refused, "An angle that is not a dyadic multiple of 2π (2πm/2^k, k ≤ 32): the bit-exact method refuses it, never rounds it; the methods in double precision take it.";

View file

@ -33,6 +33,13 @@ impl<'a> Parser<'a> {
Parser { src, toks, pos: 0, version: Version::Unstated }
}
/// How many of its tokens have been read: a reader of another grammar
/// that borrows these expressions (the pulse reader) knows where one
/// ends.
pub(crate) fn read_so_far(&self) -> usize {
self.pos
}
fn peek(&self) -> Option<&'a Tok> {
self.toks.get(self.pos).map(|t| &t.tok)
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,213 @@
use super::*;
const CAL: &str = "OPENQASM 3.0;\ndefcalgrammar \"openpulse\";\ncal {\n extern port d0;\n extern port a0;\n extern port m0;\n frame q0 = newframe(d0, 5.0e9, 0.0);\n frame r0 = newframe(m0, 7.0e9, 0.0);\n frame c0 = newframe(a0, 7.0e9, 0.0);\n}\n";
fn sched(src: &str) -> Schedule {
read(src, &Options::default()).unwrap_or_else(|d| panic!("{d}\n{src}")).schedule
}
fn refused(src: &str) -> Diagnostic {
match read(src, &Options::default()) {
Ok(r) => panic!("read, expected a refusal: {:?}", r.schedule.instructions),
Err(d) => d,
}
}
fn plays(s: &Schedule) -> Vec<(usize, u64, usize)> {
s.instructions.iter().filter_map(|i| match i.op { Op::Play { waveform, .. } => Some((i.frame, i.start, waveform)), _ => None }).collect()
}
#[test]
fn a_program_reads_to_its_schedule_on_frames() {
let src = format!(
"{CAL}defcal x $0 {{ play(q0, gaussian(0.2, 160dt, 40dt)); }}\ndefcal measure $0 -> bit {{ play(r0, constant(0.1, 400dt)); return capture_v2(c0, 400dt); }}\nbit[1] c;\nx $0;\nx $0;\nc[0] = measure $0;\n"
);
let r = read(&src, &Options::default()).unwrap();
let s = &r.schedule;
assert_eq!(s.grammar.as_deref(), Some("openpulse"));
assert_eq!(s.ports.iter().map(|p| (p.name.as_str(), p.kind, p.qubit)).collect::<Vec<_>>(), [("d0", PortKind::Drive, 0), ("a0", PortKind::Acquire, 0), ("m0", PortKind::Measure, 0)]);
assert_eq!(s.frames.len(), 3);
// The two gaussians are one waveform, played back to back.
assert_eq!(plays(s)[..2], [(0, 0, 0), (0, 160, 0)]);
assert_eq!(s.waveforms[0].samples.len(), 160);
// The measurement starts when its qubit is free: after both gates, its
// frames together.
let cap = s.instructions.iter().find(|i| matches!(i.op, Op::Capture { .. })).unwrap();
assert_eq!((cap.frame, cap.start), (2, 320));
assert_eq!(cap.op, Op::Capture { samples: 400, bit: Some(0) });
let tone = s.instructions.iter().find(|i| i.frame == 1).unwrap();
assert_eq!(tone.start, 320);
assert_eq!(s.bits, ["c[0]"]);
assert_eq!(s.duration, 720);
assert_eq!(s.calls.iter().map(|c| (c.name.as_str(), c.start, c.end)).collect::<Vec<_>>(), [("x", 0, 160), ("x", 160, 320), ("measure", 320, 720)]);
assert_eq!(s.qubits(), [0]);
// A note for each calibration, and the readout tone's warning once.
assert_eq!(r.diagnostics.iter().filter(|d| d.code == diag::PULSE_LOWERED).count(), 3);
assert_eq!(r.diagnostics.iter().filter(|d| d.code == diag::PULSE_NOT_DRIVEN).count(), 1);
assert_eq!(s.calibrations.len(), 4);
}
#[test]
fn barriers_align_frames_and_a_gate_starts_its_frames_together() {
let src = format!(
"{CAL}cal {{ extern port d1; frame q1 = newframe(d1, 5.1e9, 0); }}\ndefcal x $0 {{ play(q0, constant(0.1, 100dt)); }}\ndefcal x $1 {{ play(q1, constant(0.1, 30dt)); }}\ndefcal cz $0, $1 {{ play(q0, constant(0.1, 10dt)); play(q1, constant(0.1, 10dt)); }}\nx $0;\nx $1;\ncz $0, $1;\nbarrier $1;\ndelay[50dt] $1;\nx $1;\n"
);
let s = sched(&src);
let p = plays(&s);
let q1 = s.frames.iter().position(|f| f.name == "q1").unwrap();
// cz waits for both frames: q0 is at 100, q1 at 30.
assert!(p.contains(&(0, 100, 2)) && p.contains(&(q1, 100, 2)), "{p:?}");
// The delay on $1 moves its frame from 110 to 160.
assert_eq!(p.last().map(|x| (x.0, x.1)), Some((q1, 160)));
let d = s.instructions.iter().find(|i| matches!(i.op, Op::Delay { .. })).unwrap();
assert_eq!((d.frame, d.start, &d.op), (q1, 110, &Op::Delay { samples: 50 }));
// Inside a calibration, a barrier aligns the frames it names.
let s = sched(&format!("{CAL}cal {{ play(q0, constant(0.1, 70dt)); barrier q0, c0; capture(c0, 10dt); }}"));
assert_eq!(s.instructions.iter().find(|i| matches!(i.op, Op::Capture { .. })).map(|i| i.start), Some(70));
}
#[test]
fn phase_and_frequency_follow_the_frame_and_the_carrier_runs_on() {
let src = format!("{CAL}cal {{ shift_phase(q0, pi/2); play(q0, constant(0.1, 10dt)); set_phase(q0, -pi); shift_phase(q0, pi/4); play(q0, constant(0.1, 10dt)); set_frequency(q0, 5.1e9); play(q0, constant(0.1, 10dt)); shift_frequency(q0, -2.0e8); play(q0, constant(0.1, 10dt)); }}");
let s = sched(&src);
let ps: Vec<(f64, f64, f64, u64)> = s.instructions.iter().filter_map(|i| match i.op { Op::Play { phase, frequency_hz, carrier, .. } => Some((phase, frequency_hz, carrier, i.start)), _ => None }).collect();
assert_eq!(ps.len(), 4);
assert!((ps[0].0 - PI / 2.0).abs() < 1e-15);
assert!((ps[1].0 - 5.0 * PI / 4.0).abs() < 1e-12, "set to −π (π after wrapping), then a quarter turn: {}", ps[1].0);
assert_eq!((ps[0].1, ps[2].1, ps[3].1), (5.0e9, 5.1e9, 4.9e9));
// At 5 GHz and dt = 0.5 ns the carrier turns 2.5 cycles a sample: after
// 10 samples it is back at 0, after 20 too; then at 5.1 GHz, 2.55 a
// sample, ten samples later half a cycle on.
let dt = 0.5e-9;
let want = [0.0, fract(5.0e9 * dt * 10.0), fract(5.0e9 * dt * 20.0), fract(fract(5.0e9 * dt * 20.0) + 5.1e9 * dt * 10.0)];
// Compared on the circle: 0.999… and 0 are one phase.
let near = |a: f64, b: f64| {
let d = fract(a - b);
d.min(1.0 - d) < 1e-9
};
for (p, w) in ps.iter().zip(want) {
assert!(near(p.2, w), "carrier {} ≠ {w}", p.2);
}
assert!(near(ps[3].2, 0.5), "{}", ps[3].2);
}
#[test]
fn each_template_samples_as_its_formula_says() {
let s = sched(&format!(
"{CAL}cal {{\n waveform g = gaussian(0.5, 9dt, 2dt);\n waveform k = constant(0.25 + 0.5im, 3dt);\n waveform d = drag(0.5, 9dt, 2dt, 0.3);\n waveform q = gaussian_square(1.0, 11dt, 4dt, 1.5dt);\n waveform h = sech(0.4, 5dt, 1dt);\n waveform n = sine(1.0, 4dt, 5.0e8, 0.0);\n waveform e = {{0.1, -0.2im, 0.3 + 0.4im}};\n play(q0, g); play(q0, k); play(q0, d); play(q0, q); play(q0, h); play(q0, n); play(q0, e);\n}}"
));
let w = |n: &str| &s.waveforms.iter().find(|w| w.name == n).unwrap_or_else(|| panic!("{n}: {:?}", s.waveforms.iter().map(|w| &w.name).collect::<Vec<_>>())).samples;
let g = w("g");
assert_eq!(g.len(), 9);
for (k, &(re, im)) in g.iter().enumerate() {
let t = k as f64 - 4.0;
assert!((re - 0.5 * (-(t * t) / 8.0).exp()).abs() < 1e-15 && im == 0.0, "gaussian {k}");
}
assert_eq!(g[4], (0.5, 0.0));
assert_eq!(g[0], g[8], "centred, symmetric");
assert_eq!(w("k"), &vec![(0.25, 0.5); 3]);
let d = w("d");
for (k, &(re, im)) in d.iter().enumerate() {
let t = k as f64 - 4.0;
let gk = 0.5 * (-(t * t) / 8.0).exp();
assert!((re - gk).abs() < 1e-15 && (im + 0.3 * t / 4.0 * gk).abs() < 1e-15, "drag {k}: {re} {im}");
}
assert_eq!(d[4].1, 0.0);
let q = w("q");
assert!(q[3..8].iter().all(|&x| x == (1.0, 0.0)), "flat for |t| ≤ 2: {q:?}");
assert!((q[2].0 - (-1.0f64 / (2.0 * 2.25)).exp()).abs() < 1e-15 && q[0] == q[10]);
let h = w("h");
assert!((h[2].0 - 0.4).abs() < 1e-15 && (h[0].0 - 0.4 / 2.0f64.cosh()).abs() < 1e-15, "{h:?}");
let n = w("n");
// 500 MHz at 0.5 ns: a quarter cycle a sample.
for (k, want) in [0.0, 1.0, 0.0, -1.0].into_iter().enumerate() {
assert!((n[k].0 - want).abs() < 1e-15, "sine {k}: {}", n[k].0);
}
assert_eq!(w("e"), &vec![(0.1, 0.0), (0.0, -0.2), (0.3, 0.4)]);
// Read twice, every sample is the same bits.
let again = sched(&format!("{CAL}cal {{ waveform g = gaussian(0.5, 9dt, 2dt); play(q0, g); }}"));
assert_eq!(again.waveforms[0].samples.iter().map(|x| (x.0.to_bits(), x.1.to_bits())).collect::<Vec<_>>(), g.iter().map(|x| (x.0.to_bits(), x.1.to_bits())).collect::<Vec<_>>());
}
#[test]
fn a_parametric_calibration_binds_its_angle_and_a_fixed_one_wins() {
let src = format!(
"{CAL}defcal rx(angle[20] theta) $0 {{ play(q0, gaussian(theta / pi * 0.4, 40dt, 10dt)); }}\ndefcal rx(pi) $0 {{ play(q0, drag(0.4, 40dt, 10dt, 0.5)); }}\nconst float half = pi / 2;\nrx(half) $0;\nrx(pi) $0;\n"
);
let s = sched(&src);
assert_eq!(s.waveforms.len(), 2);
assert!((s.waveforms[0].samples[20].0 - 0.2 * (-(0.25f64) / 200.0).exp()).abs() < 1e-15, "{:?}", s.waveforms[0].samples[20]);
assert!(s.waveforms[1].name.starts_with("drag("), "the fixed calibration plays: {}", s.waveforms[1].name);
assert_eq!(s.calls.iter().map(|c| c.params[0]).collect::<Vec<_>>(), [PI / 2.0, PI]);
}
#[test]
fn durations_in_nanoseconds_must_be_whole_samples() {
let ok = sched(&format!("{CAL}cal {{ play(q0, constant(0.1, 80ns)); delay[1us] q0; }}"));
assert_eq!(ok.waveforms[0].samples.len(), 160);
assert_eq!(ok.duration, 2160);
let d = read(&format!("{CAL}cal {{ play(q0, constant(0.1, 80ns)); }}"), &Options::new(0.3)).unwrap_err();
assert_eq!(d.code, diag::PULSE_TIMING, "{d}");
// Another sample period reads the same program to another schedule.
let s = read(&format!("{CAL}cal {{ play(q0, constant(0.1, 80ns)); }}"), &Options::new(0.25)).unwrap().schedule;
assert_eq!(s.waveforms[0].samples.len(), 320);
}
#[test]
fn ports_map_by_name_and_by_the_options() {
let src = "OPENQASM 3.0;\ncal { extern port u7; frame f = newframe(u7, 5.0e9, 0); play(f, constant(0.1, 4dt)); }\n";
assert_eq!(refused(src).code, diag::PULSE_FRAME);
let s = read(src, &Options::default().with_port(Port::new("u7", PortKind::Drive, 2))).unwrap().schedule;
assert_eq!(s.qubits(), [2]);
}
/// Every refusal names its code and its place in the program.
#[test]
fn what_the_pulse_reader_does_not_take_is_refused_with_its_code_and_place() {
let cases: &[(&str, &str, u32)] = &[
("OPENQASM 3.0;\ndefcalgrammar \"other\";\n", diag::PULSE_GRAMMAR, 2),
("{CAL}cal {\n if (true) { play(q0, constant(0.1, 4dt)); }\n}\n", diag::PULSE_STATEMENT, 12),
("{CAL}cal {\n extern foo(float a) -> waveform;\n}\n", diag::PULSE_STATEMENT, 12),
("{CAL}cal {\n q0.phase = 1.0;\n}\n", diag::PULSE_STATEMENT, 12),
("{CAL}cal {\n play(q0, triangle(0.1, 4dt));\n}\n", diag::PULSE_WAVEFORM, 12),
("{CAL}cal {\n waveform w = triangle(0.1, 4dt);\n}\n", diag::PULSE_WAVEFORM, 12),
("{CAL}cal {\n play(q0, gaussian(0.1, 4dt));\n}\n", diag::PULSE_WAVEFORM, 12),
("{CAL}cal {\n play(q0, gaussian(0.1, 4dt, 0dt));\n}\n", diag::PULSE_WAVEFORM, 12),
("{CAL}cal {\n play(d0, constant(0.1, 4dt));\n}\n", diag::PULSE_FRAME, 12),
("{CAL}cal {\n play(c0, constant(0.1, 4dt));\n}\n", diag::PULSE_FRAME, 12),
("{CAL}cal {\n play(q0, constant(0.1, 4.5dt));\n}\n", diag::PULSE_TIMING, 12),
("{CAL}cal {\n delay[-4dt] q0;\n}\n", diag::PULSE_TIMING, 12),
("{CAL}x $0;\n", diag::PULSE_NO_CALIBRATION, 11),
("{CAL}defcal x $1 { play(q0, constant(0.1, 4dt)); }\nx $0;\n", diag::PULSE_NO_CALIBRATION, 12),
("{CAL}qubit q;\n", diag::PULSE_PROGRAM, 11),
("{CAL}defcal x $0 { play(q0, constant(0.1, 4dt)); }\nctrl @ x $0, $1;\n", diag::PULSE_PROGRAM, 12),
("{CAL}cal {\n capture_v2(q0, 10dt);\n}\n", diag::PULSE_CAPTURE, 12),
("{CAL}bit c;\ndefcal measure $0 -> bit { play(r0, constant(0.1, 4dt)); }\nc = measure $0;\n", diag::PULSE_CAPTURE, 13),
("{CAL}defcal x q { play(q0, constant(0.1, 4dt)); }\n", diag::PULSE_STATEMENT, 11),
("{CAL}defcal x $0 { frame g = newframe(d0, 5e9, 0); }\n", diag::PULSE_STATEMENT, 11),
("{CAL}cal {\n play(nothing, constant(0.1, 4dt));\n}\n", diag::UNDECLARED, 12),
];
for &(src, code, line) in cases {
let src = src.replace("{CAL}", CAL);
let d = refused(&src);
assert_eq!((d.code, d.line()), (code, Some(line as usize)), "{d}\n{src}");
assert!(d.span.is_some_and(|s| s.column >= 1 && s.length >= 1), "{d}");
}
// The place is the very text: the template's arguments, in their column.
let d = refused(&format!("{CAL}cal {{\n play(q0, constant(0.1, 4.5dt));\n}}\n"));
assert_eq!(d.span.map(|s| (s.line, s.column)), Some((12, 26)), "{d}");
}
#[test]
fn the_schedule_writes_as_json_with_every_instruction() {
let s = sched(&format!("{CAL}bit c;\ndefcal measure $0 -> bit {{ return capture(c0, 8dt); }}\ncal {{ play(q0, constant(0.5, 2dt)); shift_phase(q0, 0.5); }}\nc = measure $0;\n"));
let j = s.to_json();
// The block's play on $0 comes before the measurement of $0.
assert_eq!(j["duration"], 10);
assert_eq!(j["frames"][0]["kind"], "drive");
assert_eq!(j["waveforms"][0]["samples"], json!([[0.5, 0.0], [0.5, 0.0]]));
let ops: Vec<&str> = j["instructions"].as_array().unwrap().iter().map(|i| i["op"].as_str().unwrap()).collect();
assert_eq!(ops, ["play", "shift_phase", "capture"]);
assert_eq!(j["instructions"][2]["bit"], "c");
assert_eq!(j["calls"][0]["name"], "measure");
}

View file

@ -0,0 +1,834 @@
//! The pulse simulator: a schedule on frames ([`crate::qasm::pulse`]) run on
//! the qubits its ports reach, under a Lindblad master equation, by a
//! fixed-step integrator whose result is the same bits on every platform
//! and whose error is bounded and stated.
//!
//! # The model
//!
//! Each qubit is a ladder of [`Device::levels`] levels (2, or 3 to see
//! leakage), in the frame rotating at its own frequency f_q, under the
//! rotating-wave approximation, with the envelope held for each sample:
//!
//! ```text
//! H(t) = Σ_q [ (Ω_q/2)(z_q(t) a_q + z̄_q(t) a_q†) + (α_q/2) n_q(n_q − 1) ] + Σ_{p<q} ζ_pq n_p n_q
//! dρ/dt = −i[H, ρ] + Σ_k ( L_k ρ L_k† − ½{L_k† L_k, ρ} )
//! ```
//!
//! - Ω_q = 2π·[`Qubit::rabi_hz`]: a constant play of amplitude A turns
//! P(1) at rabi_hz·|A| cycles a second; α_q = 2π·[`Qubit::anharmonicity_hz`];
//! ζ_pq = 2π times a static coupling's rate ([`Device::couplings`]).
//! - z_q(t) is the sum of what the drive frames of q play at t: sample
//! A(k) times the frame's carrier e^{i(2πft + φ)} seen from the qubit's
//! frame, A·e^{i(φ + 2π(carrier + f·(t − t₀)) − 2πf_q t)}. At φ = 0 a real
//! envelope turns the qubit about +X; at φ = π/2 about −Y.
//! - L_k: √(1/T1)·a_q (relaxation) and √(2γ_q)·n_q (pure dephasing,
//! γ_q = 1/T2 − 1/(2T1)), for each qubit whose times are given.
//! - A capture reads its qubit at its first sample, a level above 0 read as
//! 1; a capture ends its qubit's schedule (no play may follow it), since
//! the readout's back-action is not modelled. Plays on measurement ports
//! drive nothing here.
//!
//! # The integrator
//!
//! Within a sample the envelope is constant and each driven qubit hears one
//! tone (two frames of different frequencies on one qubit at once are
//! refused), so in the frame turning with each qubit's tone the generator 𝓛
//! is constant over the sample. Each sample is `m` steps of h = dt/m, each
//! the Taylor series of e^{h𝓛} to order [`ORDER`]; the frame's turn back is
//! a diagonal phase. Only `+ − × ÷` and `sqrt` are used, in a fixed order,
//! with the crate's own sine and cosine, so the bits do not depend on the
//! platform.
//!
//! **The bound.** In the trace norm, ‖𝓛‖ ≤ λ = 2‖H‖ + 2Σ_k‖L_k‖², each
//! norm bounded term by term. Each step's series misses at most
//! τ = (hλ)^{K+1}/(K+1)! / (1 − hλ/(K+2)) of the state (K = [`ORDER`]), and
//! the exact evolution is a contraction, so after the whole schedule the
//! state is within E = T(1 + T) of the exact solution of the equation
//! above, T the sum of every step's τ. Every probability the result gives
//! is within E of the model's. Floating-point rounding is stated beside it
//! as an estimate, not a proof. A plan whose step is longer than 1/λ, or
//! whose bound and rounding exceed the tolerance, is refused, naming the
//! steps that would meet it ([`Refusal::Bound`]).
//!
//! What the bound is not: the model itself (the rotating-wave
//! approximation, the levels kept, the held envelope) is what is solved;
//! the bound is the distance from its exact solution.
use crate::qasm::pulse::{Op, PortKind, Schedule};
use crate::repro;
use std::collections::BTreeMap;
use std::f64::consts::TAU;
/// The order of each step's Taylor series.
pub const ORDER: u32 = 12;
/// The largest Hilbert space simulated: levels to the power of the qubits.
pub const MAX_DIM: usize = 16;
/// How far a frame may be from its qubit's frequency, Hz: the rotating-wave
/// model does not hold far from resonance.
pub const MAX_DETUNING_HZ: f64 = 1.0e9;
/// The tolerance a plan takes when none is given.
pub const DEFAULT_TOLERANCE: f64 = 1e-6;
/// The most steps a sample is divided into.
pub const MAX_SUBSTEPS: u32 = 4096;
/// The longest step against the generator's norm (hλ) the series is taken at.
pub const MAX_STEP_NORM: f64 = 1.0;
/// The most work a plan takes: complex multiply-adds.
pub const MAX_WORK: u64 = 400_000_000_000;
/// The step a plan chooses when none is given: hλ at most this.
const AUTO_STEP_NORM: f64 = 0.5;
/// The unit roundoff of double precision.
const EPS: f64 = 1.0 / 9_007_199_254_740_992.0;
/// One qubit's parameters.
#[derive(Clone, Debug, PartialEq)]
#[non_exhaustive]
pub struct Qubit {
/// Its frequency, Hz.
pub frequency_hz: f64,
/// The Rabi frequency at full amplitude, Hz.
pub rabi_hz: f64,
/// The anharmonicity, Hz (it matters with three levels).
pub anharmonicity_hz: f64,
/// Relaxation and coherence times, ns; none is no decay.
pub t1_ns: Option<f64>,
pub t2_ns: Option<f64>,
}
impl Qubit {
pub fn new(frequency_hz: f64, rabi_hz: f64) -> Qubit {
Qubit { frequency_hz, rabi_hz, anharmonicity_hz: 0.0, t1_ns: None, t2_ns: None }
}
pub fn with_anharmonicity(mut self, hz: f64) -> Qubit {
self.anharmonicity_hz = hz;
self
}
pub fn with_times(mut self, t1_ns: Option<f64>, t2_ns: Option<f64>) -> Qubit {
self.t1_ns = t1_ns;
self.t2_ns = t2_ns;
self
}
}
/// The device a schedule runs on: the levels kept, each qubit's parameters,
/// and the static couplings between pairs (Hz).
#[derive(Clone, Debug, PartialEq)]
#[non_exhaustive]
pub struct Device {
pub levels: usize,
pub qubits: BTreeMap<u32, Qubit>,
pub couplings: Vec<(u32, u32, f64)>,
}
impl Device {
pub fn new(levels: usize) -> Device {
Device { levels, qubits: BTreeMap::new(), couplings: Vec::new() }
}
pub fn with_qubit(mut self, q: u32, p: Qubit) -> Device {
self.qubits.insert(q, p);
self
}
/// A static ZZ coupling of `hz` between `a` and `b`.
pub fn with_coupling(mut self, a: u32, b: u32, hz: f64) -> Device {
self.couplings.push((a.min(b), a.max(b), hz));
self
}
}
/// What a plan is held to: the tolerance on its bound, and the steps a
/// sample is divided into (chosen when not given).
#[derive(Clone, Debug, PartialEq)]
#[non_exhaustive]
pub struct Settings {
pub tolerance: f64,
pub substeps: Option<u32>,
}
impl Default for Settings {
fn default() -> Settings {
Settings { tolerance: DEFAULT_TOLERANCE, substeps: None }
}
}
impl Settings {
pub fn new(tolerance: f64, substeps: Option<u32>) -> Settings {
Settings { tolerance, substeps }
}
}
/// Why a schedule is not run.
#[derive(Clone, Debug, PartialEq)]
#[non_exhaustive]
pub enum Refusal {
/// The schedule or the device is outside the model.
Model(String),
/// The step or the length would put the error past what can be stated
/// within the tolerance.
Bound(String),
}
impl std::fmt::Display for Refusal {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Refusal::Model(s) | Refusal::Bound(s) => f.write_str(s),
}
}
}
/// How a schedule will be run, and the error it will be within.
#[derive(Clone, Debug, PartialEq)]
#[non_exhaustive]
pub struct Plan {
/// The qubits simulated, in order: bit j of an outcome is `qubits[j]`.
pub qubits: Vec<u32>,
pub levels: usize,
pub dim: usize,
pub samples: u64,
pub dt_ns: f64,
pub substeps: u32,
pub step_ns: f64,
/// Samples times substeps.
pub steps: u64,
pub order: u32,
/// The largest bound on the generator's norm over the samples, per ns.
pub norm_max: f64,
/// The proved bound on the trace distance from the model's exact state.
pub truncation_bound: f64,
/// The estimate of floating-point rounding beside it.
pub rounding_estimate: f64,
pub tolerance: f64,
/// Complex multiply-adds.
pub work: u64,
}
/// A capture's reading: its qubit's probability of reading 1 (level 1 or
/// above) and of level 2, at its first sample.
#[derive(Clone, Debug, PartialEq)]
#[non_exhaustive]
pub struct Capture {
pub instruction: usize,
pub qubit: u32,
pub start: u64,
pub p1: f64,
pub p2: f64,
}
/// A schedule run: the final state, its populations, each capture's reading
/// and, where every capture starts at one sample on its own qubit, their
/// joint distribution.
#[derive(Clone, Debug, PartialEq)]
#[non_exhaustive]
pub struct Outcome {
pub plan: Plan,
/// The final density matrix, row by row, `dim` × `dim`.
pub rho: Vec<(f64, f64)>,
/// The final state read on every simulated qubit: index bit j is
/// `plan.qubits[j]`, a level above 0 read as 1.
pub populations: Vec<f64>,
/// Each simulated qubit's final probability of level 2.
pub leakage: Vec<f64>,
pub captures: Vec<Capture>,
/// Over the captures, index bit j the j-th capture.
pub joint: Option<Vec<f64>>,
/// The final state's trace.
pub trace: f64,
}
impl Outcome {
/// The BLAKE3 of the final density matrix's bits, row by row (re then
/// im, each as its IEEE-754 bits, little-endian), in hex.
pub fn density_hash(&self) -> String {
let mut h = blake3::Hasher::new();
for &(re, im) in &self.rho {
h.update(&re.to_bits().to_le_bytes());
h.update(&im.to_bits().to_le_bytes());
}
h.finalize().to_hex().to_string()
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq)]
struct C {
re: f64,
im: f64,
}
impl C {
fn mul(self, o: C) -> C {
C { re: self.re * o.re - self.im * o.im, im: self.re * o.im + self.im * o.re }
}
fn add(self, o: C) -> C {
C { re: self.re + o.re, im: self.im + o.im }
}
fn scale(self, k: f64) -> C {
C { re: self.re * k, im: self.im * k }
}
}
/// A play as the simulator hears it.
#[derive(Clone, Debug)]
struct Play {
start: u64,
end: u64,
waveform: usize,
phase: f64,
/// The carrier's phase against the qubit's frame at `start`, cycles.
base: f64,
/// Its turn per sample against the qubit's frame, cycles.
slope: f64,
/// The tone's offset from the qubit, rad/ns.
detuning: f64,
frequency_hz: f64,
}
/// The plays on each simulated qubit, and the qubits' rates.
struct Model {
qubits: Vec<u32>,
levels: usize,
dim: usize,
plays: Vec<Vec<Play>>,
/// Ω_q, rad/ns.
omega: Vec<f64>,
/// Each basis state's level of each qubit: `digit[i * n + j]`.
digit: Vec<usize>,
/// The static diagonal of H: anharmonicity and couplings, rad/ns.
static_diag: Vec<f64>,
/// The relaxation jumps: for each, its entries (row, column, value).
jumps: Vec<Vec<(usize, usize, f64)>>,
/// The diagonal of the dissipator: −½(D_i + D_j) less the dephasing, per (i, j).
damp: Vec<f64>,
/// ‖H‖'s static part, and Σ‖L‖², per ns.
static_norm: f64,
jump_norm: f64,
captures: Vec<(usize, u32, u64)>,
}
fn model(s: &Schedule, d: &Device) -> Result<Model, Refusal> {
let m = |s: String| Refusal::Model(s);
if !(2..=3).contains(&d.levels) {
return Err(m(format!("{} levels; the simulator keeps 2 or 3", d.levels)));
}
// The qubits: those a drive frame or a capture reaches.
let mut set = std::collections::BTreeSet::new();
for i in &s.instructions {
match s.frame_kind(i.frame) {
PortKind::Drive => {
set.insert(s.frame_qubit(i.frame));
}
PortKind::Acquire if matches!(i.op, Op::Capture { .. }) => {
set.insert(s.frame_qubit(i.frame));
}
_ => {}
}
}
let qubits: Vec<u32> = set.into_iter().collect();
let n = qubits.len();
let levels = d.levels;
let mut dim = 1usize;
for _ in 0..n {
dim = dim.saturating_mul(levels);
if dim > MAX_DIM {
return Err(m(format!("{n} qubits at {levels} levels is a space of {}; the simulator holds at most {MAX_DIM}", levels.pow(n.min(8) as u32))));
}
}
let params: Vec<&Qubit> = qubits.iter().map(|q| d.qubits.get(q).ok_or_else(|| m(format!("qubit {q} has no parameters in the device")))).collect::<Result<_, _>>()?;
for (q, p) in qubits.iter().zip(&params) {
if !(p.frequency_hz.is_finite() && p.frequency_hz > 0.0 && p.rabi_hz.is_finite() && p.rabi_hz >= 0.0 && p.anharmonicity_hz.is_finite()) {
return Err(m(format!("qubit {q}: its frequency is positive and its Rabi frequency and anharmonicity finite")));
}
for (what, t) in [("T1", p.t1_ns), ("T2", p.t2_ns)] {
if t.is_some_and(|t| !(t.is_finite() && t > 0.0)) {
return Err(m(format!("qubit {q}: {what} is a positive time")));
}
}
if let (Some(t1), Some(t2)) = (p.t1_ns, p.t2_ns) {
if t2 > 2.0 * t1 {
return Err(m(format!("qubit {q}: T2 ({t2} ns) is at most 2·T1 ({} ns)", 2.0 * t1)));
}
}
}
let u = s.dt_ns * 1e-9;
let mut plays: Vec<Vec<Play>> = vec![Vec::new(); n];
let mut captures: Vec<(usize, u32, u64)> = Vec::new();
for (idx, i) in s.instructions.iter().enumerate() {
let q = s.frame_qubit(i.frame);
let Some(j) = qubits.iter().position(|&x| x == q) else { continue };
match (&i.op, s.frame_kind(i.frame)) {
(Op::Play { waveform, phase, frequency_hz, carrier }, PortKind::Drive) => {
let fq = params[j].frequency_hz;
let off = frequency_hz - fq;
if off.abs() > MAX_DETUNING_HZ {
return Err(m(format!("{} plays at {frequency_hz} Hz, {} MHz from qubit {q} at {fq} Hz: the rotating-wave model holds within {} MHz", s.frames[i.frame].name, off / 1e6, MAX_DETUNING_HZ / 1e6)));
}
let len = s.waveforms[*waveform].samples.len() as u64;
plays[j].push(Play {
start: i.start,
end: i.start + len,
waveform: *waveform,
phase: *phase,
base: fract(carrier - fract(fq * u * i.start as f64)),
slope: off * u,
detuning: TAU * off * 1e-9,
frequency_hz: *frequency_hz,
});
}
(Op::Capture { .. }, PortKind::Acquire) => {
if let Some(c) = captures.iter().find(|c| c.1 == q) {
return Err(m(format!("a second capture of qubit {q} (at sample {}, after one at {}): a capture ends its qubit's schedule here, since the readout's back-action is not modelled", i.start, c.2)));
}
captures.push((idx, q, i.start));
}
_ => {}
}
}
for (j, ps) in plays.iter_mut().enumerate() {
ps.sort_by(|a, b| a.start.cmp(&b.start).then(a.end.cmp(&b.end)));
let q = qubits[j];
for (a, b) in ps.iter().zip(ps.iter().skip(1)) {
if b.start < a.end && a.frequency_hz != b.frequency_hz {
return Err(m(format!("two frames at {} Hz and {} Hz drive qubit {q} at once (from sample {}): the simulator takes one tone on a qubit at a time", a.frequency_hz, b.frequency_hz, b.start)));
}
}
// Overlaps further apart than neighbours.
let mut reach: Option<(u64, f64)> = None;
for p in ps.iter() {
if let Some((end, f)) = reach {
if p.start < end && p.frequency_hz != f {
return Err(m(format!("two frames at {f} Hz and {} Hz drive qubit {q} at once (from sample {}): the simulator takes one tone on a qubit at a time", p.frequency_hz, p.start)));
}
}
if reach.is_none_or(|(end, _)| p.end > end) {
reach = Some((p.end, p.frequency_hz));
}
}
if let Some(c) = captures.iter().find(|c| c.1 == q) {
if let Some(p) = ps.iter().find(|p| p.end > c.2) {
return Err(m(format!("a play on qubit {q} that ends at sample {}, after its capture at {}: a capture ends its qubit's schedule here, since the readout's back-action is not modelled", p.end, c.2)));
}
}
}
let mut digit = vec![0usize; dim * n];
for i in 0..dim {
let mut x = i;
for j in 0..n {
digit[i * n + j] = x % levels;
x /= levels;
}
}
let rad = |hz: f64| TAU * hz * 1e-9;
let omega: Vec<f64> = params.iter().map(|p| rad(p.rabi_hz)).collect();
let mut static_diag = vec![0.0; dim];
let mut static_norm = 0.0;
let l1 = (levels - 1) as f64;
for (j, p) in params.iter().enumerate() {
let a = rad(p.anharmonicity_hz);
if levels == 3 && a != 0.0 {
for i in 0..dim {
let k = digit[i * n + j] as f64;
static_diag[i] += a / 2.0 * k * (k - 1.0);
}
static_norm += a.abs();
}
}
for &(qa, qb, hz) in &d.couplings {
if !hz.is_finite() {
return Err(m(format!("the coupling of {qa} and {qb} is a finite rate")));
}
let (Some(ja), Some(jb)) = (qubits.iter().position(|&x| x == qa), qubits.iter().position(|&x| x == qb)) else { continue };
if ja == jb {
return Err(m(format!("a coupling of qubit {qa} with itself")));
}
let z = rad(hz);
for i in 0..dim {
static_diag[i] += z * (digit[i * n + ja] * digit[i * n + jb]) as f64;
}
static_norm += z.abs() * l1 * l1;
}
let mut jumps = Vec::new();
let mut damp = vec![0.0; dim * dim];
let mut jump_norm = 0.0;
let stride = |j: usize| levels.pow(j as u32);
for (j, p) in params.iter().enumerate() {
let g1 = p.t1_ns.map_or(0.0, |t| 1.0 / t);
let gphi = match (p.t1_ns, p.t2_ns) {
(_, None) => 0.0,
(t1, Some(t2)) => (1.0 / t2 - t1.map_or(0.0, |t| 0.5 / t)).max(0.0),
};
if g1 > 0.0 {
let s = stride(j);
let mut e = Vec::new();
for i in 0..dim {
let k = digit[i * n + j];
if k >= 1 {
e.push((i - s, i, (g1 * k as f64).sqrt()));
}
}
jumps.push(e);
jump_norm += g1 * l1;
}
let kappa = 2.0 * gphi;
jump_norm += kappa * l1 * l1;
for a in 0..dim {
for b in 0..dim {
let (na, nb) = (digit[a * n + j] as f64, digit[b * n + j] as f64);
damp[a * dim + b] += -0.5 * g1 * (na + nb) - 0.5 * kappa * (na - nb) * (na - nb);
}
}
}
Ok(Model { qubits, levels, dim, plays, omega, digit, static_diag, jumps, damp, static_norm, jump_norm, captures })
}
fn fract(x: f64) -> f64 {
x - x.floor()
}
/// The plays sounding at each sample, qubit by qubit.
struct Sweep {
next: Vec<usize>,
active: Vec<Vec<usize>>,
}
impl Sweep {
fn new(n: usize) -> Sweep {
Sweep { next: vec![0; n], active: vec![Vec::new(); n] }
}
fn at(&mut self, m: &Model, k: u64) {
for j in 0..m.qubits.len() {
let ps = &m.plays[j];
self.active[j].retain(|&p| ps[p].end > k);
while self.next[j] < ps.len() && ps[self.next[j]].start <= k {
if ps[self.next[j]].end > k {
self.active[j].push(self.next[j]);
}
self.next[j] += 1;
}
}
}
}
/// Σ_{j > K} x^j / j!, bounded above.
fn tail(x: f64) -> f64 {
let mut t = 1.0;
for j in 1..=ORDER + 1 {
t *= x / j as f64;
}
t / (1.0 - x / (ORDER + 2) as f64)
}
/// The bound on the generator's norm at sample `k`, per ns.
fn norm_at(m: &Model, s: &Schedule, sw: &Sweep, k: u64) -> f64 {
let l1 = (m.levels - 1) as f64;
let mut h = m.static_norm;
for j in 0..m.qubits.len() {
let mut amp = 0.0;
let mut det: f64 = 0.0;
for &p in &sw.active[j] {
let pl = &m.plays[j][p];
let (re, im) = s.waveforms[pl.waveform].samples[(k - pl.start) as usize];
amp += (re * re + im * im).sqrt();
det = det.max(pl.detuning.abs());
}
h += m.omega[j] * amp * l1.sqrt() + det * l1;
}
2.0 * h + 2.0 * m.jump_norm
}
/// Plans `s` on `d` under `set`: the qubits, the step, and the error the run
/// will be within, or why it is refused.
pub fn plan(s: &Schedule, d: &Device, set: &Settings) -> Result<Plan, Refusal> {
let m = model(s, d)?;
plan_model(s, &m, set)
}
fn plan_model(s: &Schedule, m: &Model, set: &Settings) -> Result<Plan, Refusal> {
if !(set.tolerance.is_finite() && set.tolerance > 0.0 && set.tolerance < 1.0) {
return Err(Refusal::Model(format!("the tolerance is {}; it is a number between 0 and 1", set.tolerance)));
}
let samples = s.duration;
// Every sample's norm, as counts of each distinct value.
let mut norms: BTreeMap<u64, u64> = BTreeMap::new();
let mut sw = Sweep::new(m.qubits.len());
for k in 0..samples {
sw.at(m, k);
*norms.entry(norm_at(m, s, &sw, k).to_bits()).or_insert(0) += 1;
}
let norm_max = norms.keys().map(|&b| f64::from_bits(b)).fold(0.0, f64::max);
let dt = s.dt_ns;
let d = m.dim as f64;
let cost = {
let drive: usize = m.qubits.len() * 2 * m.dim;
let h = (m.dim + drive) * 2 * m.dim;
let j: usize = m.jumps.iter().map(|e| e.len() * e.len()).sum();
(h + j + m.dim * m.dim) as u64
};
let bound = |sub: u32| -> (f64, f64) {
let h = dt / sub as f64;
let t: f64 = norms.iter().map(|(&b, &c)| c as f64 * sub as f64 * tail(h * f64::from_bits(b))).sum();
let e = if t <= 1.0 { t * (1.0 + t) } else { f64::INFINITY };
let steps = samples as f64 * sub as f64;
(e, steps * (ORDER + 1) as f64 * d * (d + 4.0) * EPS)
};
let least = ((dt * norm_max / MAX_STEP_NORM).ceil() as u32).max(1);
let sub = match set.substeps {
Some(0) => return Err(Refusal::Bound("substeps is at least 1".into())),
Some(sub) if sub > MAX_SUBSTEPS => return Err(Refusal::Bound(format!("{sub} substeps; at most {MAX_SUBSTEPS}"))),
Some(sub) => {
if dt / sub as f64 * norm_max > MAX_STEP_NORM {
return Err(Refusal::Bound(format!(
"{sub} substeps: a step of {} ns against the generator's norm, up to {norm_max} per ns, is {} > {MAX_STEP_NORM}, where the series' bound is not stated; take at least {least} substeps, or leave them to the plan",
dt / sub as f64,
dt / sub as f64 * norm_max
)));
}
sub
}
None => {
let mut sub = ((dt * norm_max / AUTO_STEP_NORM).ceil() as u32).max(1);
while sub < MAX_SUBSTEPS && bound(sub).0 > set.tolerance / 2.0 {
sub = (sub * 2).min(MAX_SUBSTEPS);
}
sub
}
};
if sub > MAX_SUBSTEPS {
return Err(Refusal::Bound(format!("the samples' norm, up to {norm_max} per ns, needs more than {MAX_SUBSTEPS} substeps of dt = {dt} ns")));
}
let (e, r) = bound(sub);
if e + r > set.tolerance {
// The fewest substeps from these that meet it, if any do.
let meets = (sub..=MAX_SUBSTEPS).step_by(((MAX_SUBSTEPS - sub) / 64).max(1) as usize).find(|&k| {
let (e, r) = bound(k);
e + r <= set.tolerance
});
let advice = match meets {
Some(k) => format!("{k} substeps meet it"),
None if r > set.tolerance => format!("rounding alone over {} steps is estimated at {r:.3e}: shorten the schedule or loosen the tolerance", samples * sub as u64),
None => "no number of substeps meets it in double precision: shorten the schedule or loosen the tolerance".to_string(),
};
return Err(Refusal::Bound(format!("{sub} substeps of dt = {dt} ns over {samples} samples bound the error at {e:.3e} with rounding estimated at {r:.3e}, past the tolerance {}; {advice}", set.tolerance)));
}
let steps = samples.saturating_mul(sub as u64);
let work = steps.saturating_mul(ORDER as u64).saturating_mul(cost);
if work > MAX_WORK {
return Err(Refusal::Model(format!("the schedule is {steps} steps on a space of {}, about {work} multiply-adds; the simulator takes at most {MAX_WORK}", m.dim)));
}
Ok(Plan {
qubits: m.qubits.clone(),
levels: m.levels,
dim: m.dim,
samples,
dt_ns: dt,
substeps: sub,
step_ns: dt / sub as f64,
steps,
order: ORDER,
norm_max,
truncation_bound: e,
rounding_estimate: r,
tolerance: set.tolerance,
work,
})
}
/// Runs `s` on `d` as `p` planned, from the ground state.
pub fn run(s: &Schedule, d: &Device, p: &Plan) -> Result<Outcome, Refusal> {
let m = model(s, d)?;
let mut rho = vec![(0.0, 0.0); m.dim * m.dim];
rho[0] = (1.0, 0.0);
integrate(s, &m, p, rho)
}
/// Runs `s` on `d` as `p` planned, from the density matrix `rho0` (row by
/// row, over the plan's qubits, qubit `qubits[0]` the fastest digit).
pub fn run_from(s: &Schedule, d: &Device, p: &Plan, rho0: &[(f64, f64)]) -> Result<Outcome, Refusal> {
let m = model(s, d)?;
if rho0.len() != m.dim * m.dim {
return Err(Refusal::Model(format!("the initial state has {} entries; the space is {} by {}", rho0.len(), m.dim, m.dim)));
}
integrate(s, &m, p, rho0.to_vec())
}
fn integrate(s: &Schedule, m: &Model, p: &Plan, rho0: Vec<(f64, f64)>) -> Result<Outcome, Refusal> {
if p.qubits != m.qubits || p.dim != m.dim || p.samples != s.duration {
return Err(Refusal::Model("the plan is for another schedule or device".into()));
}
let (n, dim) = (m.qubits.len(), m.dim);
let mut rho: Vec<C> = rho0.into_iter().map(|(re, im)| C { re, im }).collect();
let (mut term, mut tmp, mut acc) = (vec![C::default(); dim * dim], vec![C::default(); dim * dim], vec![C::default(); dim * dim]);
let mut caps: Vec<Capture> = Vec::new();
let mut sw = Sweep::new(n);
let stride: Vec<usize> = (0..n).map(|j| m.levels.pow(j as u32)).collect();
let h = p.step_ns;
let mut entries: Vec<(usize, usize, C)> = Vec::with_capacity(dim * (1 + 2 * n));
let mut by_time: Vec<(u64, usize, u32)> = m.captures.iter().map(|&(i, q, t)| (t, i, q)).collect();
by_time.sort();
let mut next_cap = 0;
let mut joint: Option<Vec<f64>> = None;
let same_time = by_time.first().is_some_and(|f| by_time.iter().all(|c| c.0 == f.0));
for k in 0..s.duration {
while next_cap < by_time.len() && by_time[next_cap].0 == k {
let (t, i, q) = by_time[next_cap];
let j = m.qubits.iter().position(|&x| x == q).unwrap_or(0);
let (mut p1, mut p2) = (0.0, 0.0);
for b in 0..dim {
let lv = m.digit[b * n + j];
if lv >= 1 {
p1 += rho[b * dim + b].re;
}
if lv == 2 {
p2 += rho[b * dim + b].re;
}
}
caps.push(Capture { instruction: i, qubit: q, start: t, p1, p2 });
next_cap += 1;
}
if same_time && joint.is_none() && by_time.first().is_some_and(|f| f.0 == k) {
joint = Some(read_out(m, &rho, &by_time.iter().map(|c| c.2).collect::<Vec<_>>()));
}
sw.at(m, k);
// The generator over this sample, in each qubit's tone's frame.
entries.clear();
let mut det = vec![0.0; n];
for j in 0..n {
let mut z = C::default();
for &pi in &sw.active[j] {
let pl = &m.plays[j][pi];
let (re, im) = s.waveforms[pl.waveform].samples[(k - pl.start) as usize];
let theta = pl.phase + TAU * fract(pl.base + pl.slope * (k - pl.start) as f64);
let (sn, cs) = repro::sin_cos(theta);
z = z.add(C { re, im }.mul(C { re: cs, im: sn }));
det[j] = pl.detuning;
}
if z != C::default() {
let half = m.omega[j] / 2.0;
let zt = z.scale(half);
let zc = C { re: zt.re, im: -zt.im };
for i in 0..dim {
let lv = m.digit[i * n + j];
if lv >= 1 {
let r = (lv as f64).sqrt();
entries.push((i - stride[j], i, zt.scale(r)));
entries.push((i, i - stride[j], zc.scale(r)));
}
}
}
}
for i in 0..dim {
let mut x = m.static_diag[i];
for j in 0..n {
x -= det[j] * m.digit[i * n + j] as f64;
}
if x != 0.0 {
entries.push((i, i, C { re: x, im: 0.0 }));
}
}
for _ in 0..p.substeps {
acc.copy_from_slice(&rho);
term.copy_from_slice(&rho);
for jx in 1..=ORDER {
lindblad(m, &entries, &term, &mut tmp);
let c = h / jx as f64;
for (t, x) in term.iter_mut().zip(&tmp) {
*t = x.scale(c);
}
for (a, t) in acc.iter_mut().zip(&term) {
*a = a.add(*t);
}
}
rho.copy_from_slice(&acc);
}
// Back from the tones' frames to the qubits'.
if det.iter().any(|&x| x != 0.0) {
let ph: Vec<C> = (0..dim)
.map(|i| {
let mut x = 0.0;
for j in 0..n {
x -= s.dt_ns * det[j] * m.digit[i * n + j] as f64;
}
let (sn, cs) = repro::sin_cos(x);
C { re: cs, im: sn }
})
.collect();
for a in 0..dim {
for b in 0..dim {
let w = ph[a].mul(C { re: ph[b].re, im: -ph[b].im });
rho[a * dim + b] = rho[a * dim + b].mul(w);
}
}
}
}
if same_time && joint.is_none() && !by_time.is_empty() {
joint = Some(read_out(m, &rho, &by_time.iter().map(|c| c.2).collect::<Vec<_>>()));
}
let all = read_out(m, &rho, &m.qubits);
let leakage: Vec<f64> = (0..n).map(|j| (0..dim).filter(|&b| m.digit[b * n + j] == 2).map(|b| rho[b * dim + b].re).sum::<f64>()).collect();
let trace: f64 = (0..dim).map(|b| rho[b * dim + b].re).sum();
let distinct = {
let mut q: Vec<u32> = by_time.iter().map(|c| c.2).collect();
q.sort_unstable();
q.dedup();
q.len() == by_time.len()
};
Ok(Outcome {
plan: p.clone(),
rho: rho.iter().map(|c| (c.re, c.im)).collect(),
populations: all,
leakage,
captures: caps,
joint: if distinct { joint } else { None },
trace,
})
}
/// The distribution of reading `qs` (each a level above 0 read as 1): index
/// bit j is `qs[j]`.
fn read_out(m: &Model, rho: &[C], qs: &[u32]) -> Vec<f64> {
let (n, dim) = (m.qubits.len(), m.dim);
let pos: Vec<usize> = qs.iter().map(|q| m.qubits.iter().position(|x| x == q).unwrap_or(0)).collect();
let mut out = vec![0.0; 1 << qs.len()];
for b in 0..dim {
let mut x = 0usize;
for (j, &jq) in pos.iter().enumerate() {
if m.digit[b * n + jq] >= 1 {
x |= 1 << j;
}
}
out[x] += rho[b * dim + b].re;
}
out
}
/// out = 𝓛(rho): −i[H, ρ] + Σ_k L_k ρ L_k† − ½{L_k†L_k, ρ}, H given by its
/// entries.
fn lindblad(m: &Model, h: &[(usize, usize, C)], rho: &[C], out: &mut [C]) {
let d = m.dim;
for (o, (r, g)) in out.iter_mut().zip(rho.iter().zip(&m.damp)) {
*o = r.scale(*g);
}
for &(r, c, v) in h {
// −i·v, and +i·v.
let mi = C { re: v.im, im: -v.re };
let pi = C { re: -v.im, im: v.re };
for j in 0..d {
out[r * d + j] = out[r * d + j].add(mi.mul(rho[c * d + j]));
}
for i in 0..d {
out[i * d + c] = out[i * d + c].add(rho[i * d + r].mul(pi));
}
}
for e in &m.jumps {
for &(r1, c1, v1) in e {
for &(r2, c2, v2) in e {
out[r1 * d + r2] = out[r1 * d + r2].add(rho[c1 * d + c2].scale(v1 * v2));
}
}
}
}
#[cfg(test)]
mod tests;

View file

@ -0,0 +1,198 @@
use super::*;
use crate::qasm::pulse::{Options, read};
const CAL: &str = "OPENQASM 3.0;\ndefcalgrammar \"openpulse\";\ncal {\n extern port d0;\n extern port a0;\n extern port d1;\n extern port a1;\n frame q0 = newframe(d0, 5.0e9, 0);\n frame c0 = newframe(a0, 5.0e9, 0);\n frame q1 = newframe(d1, 5.1e9, 0);\n frame c1 = newframe(a1, 5.1e9, 0);\n}\n";
fn sched(body: &str) -> Schedule {
let src = format!("{CAL}{body}");
read(&src, &Options::default()).unwrap_or_else(|d| panic!("{d}\n{src}")).schedule
}
fn device(t1: Option<f64>, t2: Option<f64>) -> Device {
Device::new(2).with_qubit(0, Qubit::new(5.0e9, 50e6).with_times(t1, t2)).with_qubit(1, Qubit::new(5.1e9, 40e6).with_times(t1, t2))
}
fn go(s: &Schedule, d: &Device) -> Outcome {
let p = plan(s, d, &Settings::default()).unwrap_or_else(|e| panic!("{e}"));
run(s, d, &p).unwrap_or_else(|e| panic!("{e}"))
}
/// `got` within the plan's stated error of the closed form `want`.
fn within(o: &Outcome, got: f64, want: f64, what: &str) {
let room = o.plan.truncation_bound + o.plan.rounding_estimate + 4.0 * f64::EPSILON;
assert!((got - want).abs() <= room, "{what}: {got} vs the closed form {want}, off by {:e}, past the stated {room:e}", (got - want).abs());
assert!(o.plan.truncation_bound + o.plan.rounding_estimate <= o.plan.tolerance, "{what}: the plan states its error within its tolerance");
}
const OMEGA: f64 = TAU * 50e6 * 1e-9;
#[test]
fn a_resonant_drive_turns_the_qubit_as_rabi_says() {
for n in [1u64, 5, 17, 40, 101, 333] {
let s = sched(&format!("cal {{ play(q0, constant(0.3, {n}dt)); }}"));
let o = go(&s, &device(None, None));
let theta = OMEGA * 0.3 * n as f64 * 0.5;
within(&o, o.populations[1], (theta / 2.0).sin().powi(2), &format!("P(1) after {n} samples"));
// About +X: ρ01 = (i/2) sin θ.
within(&o, o.rho[1].1, theta.sin() / 2.0, &format!("Im ρ01 after {n}"));
within(&o, o.rho[1].0, 0.0, &format!("Re ρ01 after {n}"));
assert_eq!(o.plan.qubits, [0]);
}
}
#[test]
fn a_quarter_turn_of_phase_turns_the_axis_to_minus_y() {
let s = sched("cal { shift_phase(q0, pi/2); play(q0, constant(0.3, 40dt)); }");
let o = go(&s, &device(None, None));
let theta = OMEGA * 0.3 * 40.0 * 0.5;
within(&o, o.rho[1].0, -theta.sin() / 2.0, "Re ρ01");
within(&o, o.rho[1].1, 0.0, "Im ρ01");
}
#[test]
fn a_detuned_drive_turns_less_and_faster() {
for (mhz, n) in [(20.0, 80u64), (-35.0, 61), (5.0, 400)] {
let s = sched(&format!("cal {{ set_frequency(q0, {}); play(q0, constant(0.3, {n}dt)); }}", 5.0e9 + mhz * 1e6));
let o = go(&s, &device(None, None));
let (r, d) = (OMEGA * 0.3, TAU * mhz * 1e6 * 1e-9);
let g = (r * r + d * d).sqrt();
let t = n as f64 * 0.5;
within(&o, o.populations[1], r * r / (g * g) * (g * t / 2.0).sin().powi(2), &format!("P(1) at {mhz} MHz after {n} samples"));
}
}
#[test]
fn relaxation_and_dephasing_decay_as_t1_and_t2_say() {
// T1 alone: |1⟩ decays as e^{−t/T1}.
let s = sched("cal { delay[400dt] q0; }");
let d = device(Some(50.0), None);
let p = plan(&s, &d, &Settings::default()).unwrap();
let o = run_from(&s, &d, &p, &[(0.0, 0.0), (0.0, 0.0), (0.0, 0.0), (1.0, 0.0)]).unwrap();
within(&o, o.populations[1], (-200.0f64 / 50.0).exp(), "P(1) under T1");
// T1 and T2: from |+⟩, ρ01 = ½e^{−t/T2} and ρ11 = ½e^{−t/T1}.
let plus = [(0.5, 0.0), (0.5, 0.0), (0.5, 0.0), (0.5, 0.0)];
for (t1, t2) in [(Some(100.0), Some(60.0)), (None, Some(40.0)), (Some(80.0), Some(160.0))] {
let d = device(t1, t2);
let p = plan(&s, &d, &Settings::default()).unwrap();
let o = run_from(&s, &d, &p, &plus).unwrap();
let t = 200.0f64;
within(&o, o.rho[1].0, 0.5 * (-t / t2.unwrap()).exp(), &format!("Re ρ01 under T1 {t1:?}, T2 {t2:?}"));
within(&o, o.rho[1].1, 0.0, "Im ρ01");
within(&o, o.rho[3].0, 0.5 * t1.map_or(1.0, |t1| (-t / t1).exp()), "ρ11");
within(&o, o.trace, 1.0, "the trace");
}
}
#[test]
fn the_same_schedule_gives_the_same_bits() {
let src = "cal { play(q0, drag(0.4 + 0.1im, 64dt, 16dt, 0.7)); shift_phase(q0, 0.3); set_frequency(q1, 5.08e9); play(q1, gaussian_square(0.2, 120dt, 60dt, 10dt)); barrier q0, q1, c0, c1; capture(c0, 20dt); capture(c1, 20dt); }";
let s = sched(src);
let d = Device::new(3)
.with_qubit(0, Qubit::new(5.0e9, 50e6).with_anharmonicity(-300e6).with_times(Some(80_000.0), Some(60_000.0)))
.with_qubit(1, Qubit::new(5.1e9, 40e6).with_anharmonicity(-280e6).with_times(Some(70_000.0), None))
.with_coupling(0, 1, 120e3);
let a = go(&s, &d);
let b = go(&sched(src), &d);
assert_eq!(a.density_hash(), b.density_hash());
assert_eq!(a.rho.iter().map(|x| (x.0.to_bits(), x.1.to_bits())).collect::<Vec<_>>(), b.rho.iter().map(|x| (x.0.to_bits(), x.1.to_bits())).collect::<Vec<_>>());
assert_eq!(a.plan.dim, 9);
// The digest every platform must give: native on each processor, and
// wasm32 (the browser's engine, held in wai-web).
assert_eq!(a.density_hash(), PINNED, "the bits differ from the pinned ones: {}", a.density_hash());
}
/// The density matrix's digest for `the_same_schedule_gives_the_same_bits`.
pub(crate) const PINNED: &str = "a418fc62118910d93836ea508e86997b68740a290950b67ade7577492a9e0982";
#[test]
fn simultaneous_captures_give_the_joint_reading() {
// A π pulse on qubit 0 only: the pair reads 01 (bit 0 is the first capture).
let pi_len = (1.0f64 / (2.0 * 50e6 * 0.5 * 0.5e-9)).round() as u64; // A = 0.5
let s = sched(&format!("cal {{ play(q0, constant(0.5, {pi_len}dt)); barrier q0, q1, c0, c1; capture(c0, 10dt); capture(c1, 10dt); }}"));
let o = go(&s, &device(None, None));
let j = o.joint.clone().unwrap();
within(&o, j[1], 1.0, "P(01)");
within(&o, j[0] + j[2] + j[3], 0.0, "the rest");
assert_eq!(o.captures.iter().map(|c| (c.qubit, c.start)).collect::<Vec<_>>(), [(0, pi_len), (1, pi_len)]);
within(&o, o.captures[0].p1, 1.0, "the first capture");
// Captures at different samples: each its own, no joint reading.
let s = sched(&format!("cal {{ play(q0, constant(0.5, {pi_len}dt)); barrier q0, c0; capture(c0, 10dt); capture(c1, 10dt); }}"));
let o = go(&s, &device(None, None));
assert!(o.joint.is_none());
assert_eq!(o.captures.len(), 2);
}
#[test]
fn three_levels_show_leakage_and_keep_the_trace() {
let s = sched("cal { play(q0, gaussian(0.9, 16dt, 3dt)); }");
let two = go(&s, &device(None, None));
assert!(two.leakage[0] == 0.0);
let d = Device::new(3).with_qubit(0, Qubit::new(5.0e9, 120e6).with_anharmonicity(-200e6));
let o = go(&s, &d);
assert!(o.leakage[0] > 1e-4, "a short strong pulse leaks: {}", o.leakage[0]);
within(&o, o.trace, 1.0, "the trace");
within(&o, o.populations.iter().sum::<f64>(), 1.0, "the populations, |2⟩ read as 1");
}
#[test]
fn the_bound_is_stated_and_a_step_past_it_is_refused() {
let s = sched("cal { play(q0, constant(0.9, 200dt)); }");
let d = Device::new(2).with_qubit(0, Qubit::new(5.0e9, 900e6));
let auto = plan(&s, &d, &Settings::default()).unwrap();
assert!(auto.step_ns * auto.norm_max <= 0.5 + 1e-12 && auto.substeps > 1, "{auto:?}");
let fine = plan(&s, &d, &Settings::new(DEFAULT_TOLERANCE, Some(auto.substeps * 4))).unwrap();
assert!(fine.truncation_bound < auto.truncation_bound);
// A step longer than 1/λ is refused, naming the steps that would do.
match plan(&s, &d, &Settings::new(DEFAULT_TOLERANCE, Some(1))) {
Err(Refusal::Bound(m)) => assert!(m.contains("take at least"), "{m}"),
other => panic!("{other:?}"),
}
// A tolerance the series cannot meet at this step, and one rounding alone exceeds.
let tight = Settings::new(1e-16, None);
assert!(matches!(plan(&s, &d, &tight), Err(Refusal::Bound(_))));
match plan(&s, &d, &Settings::new(1e-13, Some(auto.substeps))) {
Err(Refusal::Bound(m)) => assert!(m.contains("past the tolerance"), "{m}"),
other => panic!("{other:?}"),
}
assert!(matches!(plan(&s, &d, &Settings::new(2.0, None)), Err(Refusal::Model(_))));
}
#[test]
fn what_the_model_does_not_hold_is_refused() {
let d = device(None, None);
let model = |body: &str, dev: &Device| match plan(&sched(body), dev, &Settings::default()) {
Err(Refusal::Model(m)) => m,
other => panic!("{body}: {other:?}"),
};
// Two tones on one qubit at once.
assert!(model("cal { frame g = newframe(d0, 5.01e9, 0); play(q0, constant(0.1, 10dt)); play(g, constant(0.1, 10dt)); }", &d).contains("one tone"));
// A play after the qubit's capture, and a second capture.
assert!(model("cal { capture(c0, 10dt); delay[20dt] q0; play(q0, constant(0.1, 10dt)); }", &d).contains("after its capture"));
assert!(model("cal { capture(c0, 10dt); capture(c0, 10dt); }", &d).contains("second capture"));
// Far from resonance, more qubits than the space holds, and T2 past 2·T1.
assert!(model("cal { set_frequency(q0, 6.5e9); play(q0, constant(0.1, 10dt)); }", &d).contains("rotating-wave"));
let wide = (0..5).map(|q| format!("extern port d{}; frame w{q} = newframe(d{}, 5e9, 0); play(w{q}, constant(0.1, 1dt));", q + 2, q + 2)).collect::<String>();
let mut dev = device(None, None);
for q in 2..7 {
dev = dev.with_qubit(q, Qubit::new(5e9, 1e6));
}
assert!(model(&format!("cal {{ {wide} }}"), &dev).contains("holds at most"));
assert!(model("cal { delay[10dt] q0; }", &device(Some(10.0), Some(30.0))).contains("at most 2·T1"));
assert!(model("cal { delay[10dt] q0; }", &Device::new(2)).contains("no parameters"));
}
#[test]
fn a_detuned_play_split_in_two_is_the_same_play() {
// The carrier runs on across plays: two plays back to back on a frame
// 20 MHz off resonance are the one play they make, closed form and all.
let one = sched("cal { set_frequency(q0, 5.02e9); play(q0, constant(0.3, 81dt)); }");
let two = sched("cal { set_frequency(q0, 5.02e9); play(q0, constant(0.3, 40dt)); play(q0, constant(0.3, 41dt)); }");
let (a, b) = (go(&one, &device(None, None)), go(&two, &device(None, None)));
let room = a.plan.truncation_bound + b.plan.truncation_bound + a.plan.rounding_estimate + b.plan.rounding_estimate + 1e-14;
for (x, y) in a.rho.iter().zip(&b.rho) {
assert!((x.0 - y.0).abs() <= room && (x.1 - y.1).abs() <= room, "{x:?} vs {y:?}");
}
let (r, d) = (OMEGA * 0.3, TAU * 20e6 * 1e-9);
let g = (r * r + d * d).sqrt();
within(&b, b.populations[1], r * r / (g * g) * (g * 40.5 / 2.0).sin().powi(2), "P(1) over the two plays");
}

View file

@ -4,19 +4,19 @@
"placeholder": "__WAI_QUANTUM_VIEW__",
"limit_bytes": 1048576,
"bytes": {
"page": 803203,
"page": 804708,
"script": 94216,
"styles": 49303,
"styles": 50804,
"engine_wasm": 193526,
"engine_wasm_base64": 258036,
"engine_js": 401198
"engine_js": 401202
},
"sha256": {
"page": "3bb78cf8bef7f6851314f97b59921c4902c06619006d86dfced4e941f3d5f608",
"page": "9b412e847dde188381d790b7d50aaaee11985ea3c5ba61a2dcac46d0fe1ba170",
"script": "136d99215a1a8cc7a1302a107a222823272fe7371949ffa2816cbb4f9599776a",
"styles": "3374b613ce5f62a37f5c055a22c3e58e9d904422171f974c4f4538fd1c9d0eb3",
"engine_wasm": "68390ba3ee64bd37096708d73d807ef9c565ba0891f032c26222bba9c42b48db",
"engine_js": "434c0a2354df4d7b51e068186bab8ad9df8612b35ab97cc5b9290900608ee60c"
"styles": "11ebcd4416c68e9eaf1156ca6f7c92b9a96744895ed13c377eba98fdd8dca9a6",
"engine_wasm": "8373ee77c3603419a70387e01ad7f906d4d93b4345b5674a7785dfb37e573c0a",
"engine_js": "d5ecf0d52b1295077c60af6411fdba0757d9f3b6861e0b51bc30b97d21307f5c"
},
"sources": {
"wai/quantum-studio/build-views.mjs": "da5b161829a990b3d1bb4c1b3ec7c8f254ff7b5078476156e504ae322bc4f010",
@ -56,7 +56,7 @@
"wai/quantum-studio/src/estimate-view.js": "40cb4bd738afb628dd686cdad44813d9d09910fbc60905098852dcca6cfe1b27",
"wai/quantum-studio/src/i18n.js": "f661474772b05ef6614f2f6096e85d94cd479c4fcd9448f34e8d312a2fd43498",
"wai/quantum-studio/src/result-parts.js": "c0c0bceae031a45e6e9862533706eda4cf119b6f65456237c2572ed534f9513e",
"wai/quantum-studio/src/studio.css": "3c14ecf5e5f70e78588bc0e8a0933495197711966fc427578747e4115aa60037",
"wai/quantum-studio/src/studio.css": "8891f2a1b71e1ada344ee1dc41939ba9cd41232f25ab2703d05394c141855997",
"wai/quantum-studio/src/tokens.css": "492fbaf8a6cc5b1f7e89346336af3e0a1930a0b48a23731d80d86c128a9032c3",
"wai/quantum-studio/src/verdict-view.js": "aec18c93c6f502657ffe2981ca4d9ae0956e1277638d1a9bb1b0846ac8e80160",
"wai/quantum-studio/src/views/check.js": "2edae7b446e72a49293478ebcbfe21792b74762dcfb097fb9e75ec3a4feb8d9a",

File diff suppressed because one or more lines are too long

View file

@ -125,6 +125,28 @@ impl QuantumEngine {
}
}
/// A pulse call ([`wai::engine::pulse::call`]): `schedule` (a pulse
/// program's schedule on frames: every instruction at its sample, the
/// waveforms' samples and the gate calls, which the Studio's pulse view
/// draws) or `check` (a pulse job checked by running it again), its
/// arguments as JSON; the answer sealed with the page's key in a job's
/// form, so `engine_verify` checks it; or `{"error"}` with why it was
/// refused. A pulse run is a job: `run` with `"method": "pulse"`.
pub fn pulse(&self, op: &str, args: &str) -> String {
let req = match serde_json::from_str::<Value>(args) {
Ok(r) => r,
Err(e) => return error_json(format!("{op}: the arguments are not JSON: {e}")),
};
match wai::engine::pulse::call(op, &req) {
Ok(result) => wai::engine::pulse::seal(&req, result, &self.key).to_string(),
Err(e) => {
let mut v = refusal::refusal_json(&e);
v["error"] = json!(e);
v.to_string()
}
}
}
/// Each method, its accuracy, what it takes, and the most qubits it holds
/// in a job's memory here.
pub fn methods(&self) -> String {
@ -800,6 +822,11 @@ mod tests {
/// The compile sealed before either, whose bytes hashed to 14db506e…, is
/// kept in wai-quantum's `src/engine/testdata/compile-legacy.json` and still
/// verifies.
/// A pulse program, and the digest of its run's final density matrix:
/// the same bits natively and in wasm32 (the engine pins it too).
pub(super) const PULSE_XM: &str = "OPENQASM 3.0;\ndefcalgrammar \"openpulse\";\ncal {\n extern port d0;\n extern port m0;\n extern port a0;\n frame q0 = newframe(d0, 5.0e9, 0);\n frame r0 = newframe(m0, 7.1e9, 0);\n frame c0 = newframe(a0, 7.1e9, 0);\n}\ndefcal x $0 { play(q0, gaussian(0.4, 64dt, 16dt)); }\ndefcal measure $0 -> bit { play(r0, constant(0.1, 200dt)); return capture_v2(c0, 200dt); }\nbit[1] c;\nx $0;\nc[0] = measure $0;\n";
pub(super) const PULSE_XM_DENSITY: &str = "03c0c48a8a3568fdd86e792633c5b4ea98eca3d09dc40d028bb50e13c489828b";
pub(super) const COMPILE_DIGEST: &str = "ac94813324e9bf7ab83b4bf0290e5abf25bef987826311f0632c2701ebbd045a";
/// A compile in the page: Clifford+T within the budget, every pass
@ -1185,6 +1212,23 @@ mod tests {
assert_eq!(job["result"]["mitigation"]["method"], "vd", "{job}");
}
/// A pulse program's schedule draws in the page, its run is a job of
/// the method pulse whose bits are the engine's, and the run checks.
#[test]
fn a_pulse_program_draws_runs_and_checks_in_the_page() {
let e = QuantumEngine::new(&[4u8; 32], 512).unwrap();
let s: Value = serde_json::from_str(&e.pulse("schedule", &json!({ "qasm": PULSE_XM }).to_string())).unwrap();
assert_eq!((s["result"]["duration"].as_u64(), s["result"]["frames"].as_array().map(Vec::len)), (Some(264), Some(3)), "{s}");
let v: Value = serde_json::from_str(&engine_verify(&s.to_string())).unwrap();
assert_eq!(v["valid"], true, "{v}");
let job: Value = serde_json::from_str(&e.run(&json!({ "qasm": PULSE_XM, "method": "pulse", "shots": 100 }).to_string())).unwrap();
assert_eq!((job["method"].as_str(), job["result"]["pulse"]["density_hash"].as_str()), (Some("pulse"), Some(PULSE_XM_DENSITY)), "{job}");
let c: Value = serde_json::from_str(&e.pulse("check", &job.to_string())).unwrap();
assert_eq!(c["result"]["verified"], true, "{c}");
let bad: Value = serde_json::from_str(&e.pulse("schedule", r#"{"qasm": "OPENQASM 3.0;\ncal { play(f, constant(0.1, 1dt)); }\n"}"#)).unwrap();
assert_eq!(bad["code"], "Q102", "{bad}");
}
#[test]
fn the_classical_shortcuts_answer_as_json() {
let s: Value = serde_json::from_str(&engine_shortcuts("qubit[40] q;\nh q[0];\ncx q[0], q[39];\n", false)).unwrap();
@ -1243,6 +1287,17 @@ mod wasm_tests {
assert_eq!(c["result"]["residual"]["logical"], false, "{c}");
}
/// The same pulse run as the native test's, to the same bits: the
/// waveforms' exponentials, the carrier's sines and the integrator.
#[wasm_bindgen_test]
fn a_pulse_run_is_the_native_bits_in_wasm() {
let e = QuantumEngine::new(&[9u8; 32], 512).unwrap();
let job = run(&e, json!({ "qasm": super::tests::PULSE_XM, "method": "pulse", "shots": 100 }));
assert_eq!(job["result"]["pulse"]["density_hash"].as_str(), Some(super::tests::PULSE_XM_DENSITY), "{job}");
let v: Value = serde_json::from_str(&engine_verify(&job.to_string())).unwrap();
assert_eq!(v["valid"], true, "{v}");
}
#[wasm_bindgen_test]
fn a_compile_runs_in_wasm_to_the_native_bytes() {
let e = QuantumEngine::new(&[9u8; 32], 512).unwrap();