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]>
145 lines
7.6 KiB
Markdown
145 lines
7.6 KiB
Markdown
# 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.
|