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