open-standards/wai/wai-quantum/docs/pulse.md
Transaction Science 13151abfd7 wai-quantum docs: the pulse page, the READMEs and the feature matrix
docs/pulse.md gives a pulse program, its schedule, its run as a job, its
check and a refusal, all run by the docs checker against the command line
(the receipt a block writes verified), and states the reader's constructs,
the model, the integrator and its bound, what a receipt binds, and the
options. The engine's and the command line's READMEs (with their Simplified
Chinese translations), the service's README, the docs index and the
OpenQASM 3 feature matrix name the pulse layer; the agent docs are
regenerated from the matrix. The reader's module documentation states the
qubit timeline a gate waits for.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 12:05:14 -04:00

145 lines
7.6 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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.