From a14daafa70b1b01c059186cee20500b8f9594236 Mon Sep 17 00:00:00 2001 From: Transaction Science Date: Fri, 30 Oct 2026 09:57:10 -0400 Subject: [PATCH 01/10] wai-quantum: OpenPulse read into a schedule on frames qasm::pulse reads a pulse program's calibrations (cal and defcal on physical qubits) and runs them in program order into a schedule: ports, frames with their own times, the waveform templates (constant, gaussian, sech, gaussian_square, drag, sine) and explicit samples, play, delay, shift_phase, set_phase, shift_frequency, set_frequency, captures, barriers, and each gate call with the samples it spans. A gate starts when its qubits and frames are free; a frame's carrier runs on continuously across a frequency change, and each play records the carrier's phase at its first sample. Durations are whole samples of dt or refused. What it does not take is refused with a code and its place: Q453 to Q462 join qasm::diag after Q452, renumbering nothing, and the table in docs/qasm-diagnostics.md holds them. Co-Authored-By: Claude Opus 5.5 --- wai/wai-quantum/docs/qasm-diagnostics.md | 10 + wai/wai-quantum/src/qasm.rs | 3 + wai/wai-quantum/src/qasm/diag.rs | 10 + wai/wai-quantum/src/qasm/parse.rs | 7 + wai/wai-quantum/src/qasm/pulse.rs | 1787 ++++++++++++++++++++++ wai/wai-quantum/src/qasm/pulse/tests.rs | 213 +++ 6 files changed, 2030 insertions(+) create mode 100644 wai/wai-quantum/src/qasm/pulse.rs create mode 100644 wai/wai-quantum/src/qasm/pulse/tests.rs diff --git a/wai/wai-quantum/docs/qasm-diagnostics.md b/wai/wai-quantum/docs/qasm-diagnostics.md index 1a5df611f..063205fea 100644 --- a/wai/wai-quantum/docs/qasm-diagnostics.md +++ b/wai/wai-quantum/docs/qasm-diagnostics.md @@ -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. | diff --git a/wai/wai-quantum/src/qasm.rs b/wai/wai-quantum/src/qasm.rs index b274aed27..deb47dc3a 100644 --- a/wai/wai-quantum/src/qasm.rs +++ b/wai/wai-quantum/src/qasm.rs @@ -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}; diff --git a/wai/wai-quantum/src/qasm/diag.rs b/wai/wai-quantum/src/qasm/diag.rs index d8418870b..ebf0b9a77 100644 --- a/wai/wai-quantum/src/qasm/diag.rs +++ b/wai/wai-quantum/src/qasm/diag.rs @@ -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."; diff --git a/wai/wai-quantum/src/qasm/parse.rs b/wai/wai-quantum/src/qasm/parse.rs index 5c1f79967..bd144a8e9 100644 --- a/wai/wai-quantum/src/qasm/parse.rs +++ b/wai/wai-quantum/src/qasm/parse.rs @@ -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) } diff --git a/wai/wai-quantum/src/qasm/pulse.rs b/wai/wai-quantum/src/qasm/pulse.rs new file mode 100644 index 000000000..402f2eb4c --- /dev/null +++ b/wai/wai-quantum/src/qasm/pulse.rs @@ -0,0 +1,1787 @@ +//! OpenPulse, the calibration grammar of OpenQASM 3, read into a schedule on +//! frames: what the pulse simulator integrates ([`crate::quantum_pulse`]). +//! +//! A pulse program names physical qubits (`$0`) and calibrates each gate it +//! calls with a `defcal`; its `cal` blocks declare ports, frames, waveforms +//! and values, and may play on frames themselves. [`read`] runs the program +//! in order, each calibration's body at its call, into a [`Schedule`]: every +//! play, delay, phase and frequency change, capture and barrier on its frame +//! at its sample, the waveforms as their samples, and each gate call with +//! the samples it spans. The circuit reader ([`super::read`]) keeps the same +//! blocks verbatim and runs none of them (Q450, Q451); this reader runs them +//! (Q453) and refuses, with a code and its place, what it does not take +//! (Q454 to Q461). +//! +//! The model: +//! +//! - **Time** is counted in samples of the sample period `dt` +//! ([`Options::dt_ns`]): `160dt` is 160 samples, and `80ns` is 80 ns over +//! `dt`, refused unless a whole number of samples (Q458). +//! - **A frame** is a port, a frequency and a phase, with a time of its own, +//! which starts at 0. A play starts at its frame's time and moves it on by +//! the waveform's length; `delay[d] f` moves `f` on by `d`; `barrier f, g` +//! (`barrier;`: every frame in scope) moves each to the latest of them. A +//! gate's calibration starts its frames together: every frame its body +//! names is moved to the latest of them before the body runs. At the top +//! level, `barrier $0, $1` and `delay[d] $0` act on every frame on those +//! qubits' ports (`barrier;` on every frame). +//! - **The carrier** of a frame of frequency f and phase φ at time t is +//! e^{i(2πft + φ)}. Its phase runs on continuously when the frequency +//! changes (`set_frequency`, `shift_frequency`); `set_phase` and +//! `shift_phase` move φ alone. Each play records the frame's frequency, its +//! phase and the carrier's own phase (in cycles) at the play's first sample, +//! so the carrier is known at every instant of it. +//! - **Ports** reach a qubit by their names: `d3` drives qubit 3, `m3` is its +//! measurement port and `a3` its acquisition port; [`Options::ports`] maps +//! other names. A capture is on a frame of an acquisition port and reads +//! that qubit; a play on a measurement port is scheduled and timed, its +//! effect on the readout not modelled (Q462). +//! - **Waveforms**: `constant(amp, l)`, `gaussian(amp, l, σ)`, +//! `sech(amp, l, σ)`, `gaussian_square(amp, l, width, σ)`, +//! `drag(amp, l, σ, β)`, `sine(amp, l, frequency, phase)` and explicit +//! samples `{s0, s1, …}`, `amp` and each sample real or complex. A +//! template's sample k is taken at t = k − (l − 1)/2 (in samples, centred, +//! not lifted): the gaussian is amp·exp(−t²/2σ²), DRAG is +//! amp·g(t)·(1 − iβt/σ²) (β in samples), the gaussian-square is flat for +//! |t| ≤ width/2 with gaussian edges of σ beyond, sech is amp·sech(t/σ), +//! and sine is amp·sin(2π·frequency·k·dt + phase). The exponential and +//! the sine are the crate's own, so every sample is the same bits on every +//! platform. +//! +//! What a calibration may hold: `extern port p;` (and `port p;`), `frame f = +//! newframe(port, frequency, phase);` (in a `cal` block), `waveform w = …;`, +//! constants and values (`const float a = 0.5;`, `duration`, `angle`, +//! `complex`, `int`), `bit b = capture_v2(f, d);`, `play(f, w);`, +//! `delay[d] f, …;`, `shift_phase(f, θ);`, `set_phase(f, θ);`, +//! `shift_frequency(f, Δ);`, `set_frequency(f, ν);`, a capture (`capture`, +//! `capture_v0` to `capture_v4`, each read as a discriminated bit), +//! `barrier f, …;` and, in a measurement's calibration, `return` of its +//! capture. Frequencies are in hertz and phases in radians. + +use super::ast::{E, Expr, Loc, S, Stmt, Type}; +use super::diag::{self, Diagnostic, Span}; +use super::lex::{self, Tok, Token, Verb}; +use super::parse::{Parser, Version}; +use crate::repro; +use serde_json::{Value, json}; +use std::collections::{BTreeMap, BTreeSet}; +use std::f64::consts::{E as EULER, PI, TAU}; + +/// The sample period a reading takes when none is given, ns. +pub const DEFAULT_DT_NS: f64 = 0.5; +/// The longest schedule, in samples. +pub const MAX_SAMPLES: u64 = 1 << 22; +/// The longest waveform, in samples. +pub const MAX_WAVEFORM_SAMPLES: u64 = 1 << 20; +/// The samples a schedule holds over all its distinct waveforms. +pub const MAX_HELD_SAMPLES: usize = 1 << 22; +/// The most instructions a schedule holds. +pub const MAX_INSTRUCTIONS: usize = 1 << 20; +/// The most gate calls a program makes. +pub const MAX_CALLS: usize = 1 << 18; + +/// The waveform templates the reader samples. +pub fn templates() -> &'static [&'static str] { + &["constant", "gaussian", "sech", "gaussian_square", "drag", "sine"] +} + +const CAPTURES: [&str; 6] = ["capture", "capture_v0", "capture_v1", "capture_v2", "capture_v3", "capture_v4"]; +const FRAME_CALLS: [&str; 5] = ["play", "shift_phase", "set_phase", "shift_frequency", "set_frequency"]; +const VALUE_TYPES: [&str; 7] = ["float", "int", "uint", "angle", "duration", "complex", "bool"]; +const CONTROL: [&str; 17] = ["if", "else", "for", "while", "switch", "case", "default", "def", "gate", "box", "break", "continue", "end", "reset", "measure", "input", "output"]; + +/// What a port is for. +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +#[non_exhaustive] +pub enum PortKind { + /// Drives its qubit. + Drive, + /// Plays the readout tone: scheduled and timed, its effect not modelled. + Measure, + /// Captured from: a capture on it reads its qubit. + Acquire, +} + +impl PortKind { + pub fn as_str(self) -> &'static str { + match self { + PortKind::Drive => "drive", + PortKind::Measure => "measure", + PortKind::Acquire => "acquire", + } + } + + /// The kind a name gives (`drive`, `measure`, `acquire`). + pub fn from_name(s: &str) -> Option { + match s { + "drive" => Some(PortKind::Drive), + "measure" => Some(PortKind::Measure), + "acquire" => Some(PortKind::Acquire), + _ => None, + } + } +} + +/// A port: its name as the program declares it, what it is for, and the +/// qubit it reaches. +#[derive(Clone, Debug, PartialEq)] +#[non_exhaustive] +pub struct Port { + pub name: String, + pub kind: PortKind, + pub qubit: u32, +} + +impl Port { + pub fn new(name: impl Into, kind: PortKind, qubit: u32) -> Port { + Port { name: name.into(), kind, qubit } + } + + /// The port a name reaches by convention: `d3`, `m3`, `a3`. + pub fn by_convention(name: &str) -> Option { + let kind = match name.chars().next()? { + 'd' => PortKind::Drive, + 'm' => PortKind::Measure, + 'a' => PortKind::Acquire, + _ => return None, + }; + let digits = &name[1..]; + if digits.is_empty() || !digits.bytes().all(|b| b.is_ascii_digit()) || digits.len() > 6 { + return None; + } + Some(Port::new(name, kind, digits.parse().ok()?)) + } +} + +/// How to read a pulse program: the sample period, and the ports whose names +/// the convention does not give. +#[derive(Clone, Debug, PartialEq)] +#[non_exhaustive] +pub struct Options { + /// The sample period, ns. + pub dt_ns: f64, + /// Ports by name, before the convention (`d3`, `m3`, `a3`). + pub ports: Vec, +} + +impl Default for Options { + fn default() -> Options { + Options { dt_ns: DEFAULT_DT_NS, ports: Vec::new() } + } +} + +impl Options { + pub fn new(dt_ns: f64) -> Options { + Options { dt_ns, ports: Vec::new() } + } + + /// These options with `port` mapped. + pub fn with_port(mut self, port: Port) -> Options { + self.ports.push(port); + self + } +} + +/// A frame as declared: its port and its frequency (Hz) and phase (rad) at +/// time 0. +#[derive(Clone, Debug, PartialEq)] +#[non_exhaustive] +pub struct Frame { + pub name: String, + pub port: usize, + pub frequency_hz: f64, + pub phase: f64, +} + +/// A waveform: the name the program gives it (or its template call as +/// written), and its samples (re, im). +#[derive(Clone, Debug, PartialEq)] +#[non_exhaustive] +pub struct Waveform { + pub name: String, + pub samples: Vec<(f64, f64)>, +} + +/// What an instruction does on its frame. +#[derive(Clone, Debug, PartialEq)] +#[non_exhaustive] +pub enum Op { + /// The waveform played with the frame's phase (rad) and frequency (Hz), + /// and the carrier's own phase in cycles, in [0, 1), at the first sample. + Play { waveform: usize, phase: f64, frequency_hz: f64, carrier: f64 }, + Delay { samples: u64 }, + ShiftPhase(f64), + SetPhase(f64), + ShiftFrequency(f64), + SetFrequency(f64), + /// A capture of `samples`, read as a bit: the program's bit it is + /// written to, by index into [`Schedule::bits`], when it is. + Capture { samples: u64, bit: Option }, + /// The frame aligned with others here. + Barrier, +} + +impl Op { + /// The instruction's name, as the schedule's JSON writes it. + pub fn name(&self) -> &'static str { + match self { + Op::Play { .. } => "play", + Op::Delay { .. } => "delay", + Op::ShiftPhase(_) => "shift_phase", + Op::SetPhase(_) => "set_phase", + Op::ShiftFrequency(_) => "shift_frequency", + Op::SetFrequency(_) => "set_frequency", + Op::Capture { .. } => "capture", + Op::Barrier => "barrier", + } + } +} + +/// One instruction: its frame, its first sample, what it does, and the line +/// it comes from. +#[derive(Clone, Debug, PartialEq)] +#[non_exhaustive] +pub struct Instruction { + pub frame: usize, + pub start: u64, + pub op: Op, + pub line: u32, +} + +/// A gate or a measurement as called, and the samples its calibration spans. +#[derive(Clone, Debug, PartialEq)] +#[non_exhaustive] +pub struct Call { + pub name: String, + pub qubits: Vec, + pub params: Vec, + pub start: u64, + pub end: u64, + pub line: u32, +} + +/// A pulse program read: see the module documentation. +#[derive(Clone, Debug, PartialEq)] +#[non_exhaustive] +pub struct Schedule { + pub dt_ns: f64, + /// The `defcalgrammar` the program declares, if any. + pub grammar: Option, + pub ports: Vec, + pub frames: Vec, + pub waveforms: Vec, + /// In the order the program makes them. + pub instructions: Vec, + pub calls: Vec, + /// The program's bits by name (`c[0]`), in order. + pub bits: Vec, + /// The latest time of any frame, samples. + pub duration: u64, + /// Every `cal`, `defcal` and `defcalgrammar` statement as written: the + /// control program's calibrations. + pub calibrations: Vec, +} + +/// A pulse program read: its schedule, and the reader's notes and warnings. +#[derive(Clone, Debug, PartialEq)] +#[non_exhaustive] +pub struct Reading { + pub schedule: Schedule, + pub diagnostics: Vec, +} + +impl Schedule { + /// The qubit a frame's port reaches. + pub fn frame_qubit(&self, frame: usize) -> u32 { + self.ports[self.frames[frame].port].qubit + } + + /// The kind of a frame's port. + pub fn frame_kind(&self, frame: usize) -> PortKind { + self.ports[self.frames[frame].port].kind + } + + /// The qubits the schedule acts on: those of every frame an instruction + /// is on, in order. + pub fn qubits(&self) -> Vec { + let set: BTreeSet = self.instructions.iter().map(|i| self.frame_qubit(i.frame)).collect(); + set.into_iter().collect() + } + + /// The samples an instruction spans. + pub fn length(&self, i: &Instruction) -> u64 { + match &i.op { + Op::Play { waveform, .. } => self.waveforms[*waveform].samples.len() as u64, + Op::Delay { samples } | Op::Capture { samples, .. } => *samples, + _ => 0, + } + } + + /// The schedule as JSON: the sample period and the length, the ports, + /// the frames, every waveform with its samples, every instruction, the + /// gate calls and the bits. Numbers are as the schedule holds them. + pub fn to_json(&self) -> Value { + let frames: Vec = self + .frames + .iter() + .map(|f| json!({ "name": f.name, "port": self.ports[f.port].name, "qubit": self.ports[f.port].qubit, "kind": self.ports[f.port].kind.as_str(), "frequency_hz": f.frequency_hz, "phase": f.phase })) + .collect(); + let waveforms: Vec = self + .waveforms + .iter() + .map(|w| json!({ "name": w.name, "length": w.samples.len(), "samples": w.samples.iter().map(|&(re, im)| json!([re, im])).collect::>() })) + .collect(); + let instructions: Vec = self + .instructions + .iter() + .map(|i| { + let mut v = json!({ "frame": i.frame, "op": i.op.name(), "start": i.start, "length": self.length(i), "line": i.line }); + match &i.op { + Op::Play { waveform, phase, frequency_hz, carrier } => { + v["waveform"] = json!(waveform); + v["phase"] = json!(phase); + v["frequency_hz"] = json!(frequency_hz); + v["carrier"] = json!(carrier); + } + Op::ShiftPhase(x) | Op::SetPhase(x) | Op::ShiftFrequency(x) | Op::SetFrequency(x) => v["value"] = json!(x), + Op::Capture { bit, .. } => v["bit"] = bit.map_or(Value::Null, |b| json!(self.bits[b])), + Op::Delay { .. } | Op::Barrier => {} + } + v + }) + .collect(); + let calls: Vec = self.calls.iter().map(|c| json!({ "name": c.name, "qubits": c.qubits, "params": c.params, "start": c.start, "end": c.end, "line": c.line })).collect(); + json!({ + "dt_ns": self.dt_ns, + "duration": self.duration, + "duration_ns": self.duration as f64 * self.dt_ns, + "grammar": self.grammar, + "ports": self.ports.iter().map(|p| json!({ "name": p.name, "kind": p.kind.as_str(), "qubit": p.qubit })).collect::>(), + "frames": frames, + "waveforms": waveforms, + "instructions": instructions, + "calls": calls, + "bits": self.bits, + "qubits": self.qubits(), + }) + } +} + +type R = Result; + +/// Reads a pulse program under `opts`: its schedule, or the first thing in +/// it the pulse reader does not take, with its code and its place. +pub fn read(src: &str, opts: &Options) -> Result { + if !(opts.dt_ns.is_finite() && opts.dt_ns > 0.0) { + return Err(Diagnostic::new(diag::PULSE_TIMING, None, format!("the sample period is {} ns; it is a positive number of nanoseconds", opts.dt_ns))); + } + let toks = lex::lex(src)?; + let (stmts, _) = super::parse::parse(src, &toks)?; + let mut l = Lower::new(src, opts); + l.program(&stmts)?; + Ok(l.finish()) +} + +// --------------------------------------------------------------------------- +// Values +// --------------------------------------------------------------------------- + +#[derive(Clone, Debug, PartialEq)] +enum V { + Num(f64), + Cx(f64, f64), + /// A duration, in samples (not yet held to a whole number). + Dur(f64), + Bool(bool), + Wave(usize), + Frame(usize), + Port(usize), + /// A bit, and the capture that writes it, by its instruction. + Bit(Option), +} + +impl V { + fn kind(&self) -> &'static str { + match self { + V::Num(_) => "a number", + V::Cx(..) => "a complex number", + V::Dur(_) => "a duration", + V::Bool(_) => "a boolean", + V::Wave(_) => "a waveform", + V::Frame(_) => "a frame", + V::Port(_) => "a port", + V::Bit(_) => "a bit", + } + } +} + +fn fract(x: f64) -> f64 { + x - x.floor() +} + +fn wrap(x: f64) -> f64 { + x - TAU * (x / TAU).floor() +} + +fn cmul(a: (f64, f64), b: (f64, f64)) -> (f64, f64) { + (a.0 * b.0 - a.1 * b.1, a.0 * b.1 + a.1 * b.0) +} + +// --------------------------------------------------------------------------- +// Calibration statements +// --------------------------------------------------------------------------- + +#[derive(Clone, Debug)] +enum C { + Port(String), + /// `extern` of a template the reader knows: nothing to do. + Known, + Frame(String, Expr), + /// A declared value: its type word, its name, its value. + Let(&'static str, String, Expr), + Bit(String, Option), + Call(Expr), + Delay(Expr, Vec), + Barrier(Vec), + Assign(String, Expr), + Return(Expr), +} + +#[derive(Clone, Debug)] +struct CStmt { + c: C, + at: Loc, +} + +#[derive(Clone, Debug)] +enum Param { + Var(String), + Fixed(f64), +} + +#[derive(Clone, Debug)] +struct Defcal { + name: String, + params: Vec, + qubits: Vec, + body: Vec, + /// The names the body uses as frames. + frames: Vec, +} + +fn loc(t: &Token) -> Loc { + Loc { line: t.line, col: t.col, start: t.start, end: t.end } +} + +fn loc_all(ts: &[Token]) -> Loc { + match (ts.first(), ts.last()) { + (Some(a), Some(b)) => loc(a).to(loc(b)), + _ => Loc::default(), + } +} + +fn is_sym(t: Option<&Token>, s: &str) -> bool { + matches!(t.map(|t| &t.tok), Some(Tok::Sym(x)) if *x == s) +} + +fn word(t: Option<&Token>) -> Option<&str> { + match t.map(|t| &t.tok) { + Some(Tok::Ident(w)) => Some(w.as_str()), + _ => None, + } +} + +fn is_physical(name: &str) -> Option { + name.strip_prefix('$').filter(|d| !d.is_empty() && d.len() <= 6 && d.bytes().all(|b| b.is_ascii_digit())).and_then(|d| d.parse().ok()) +} + +/// The line and column of byte `at` of `src`. +fn line_col(src: &str, at: usize) -> (usize, usize) { + let before = &src[..at]; + let line = before.matches('\n').count() + 1; + let col = before.rsplit('\n').next().map_or(0, |s| s.chars().count()) + 1; + (line, col) +} + +/// The tokens of `src[start..end]`, placed where they stand in `src`. +fn tokens_at(src: &str, start: usize, end: usize) -> R> { + let (line, col) = line_col(src, start); + let shift = |l: usize, c: usize| if l == 1 { (line, c + col - 1) } else { (l + line - 1, c) }; + let toks = lex::lex(&src[start..end]).map_err(|mut d| { + if let Some(s) = d.span.as_mut() { + let (l, c) = shift(s.line as usize, s.column as usize); + s.line = l as u32; + s.column = c as u32; + } + if d.code == diag::BAD_CHARACTER && d.message.contains("'.'") { + return Diagnostic::new(diag::PULSE_STATEMENT, d.span, "a frame's attributes (`f.phase`, `f.frequency`, `f.time`) are not taken: write set_phase, shift_phase, set_frequency and shift_frequency"); + } + d + })?; + Ok(toks + .into_iter() + .map(|t| { + let (l, c) = shift(t.line, t.col); + Token { tok: t.tok, line: l, col: c, start: t.start + start, end: t.end + start } + }) + .collect()) +} + +/// The tokens between the brackets that open at `ts[open]`, and the index of +/// the one that closes them. +fn bracketed(ts: &[Token], open: usize) -> Option { + let mut depth = 0i32; + for (i, t) in ts.iter().enumerate().skip(open) { + match &t.tok { + Tok::Sym("(" | "[" | "{") => depth += 1, + Tok::Sym(")" | "]" | "}") => { + depth -= 1; + if depth == 0 { + return Some(i); + } + } + _ => {} + } + } + None +} + +/// `ts` split at the commas outside brackets. +fn commas(ts: &[Token]) -> Vec<&[Token]> { + let mut out = Vec::new(); + let (mut depth, mut from) = (0i32, 0usize); + for (i, t) in ts.iter().enumerate() { + match &t.tok { + Tok::Sym("(" | "[" | "{") => depth += 1, + Tok::Sym(")" | "]" | "}") => depth -= 1, + Tok::Sym(",") if depth == 0 => { + out.push(&ts[from..i]); + from = i + 1; + } + _ => {} + } + } + if from < ts.len() || !out.is_empty() { + out.push(&ts[from..]); + } + out +} + +struct Lower<'a> { + src: &'a str, + dt_ns: f64, + port_map: &'a [Port], + globals: Vec<(String, V)>, + ports: Vec, + frames: Vec, + state: Vec, + waveforms: Vec, + named: Vec, + keys: BTreeMap, + held: usize, + instructions: Vec, + calls: Vec, + defcals: Vec, + bits: Vec, + registers: Vec<(String, usize, usize)>, + calibrations: Vec, + diagnostics: Vec, + grammar: Option, + warned: BTreeSet, + /// Each physical qubit's time: the end of the last gate on it. + qtime: BTreeMap, +} + +#[derive(Clone, Debug)] +struct FrameState { + time: u64, + phase: f64, + freq: f64, + anchor_k: u64, + anchor_c: f64, +} + +impl<'a> Lower<'a> { + fn new(src: &'a str, opts: &'a Options) -> Lower<'a> { + Lower { + src, + dt_ns: opts.dt_ns, + port_map: &opts.ports, + globals: Vec::new(), + ports: Vec::new(), + frames: Vec::new(), + state: Vec::new(), + waveforms: Vec::new(), + named: Vec::new(), + keys: BTreeMap::new(), + held: 0, + instructions: Vec::new(), + calls: Vec::new(), + defcals: Vec::new(), + bits: Vec::new(), + registers: Vec::new(), + calibrations: Vec::new(), + diagnostics: Vec::new(), + grammar: None, + warned: BTreeSet::new(), + qtime: BTreeMap::new(), + } + } + + fn finish(self) -> Reading { + let duration = self.state.iter().map(|s| s.time).max().unwrap_or(0); + Reading { + schedule: Schedule { + dt_ns: self.dt_ns, + grammar: self.grammar, + ports: self.ports, + frames: self.frames, + waveforms: self.waveforms, + instructions: self.instructions, + calls: self.calls, + bits: self.bits, + duration, + calibrations: self.calibrations, + }, + diagnostics: self.diagnostics, + } + } + + fn span(&self, at: Loc) -> Span { + at.span(self.src) + } + + fn fail(&self, code: &'static str, at: Loc, msg: impl Into) -> R { + Err(Diagnostic::new(code, Some(self.span(at)), msg)) + } + + fn note(&mut self, code: &'static str, at: Loc, msg: impl Into) { + let d = Diagnostic::new(code, Some(self.span(at)), msg); + self.diagnostics.push(d); + } + + // ----------------------------------------------------------------------- + // The program + // ----------------------------------------------------------------------- + + fn program(&mut self, stmts: &[Stmt]) -> R<()> { + for s in stmts { + let at = s.at; + match &s.kind { + S::Version(_) | S::Include(_) | S::Gate { .. } | S::Opaque { .. } => {} + S::Pragma(t) => self.note(diag::UNUSED_PRAGMA, at, format!("`pragma {t}` is kept, not applied")), + S::Verbatim(Verb::Defcalgrammar, text) => { + let g = text.split('"').nth(1).unwrap_or("").to_string(); + if g != "openpulse" { + return self.fail(diag::PULSE_GRAMMAR, at, format!("`defcalgrammar \"{g}\"`: the pulse reader reads the calibration grammar \"openpulse\" alone")); + } + self.grammar = Some(g); + self.calibrations.push(text.clone()); + } + S::Verbatim(Verb::Cal, text) => { + let body = self.block(at, text, "cal".len())?; + self.calibrations.push(text.clone()); + self.note(diag::PULSE_LOWERED, at, "a `cal` block read as OpenPulse and run here"); + let mut locals = Vec::new(); + let all: Vec = (0..self.frames.len()).collect(); + let r = self.exec(&body, &mut locals, true, &all)?; + if r.is_some() { + return self.fail(diag::PULSE_STATEMENT, at, "`return` in a `cal` block: only a measurement's `defcal` returns its capture"); + } + // What a block plays on a qubit's frames comes before + // the gates called on that qubit after it. + for f in 0..self.frames.len() { + let (q, t) = (self.ports[self.frames[f].port].qubit, self.state[f].time); + let e = self.qtime.entry(q).or_insert(0); + *e = (*e).max(t); + } + } + S::Verbatim(Verb::Defcal, text) => { + let d = self.defcal(at, text)?; + self.calibrations.push(text.clone()); + self.note(diag::PULSE_LOWERED, at, format!("the `defcal` of {} read as OpenPulse; it runs at each call", d.name)); + self.defcals.push(d); + } + S::Decl { ty: Type::Bit(size), name, init, how } => { + if init.is_some() || *how != super::ast::How::Plain { + return self.fail(diag::PULSE_PROGRAM, at, "a pulse program's bits are written by its measurements: declare them without a value (`bit[2] c;`)"); + } + let n = match size { + None => 1, + Some(e) => match self.eval(e, &[])? { + V::Num(x) if x.fract() == 0.0 && (1.0..=64.0).contains(&x) => x as usize, + v => return self.fail(diag::PULSE_PROGRAM, e.at, format!("a bit register's size is a whole number from 1 to 64, not {}", v.kind())), + }, + }; + if self.registers.iter().any(|r| r.0 == name.text) || self.global(&name.text).is_some() { + return self.fail(diag::DECLARED_TWICE, name.at, format!("{} is declared twice", name.text)); + } + let first = self.bits.len(); + for i in 0..n { + self.bits.push(if size.is_none() { name.text.clone() } else { format!("{}[{i}]", name.text) }); + } + self.registers.push((name.text.clone(), first, n)); + } + S::Decl { ty, name, init: Some(e), how: super::ast::How::Const } => { + let v = self.eval(e, &[])?; + let v = self.typed(ty.word(), v, e.at)?; + self.declare_global(&name.text, v, name.at)?; + } + S::Call { mods, name, params, duration, operands } => { + if let Some(m) = mods.first() { + let at = match m { + super::ast::Modifier::Inv(a) | super::ast::Modifier::Pow(_, a) | super::ast::Modifier::Ctrl { at: a, .. } => *a, + }; + return self.fail(diag::PULSE_PROGRAM, at, "a modifier on a gate in a pulse run: each gate plays its own calibration, so calibrate the gate the modifier makes and call it"); + } + if let Some(d) = duration { + return self.fail(diag::PULSE_PROGRAM, d.at, "a gate's duration in a pulse run: the gate lasts as long as its calibration plays"); + } + let qubits = self.physical(operands)?; + let mut ps = Vec::new(); + for p in params { + match self.eval(p, &[])? { + V::Num(x) => ps.push(x), + v => return self.fail(diag::PULSE_PROGRAM, p.at, format!("a gate's parameter is a number, not {}", v.kind())), + } + } + self.call_gate(&name.text, &qubits, &ps, None, at)?; + } + S::Measure { src: q, target } => self.measure(q, target.as_ref(), at)?, + S::Assign { target, op: "=", value: Expr { kind: E::Measure(q), .. } } => self.measure(q, Some(target), at)?, + S::Barrier(ops) => { + let qubits = if ops.is_empty() { self.all_qubits() } else { self.physical(ops)? }; + self.align_qubits(&qubits, at, true); + } + S::Delay { duration, operands } => { + let d = self.eval(duration, &[])?; + let d = self.samples(d, duration.at, "the delay")?; + let qubits = self.physical(operands)?; + let t = self.align_qubits(&qubits, at, false); + for f in self.frames_of(&qubits) { + self.push(f, Op::Delay { samples: d }, at)?; + self.advance(f, d, at)?; + } + if t.saturating_add(d) > MAX_SAMPLES { + return self.fail(diag::PULSE_TIMING, at, format!("the schedule runs past {MAX_SAMPLES} samples")); + } + for q in qubits { + self.qtime.insert(q, t + d); + } + } + S::Qubit { .. } => return self.fail(diag::PULSE_PROGRAM, at, "a pulse program names physical qubits ($0, $1, …), which its calibrations and ports name; a declared qubit reaches no port"), + S::Reset(_) => return self.fail(diag::PULSE_PROGRAM, at, "`reset` in a pulse run: the schedule starts from the ground state; calibrate a reset as pulses if it needs one"), + _ => return self.fail(diag::PULSE_PROGRAM, at, "a statement a pulse program does not take: it holds calibrations (`cal`, `defcal`), constants, bits, and gate calls, measurements, barriers and delays on physical qubits"), + } + } + Ok(()) + } + + fn physical(&self, ops: &[Expr]) -> R> { + let mut out = Vec::new(); + for o in ops { + match &o.kind { + E::Name(n) if is_physical(n).is_some() => { + let q = is_physical(n).unwrap_or(0); + if out.contains(&q) { + return self.fail(diag::QUBIT_TWICE, o.at, format!("{n} is named twice")); + } + out.push(q); + } + _ => return self.fail(diag::PULSE_PROGRAM, o.at, "a pulse program acts on physical qubits ($0, $1, …)"), + } + } + Ok(out) + } + + /// Every qubit a frame's port reaches, and every qubit a gate was called on. + fn all_qubits(&self) -> Vec { + let mut set: BTreeSet = self.qtime.keys().copied().collect(); + set.extend(self.ports.iter().map(|p| p.qubit)); + set.into_iter().collect() + } + + /// The qubits' frames and their times moved to the latest of them: the + /// time they are at. + fn align_qubits(&mut self, qubits: &[u32], at: Loc, mark: bool) -> u64 { + let frames = self.frames_of(qubits); + let t = frames.iter().map(|&f| self.state[f].time).chain(qubits.iter().map(|q| self.qtime.get(q).copied().unwrap_or(0))).max().unwrap_or(0); + for &f in &frames { + self.state[f].time = t; + } + self.align(&frames, at, mark); + for &q in qubits { + self.qtime.insert(q, t); + } + t + } + + fn frames_of(&self, qubits: &[u32]) -> Vec { + (0..self.frames.len()).filter(|&f| qubits.contains(&self.ports[self.frames[f].port].qubit)).collect() + } + + fn bit_of(&mut self, e: &Expr) -> R { + let reg = |n: &str| self.registers.iter().find(|r| r.0 == n).cloned(); + match &e.kind { + E::Name(n) => match reg(n) { + Some((_, first, 1)) => Ok(first), + Some((_, _, len)) => self.fail(diag::MEASURE_LENGTHS, e.at, format!("{n} is {len} bits: measure one qubit into one bit (`{n}[0] = measure $0;`)")), + None => self.fail(diag::UNDECLARED, e.at, format!("{n} is not a declared bit")), + }, + E::Index(b, items) => match (&b.kind, items.as_slice()) { + (E::Name(n), [i]) => { + let Some((_, first, len)) = reg(n) else { return self.fail(diag::UNDECLARED, b.at, format!("{n} is not a declared bit register")) }; + match self.eval(i, &[])? { + V::Num(x) if x.fract() == 0.0 && x >= 0.0 && (x as usize) < len => Ok(first + x as usize), + _ => self.fail(diag::OUT_OF_RANGE_INDEX, i.at, format!("{n} has bits 0 to {}", len - 1)), + } + } + _ => self.fail(diag::PULSE_PROGRAM, e.at, "a measurement in a pulse run is written to one bit"), + }, + _ => self.fail(diag::PULSE_PROGRAM, e.at, "a measurement in a pulse run is written to a bit"), + } + } + + fn measure(&mut self, q: &Expr, target: Option<&Expr>, at: Loc) -> R<()> { + let qubits = self.physical(std::slice::from_ref(q))?; + let bit = target.map(|t| self.bit_of(t)).transpose()?; + self.call_gate("measure", &qubits, &[], bit.map(Some), at) + } + + fn declare_global(&mut self, name: &str, v: V, at: Loc) -> R<()> { + if self.global(name).is_some() || self.registers.iter().any(|r| r.0 == name) { + return self.fail(diag::DECLARED_TWICE, at, format!("{name} is declared twice")); + } + if matches!(name, "pi" | "π" | "tau" | "τ" | "euler" | "ℇ") || is_physical(name).is_some() { + return self.fail(diag::RESERVED_NAME, at, format!("{name} is a built-in name")); + } + self.globals.push((name.to_string(), v)); + Ok(()) + } + + fn global(&self, name: &str) -> Option<&V> { + self.globals.iter().rev().find(|g| g.0 == name).map(|g| &g.1) + } + + // ----------------------------------------------------------------------- + // Frames and time + // ----------------------------------------------------------------------- + + fn push(&mut self, frame: usize, op: Op, at: Loc) -> R { + if self.instructions.len() >= MAX_INSTRUCTIONS { + return self.fail(diag::PULSE_TIMING, at, format!("the schedule holds more than {MAX_INSTRUCTIONS} instructions")); + } + let start = self.state[frame].time; + self.instructions.push(Instruction { frame, start, op, line: at.line as u32 }); + Ok(self.instructions.len() - 1) + } + + fn advance(&mut self, frame: usize, by: u64, at: Loc) -> R<()> { + let t = self.state[frame].time.saturating_add(by); + if t > MAX_SAMPLES { + return self.fail(diag::PULSE_TIMING, at, format!("the schedule runs past {MAX_SAMPLES} samples on {}", self.frames[frame].name)); + } + self.state[frame].time = t; + Ok(()) + } + + /// The frames moved to the latest of their times; with `mark`, each gets + /// a barrier at that time. + fn align(&mut self, frames: &[usize], at: Loc, mark: bool) { + let t = frames.iter().map(|&f| self.state[f].time).max().unwrap_or(0); + for &f in frames { + self.state[f].time = t; + if mark && self.instructions.len() < MAX_INSTRUCTIONS { + self.instructions.push(Instruction { frame: f, start: t, op: Op::Barrier, line: at.line as u32 }); + } + } + } + + /// The carrier's phase of `frame`, in cycles in [0, 1), at its time. + fn carrier(&self, frame: usize) -> f64 { + let s = &self.state[frame]; + fract(s.anchor_c + s.freq * (self.dt_ns * 1e-9) * (s.time - s.anchor_k) as f64) + } + + fn set_frequency(&mut self, frame: usize, f: f64, at: Loc) -> R<()> { + if !(f.is_finite() && f >= 0.0) { + return self.fail(diag::PULSE_FRAME, at, format!("a frame's frequency is a number of hertz, 0 or more, not {f}")); + } + let c = self.carrier(frame); + let s = &mut self.state[frame]; + s.anchor_c = c; + s.anchor_k = s.time; + s.freq = f; + Ok(()) + } + + /// The number of whole samples a duration is. + fn samples(&self, v: V, at: Loc, what: &str) -> R { + let x = match v { + V::Dur(x) => x, + V::Num(_) => return self.fail(diag::BAD_DURATION, at, format!("{what} is a duration: give it a unit (`160dt`, `80ns`)")), + v => return self.fail(diag::WRONG_TYPE, at, format!("{what} is a duration, not {}", v.kind())), + }; + if !(x.is_finite() && x >= 0.0) { + return self.fail(diag::PULSE_TIMING, at, format!("{what} is {x} samples; a duration is 0 or more")); + } + let r = x.round(); + if (x - r).abs() > 1e-9 * x.max(1.0) { + return self.fail(diag::PULSE_TIMING, at, format!("{what} is {x} samples at dt = {} ns: a duration in a calibration is a whole number of samples (write it in dt)", self.dt_ns)); + } + if r > MAX_SAMPLES as f64 { + return self.fail(diag::PULSE_TIMING, at, format!("{what} is {r} samples; a schedule holds at most {MAX_SAMPLES}")); + } + Ok(r as u64) + } + + // ----------------------------------------------------------------------- + // Calibration blocks + // ----------------------------------------------------------------------- + + /// The statements of a `cal` block, its word `skip` bytes long. + fn block(&self, at: Loc, text: &str, skip: usize) -> R> { + let toks = tokens_at(self.src, at.start + skip, at.start + text.len())?; + if !is_sym(toks.first(), "{") || !is_sym(toks.last(), "}") { + return self.fail(diag::EXPECTED, at, "a `cal` block is `cal { … }`"); + } + self.statements(&toks[1..toks.len() - 1]) + } + + fn defcal(&mut self, at: Loc, text: &str) -> R { + let toks = tokens_at(self.src, at.start + "defcal".len(), at.start + text.len())?; + let Some(name) = word(toks.first()).map(str::to_string) else { return self.fail(diag::EXPECTED, at, "a `defcal` names the gate it calibrates: `defcal x $0 { … }`") }; + let mut i = 1; + let mut params = Vec::new(); + if is_sym(toks.get(1), "(") { + let Some(close) = bracketed(&toks, 1) else { return self.fail(diag::UNCLOSED, loc(&toks[1]), "a `(` that is not closed") }; + for p in commas(&toks[2..close]) { + if p.is_empty() { + return self.fail(diag::EXPECTED, loc(&toks[1]), "expected a parameter"); + } + let typed = word(p.first()).is_some_and(|w| VALUE_TYPES.contains(&w)) && p.len() >= 2 && word(p.last()).is_some(); + if typed { + let n = word(p.last()).unwrap_or_default().to_string(); + params.push(Param::Var(n)); + } else { + let e = self.expr(p, loc_all(p))?; + match self.eval(&e, &[])? { + V::Num(x) => params.push(Param::Fixed(x)), + v => return self.fail(diag::PULSE_STATEMENT, e.at, format!("a calibration's fixed parameter is a number, not {}", v.kind())), + } + } + } + i = close + 1; + } + let mut qubits = Vec::new(); + while i < toks.len() && !is_sym(toks.get(i), "{") && !is_sym(toks.get(i), "->") { + match word(toks.get(i)) { + Some(w) if is_physical(w).is_some() => qubits.push(is_physical(w).unwrap_or(0)), + Some(w) => return self.fail(diag::PULSE_STATEMENT, loc(&toks[i]), format!("a `defcal` over the qubit argument {w}: the pulse reader takes calibrations of physical qubits ($0), each naming its own frames")), + None if is_sym(toks.get(i), ",") => {} + None => return self.fail(diag::EXPECTED, loc(&toks[i]), "expected a physical qubit ($0) or `{`"), + } + i += 1; + } + if qubits.is_empty() { + return self.fail(diag::EXPECTED, at, format!("the `defcal` of {name} names no physical qubit ($0)")); + } + while i < toks.len() && !is_sym(toks.get(i), "{") { + i += 1; + } + if i >= toks.len() || !is_sym(toks.last(), "}") { + return self.fail(diag::EXPECTED, at, "a `defcal` has a body in braces"); + } + let body = self.statements(&toks[i + 1..toks.len() - 1])?; + let mut frames = Vec::new(); + for s in &body { + names_as_frames(&s.c, &mut frames); + } + if body.iter().any(|s| matches!(s.c, C::Frame(..) | C::Port(_))) { + let s = body.iter().find(|s| matches!(s.c, C::Frame(..) | C::Port(_))).map(|s| s.at).unwrap_or(at); + return self.fail(diag::PULSE_STATEMENT, s, "a port or a frame made inside a `defcal`: declare ports and frames in a `cal` block, so every calibration plays on the same frames"); + } + Ok(Defcal { name, params, qubits, body, frames }) + } + + fn statements(&self, ts: &[Token]) -> R> { + let mut out = Vec::new(); + let mut i = 0; + while i < ts.len() { + match &ts[i].tok { + Tok::Verbatim(..) => return self.fail(diag::PULSE_STATEMENT, loc(&ts[i]), "a calibration inside a calibration: `cal` and `defcal` stand at the top level"), + Tok::Pragma(_) | Tok::Annotation(..) => { + i += 1; + continue; + } + _ => {} + } + if let Some(w) = word(ts.get(i)) { + if CONTROL.contains(&w) { + return self.fail(diag::PULSE_STATEMENT, loc(&ts[i]), format!("`{w}` in a calibration: the pulse reader takes straight-line calibrations (plays, delays, phase and frequency changes, captures, barriers), each a statement of its own")); + } + } + let mut depth = 0i32; + let mut j = i; + while j < ts.len() { + match &ts[j].tok { + Tok::Sym("(" | "[" | "{") => depth += 1, + Tok::Sym(")" | "]" | "}") => depth -= 1, + Tok::Sym(";") if depth == 0 => break, + _ => {} + } + j += 1; + } + if j >= ts.len() { + return self.fail(diag::EXPECTED, loc(&ts[ts.len() - 1]), "expected `;`"); + } + out.push(self.statement(&ts[i..j])?); + i = j + 1; + } + Ok(out) + } + + fn expr(&self, ts: &[Token], at: Loc) -> R { + if ts.is_empty() { + return self.fail(diag::EXPECTED, at, "expected an expression"); + } + let mut p = Parser::new(self.src, ts); + p.version = Version::Three; + let e = p.expr()?; + if p.read_so_far() != ts.len() { + let t = &ts[p.read_so_far()]; + return self.fail(diag::EXPECTED, loc(t), format!("expected the end of the statement, not `{}`", lex::text(&t.tok))); + } + Ok(e) + } + + fn list(&self, ts: &[Token], at: Loc) -> R> { + if ts.is_empty() { + return Ok(Vec::new()); + } + commas(ts).into_iter().map(|p| self.expr(p, at)).collect() + } + + fn statement(&self, ts: &[Token]) -> R { + let at = loc_all(ts); + let c = match (word(ts.first()), word(ts.get(1))) { + (Some("extern"), Some("port")) => match (word(ts.get(2)), ts.len()) { + (Some(n), 3) => C::Port(n.to_string()), + _ => return self.fail(diag::EXPECTED, at, "a port is declared `extern port d0;`"), + }, + (Some("extern"), Some(f)) if is_sym(ts.get(2), "(") && templates().contains(&f) => C::Known, + (Some("extern"), _) => return self.fail(diag::PULSE_STATEMENT, at, format!("an `extern` function: its body is outside the program; the pulse reader samples the templates {} itself, and takes `extern port`", templates().join(", "))), + (Some("port"), Some(n)) if ts.len() == 2 => C::Port(n.to_string()), + (Some("frame"), Some(n)) => { + if !is_sym(ts.get(2), "=") { + return self.fail(diag::PULSE_FRAME, at, "a frame is made with newframe(port, frequency, phase): `frame f = newframe(d0, 5.0e9, 0);`"); + } + C::Frame(n.to_string(), self.expr(&ts[3..], at)?) + } + (Some("waveform"), Some(n)) => { + if !is_sym(ts.get(2), "=") { + return self.fail(diag::PULSE_WAVEFORM, at, "a waveform is declared with its value: `waveform w = gaussian(0.2, 160dt, 40dt);`"); + } + C::Let("waveform", n.to_string(), self.expr(&ts[3..], at)?) + } + (Some("const"), _) => self.declaration(&ts[1..], at)?, + (Some(t), _) if VALUE_TYPES.contains(&t) => self.declaration(ts, at)?, + (Some("bit"), _) => { + if is_sym(ts.get(1), "[") { + return self.fail(diag::PULSE_CAPTURE, at, "a bit in a calibration is one bit: `bit b = capture_v2(a0, 1000dt);`"); + } + let Some(n) = word(ts.get(1)) else { return self.fail(diag::EXPECTED, at, "expected the bit's name") }; + match ts.len() { + 2 => C::Bit(n.to_string(), None), + _ if is_sym(ts.get(2), "=") => C::Bit(n.to_string(), Some(self.expr(&ts[3..], at)?)), + _ => return self.fail(diag::EXPECTED, at, "expected `=` or `;`"), + } + } + (Some("delay"), _) => { + if !is_sym(ts.get(1), "[") { + return self.fail(diag::EXPECTED, at, "a delay is `delay[d] f, …;`"); + } + let Some(close) = bracketed(ts, 1) else { return self.fail(diag::UNCLOSED, at, "a `[` that is not closed") }; + let d = self.expr(&ts[2..close], at)?; + C::Delay(d, self.list(&ts[close + 1..], at)?) + } + (Some("barrier"), _) => C::Barrier(self.list(&ts[1..], at)?), + (Some("return"), _) => C::Return(self.expr(&ts[1..], at)?), + (Some(n), _) if is_sym(ts.get(1), "=") => C::Assign(n.to_string(), self.expr(&ts[2..], at)?), + (Some(_), _) if is_sym(ts.get(1), "(") => C::Call(self.expr(ts, at)?), + _ => return self.fail(diag::PULSE_STATEMENT, at, "a statement the pulse reader does not take in a calibration: it takes ports, frames, waveforms, values, bits, play, delay, shift_phase, set_phase, shift_frequency, set_frequency, captures, barriers and `return`"), + }; + Ok(CStmt { c, at }) + } + + /// `type[…] name = value` (the `const` read). + fn declaration(&self, ts: &[Token], at: Loc) -> R { + let Some(t) = word(ts.first()).and_then(|w| VALUE_TYPES.iter().copied().find(|x| *x == w)) else { + return self.fail(diag::PULSE_STATEMENT, at, "expected a type: float, int, uint, angle, duration, complex or bool"); + }; + let mut i = 1; + if is_sym(ts.get(1), "[") { + let Some(close) = bracketed(ts, 1) else { return self.fail(diag::UNCLOSED, at, "a `[` that is not closed") }; + i = close + 1; + } + let Some(n) = word(ts.get(i)) else { return self.fail(diag::EXPECTED, at, "expected the name declared") }; + if !is_sym(ts.get(i + 1), "=") { + return self.fail(diag::PULSE_STATEMENT, at, format!("{n} is declared without a value: a calibration's values are given where they are declared")); + } + Ok(C::Let(t, n.to_string(), self.expr(&ts[i + 2..], at)?)) + } + + // ----------------------------------------------------------------------- + // Running a calibration + // ----------------------------------------------------------------------- + + fn lookup(&self, name: &str, locals: &[(String, V)]) -> Option { + locals.iter().rev().find(|l| l.0 == name).map(|l| l.1.clone()).or_else(|| self.global(name).cloned()) + } + + fn typed(&self, ty: &str, v: V, at: Loc) -> R { + let ok = match (ty, &v) { + ("waveform", V::Wave(_)) => true, + ("duration", V::Dur(_)) => true, + ("float" | "angle" | "int" | "uint", V::Num(_)) => true, + ("complex", V::Num(x)) => return Ok(V::Cx(*x, 0.0)), + ("complex", V::Cx(..)) => true, + ("bool", V::Bool(_)) => true, + _ => false, + }; + if !ok { + let code = if ty == "waveform" { diag::PULSE_WAVEFORM } else { diag::WRONG_TYPE }; + return self.fail(code, at, format!("a {ty} is declared with {}", v.kind())); + } + if matches!(ty, "int" | "uint") { + if let V::Num(x) = v { + if x.fract() != 0.0 || (ty == "uint" && x < 0.0) { + return self.fail(diag::INTEGER_RANGE, at, format!("an {ty} is a whole number{}, not {x}", if ty == "uint" { " from 0" } else { "" })); + } + } + } + Ok(v) + } + + fn frame_arg(&mut self, e: &Expr, locals: &[(String, V)], what: &str) -> R { + match self.eval(e, locals)? { + V::Frame(f) => Ok(f), + v => self.fail(diag::PULSE_FRAME, e.at, format!("{what} is on a frame, not {}", v.kind())), + } + } + + fn exec(&mut self, body: &[CStmt], locals: &mut Vec<(String, V)>, global: bool, scope: &[usize]) -> R> { + for s in body { + let at = s.at; + match &s.c { + C::Port(name) => { + if !global { + return self.fail(diag::PULSE_STATEMENT, at, "declare ports in a `cal` block"); + } + let Some(p) = self.port_map.iter().find(|p| p.name == *name).cloned().or_else(|| Port::by_convention(name)) else { + return self.fail(diag::PULSE_FRAME, at, format!("the port {name} reaches no qubit: name drive ports d0, d1, …, measurement ports m0, … and acquisition ports a0, …, or map it in the job's pulse.ports")); + }; + self.ports.push(p); + let i = self.ports.len() - 1; + self.declare_global(name, V::Port(i), at)?; + } + C::Known => {} + C::Frame(name, e) => { + let E::Call(f, args) = &e.kind else { return self.fail(diag::PULSE_FRAME, e.at, "a frame is made with newframe(port, frequency, phase)") }; + if f.text != "newframe" || args.len() != 3 { + return self.fail(diag::PULSE_FRAME, e.at, "a frame is made with newframe(port, frequency, phase): three arguments"); + } + let port = match self.eval(&args[0], locals)? { + V::Port(p) => p, + v => return self.fail(diag::PULSE_FRAME, args[0].at, format!("newframe's first argument is a port, not {}", v.kind())), + }; + let freq = match self.eval(&args[1], locals)? { + V::Num(x) if x.is_finite() && x >= 0.0 => x, + v => return self.fail(diag::PULSE_FRAME, args[1].at, format!("a frame's frequency is a number of hertz, 0 or more, not {}", v.kind())), + }; + let phase = match self.eval(&args[2], locals)? { + V::Num(x) if x.is_finite() => wrap(x), + v => return self.fail(diag::PULSE_FRAME, args[2].at, format!("a frame's phase is an angle, not {}", v.kind())), + }; + self.frames.push(Frame { name: name.clone(), port, frequency_hz: freq, phase }); + self.state.push(FrameState { time: 0, phase, freq, anchor_k: 0, anchor_c: 0.0 }); + let i = self.frames.len() - 1; + self.declare_global(name, V::Frame(i), at)?; + } + C::Let(ty, name, e) => { + if *ty == "waveform" { + self.known_template(e)?; + } + let v = self.eval(e, locals)?; + let v = self.typed(ty, v, e.at)?; + if let (&"waveform", V::Wave(w)) = (ty, &v) { + if !self.named[*w] { + self.waveforms[*w].name = name.clone(); + self.named[*w] = true; + } + } + if global { + self.declare_global(name, v, at)?; + } else { + if locals.iter().any(|l| l.0 == *name) { + return self.fail(diag::DECLARED_TWICE, at, format!("{name} is declared twice")); + } + locals.push((name.clone(), v)); + } + } + C::Bit(name, init) => { + let v = match init { + Some(e) => match self.eval(e, locals)? { + b @ V::Bit(_) => b, + v => return self.fail(diag::PULSE_CAPTURE, e.at, format!("a bit is given a capture, not {}", v.kind())), + }, + None => V::Bit(None), + }; + if global { + self.declare_global(name, v, at)?; + } else { + locals.push((name.clone(), v)); + } + } + C::Assign(name, e) => { + let v = self.eval(e, locals)?; + let is_bit = matches!(self.lookup(name, locals), Some(V::Bit(_))); + match (is_bit, &v) { + (true, V::Bit(_)) => { + if let Some(l) = locals.iter_mut().rev().find(|l| l.0 == *name) { + l.1 = v; + } else if let Some(g) = self.globals.iter_mut().rev().find(|g| g.0 == *name) { + g.1 = v; + } + } + (true, _) => return self.fail(diag::PULSE_CAPTURE, e.at, format!("{name} is a bit: it is given a capture, not {}", v.kind())), + (false, _) => return self.fail(diag::PULSE_STATEMENT, at, format!("an assignment to {name}: a calibration's values are fixed where they are declared; only a bit is given a capture later")), + } + } + C::Call(e) => self.exec_call(e, locals)?, + C::Delay(d, frames) => { + let d = self.eval(d, locals)?; + let n = self.samples(d, at, "the delay")?; + let mut fs = Vec::new(); + for f in frames { + fs.push(self.frame_arg(f, locals, "a delay")?); + } + if fs.is_empty() { + return self.fail(diag::PULSE_FRAME, at, "a delay in a calibration names its frames: `delay[100dt] f;`"); + } + for f in fs { + self.push(f, Op::Delay { samples: n }, at)?; + self.advance(f, n, at)?; + } + } + C::Barrier(frames) => { + let mut fs = Vec::new(); + for f in frames { + fs.push(self.frame_arg(f, locals, "a barrier")?); + } + if fs.is_empty() { + fs = if global { (0..self.frames.len()).collect() } else { scope.to_vec() }; + } + self.align(&fs, at, true); + } + C::Return(e) => { + if global { + return self.fail(diag::PULSE_STATEMENT, at, "`return` in a `cal` block: only a measurement's `defcal` returns its capture"); + } + return Ok(Some(self.eval(e, locals)?)); + } + } + } + Ok(None) + } + + fn exec_call(&mut self, e: &Expr, locals: &[(String, V)]) -> R<()> { + let E::Call(name, args) = &e.kind else { return self.fail(diag::PULSE_STATEMENT, e.at, "a statement the pulse reader does not take") }; + let arity = |n: usize| -> R<()> { if args.len() == n { Ok(()) } else { self.fail(diag::ARITY, e.at, format!("{} takes {n} arguments, not {}", name.text, args.len())) } }; + let at = e.at; + match name.text.as_str() { + "play" => { + arity(2)?; + let f = self.frame_arg(&args[0], locals, "a play")?; + self.known_template(&args[1])?; + let w = match self.eval(&args[1], locals)? { + V::Wave(w) => w, + v => return self.fail(diag::PULSE_WAVEFORM, args[1].at, format!("a play plays a waveform, not {}", v.kind())), + }; + let kind = self.ports[self.frames[f].port].kind; + match kind { + PortKind::Acquire => return self.fail(diag::PULSE_FRAME, args[0].at, format!("a play on {}, a frame of the acquisition port {}: acquisition ports are captured, not played on", self.frames[f].name, self.ports[self.frames[f].port].name)), + PortKind::Measure if self.warned.insert(f) => { + let msg = format!("plays on {}, a frame of the measurement port {}, are scheduled and timed; their effect on the readout is not simulated (a capture reads the qubit's state)", self.frames[f].name, self.ports[self.frames[f].port].name); + self.note(diag::PULSE_NOT_DRIVEN, at, msg); + } + _ => {} + } + let (phase, frequency_hz, carrier) = (self.state[f].phase, self.state[f].freq, self.carrier(f)); + let len = self.waveforms[w].samples.len() as u64; + self.push(f, Op::Play { waveform: w, phase, frequency_hz, carrier }, at)?; + self.advance(f, len, at)?; + } + "shift_phase" | "set_phase" => { + arity(2)?; + let f = self.frame_arg(&args[0], locals, &name.text)?; + let x = match self.eval(&args[1], locals)? { + V::Num(x) if x.is_finite() => x, + v => return self.fail(diag::PULSE_FRAME, args[1].at, format!("a phase is an angle, not {}", v.kind())), + }; + let s = &mut self.state[f]; + s.phase = if name.text == "set_phase" { wrap(x) } else { wrap(s.phase + x) }; + let op = if name.text == "set_phase" { Op::SetPhase(x) } else { Op::ShiftPhase(x) }; + self.push(f, op, at)?; + } + "shift_frequency" | "set_frequency" => { + arity(2)?; + let f = self.frame_arg(&args[0], locals, &name.text)?; + let x = match self.eval(&args[1], locals)? { + V::Num(x) if x.is_finite() => x, + v => return self.fail(diag::PULSE_FRAME, args[1].at, format!("a frequency is a number of hertz, not {}", v.kind())), + }; + let to = if name.text == "set_frequency" { x } else { self.state[f].freq + x }; + self.set_frequency(f, to, args[1].at)?; + let op = if name.text == "set_frequency" { Op::SetFrequency(x) } else { Op::ShiftFrequency(x) }; + self.push(f, op, at)?; + } + n if CAPTURES.contains(&n) => { + self.eval(e, locals)?; + } + "newframe" => return self.fail(diag::PULSE_STATEMENT, at, "newframe makes a frame in a declaration: `frame f = newframe(d0, 5.0e9, 0);` in a `cal` block"), + "get_phase" | "get_frequency" => return self.fail(diag::PULSE_STATEMENT, at, format!("{}: the reader does not read a frame's state back; keep the value in a constant", name.text)), + n => return self.fail(diag::PULSE_STATEMENT, at, format!("`{n}(…)` is not a statement the pulse reader takes: play, shift_phase, set_phase, shift_frequency, set_frequency, a capture, delay and barrier are")), + } + Ok(()) + } + + fn call_gate(&mut self, name: &str, qubits: &[u32], params: &[f64], bit: Option>, at: Loc) -> R<()> { + if self.calls.len() >= MAX_CALLS { + return self.fail(diag::PULSE_TIMING, at, format!("more than {MAX_CALLS} gate calls")); + } + let fits = |d: &Defcal, exact: bool| { + d.name == name + && d.qubits == qubits + && d.params.len() == params.len() + && d.params.iter().zip(params).all(|(p, &x)| match p { + Param::Fixed(y) => (x - y).abs() <= 1e-12 * x.abs().max(1.0), + Param::Var(_) => !exact, + }) + }; + let Some(d) = self.defcals.iter().find(|d| fits(d, true)).or_else(|| self.defcals.iter().find(|d| fits(d, false))).cloned() else { + let qs: Vec = qubits.iter().map(|q| format!("${q}")).collect(); + let elsewhere: Vec = self.defcals.iter().filter(|d| d.name == name).map(|d| d.qubits.iter().map(|q| format!("${q}")).collect::>().join(", ")).collect(); + let hint = if elsewhere.is_empty() { String::new() } else { format!("; {name} is calibrated on {}", elsewhere.join("; ")) }; + return self.fail(diag::PULSE_NO_CALIBRATION, at, format!("no `defcal` calibrates {name} on {} with {} parameter{}: a pulse run plays each gate's calibration, so add `defcal {name} {} {{ … }}` before the call{hint}", qs.join(", "), params.len(), if params.len() == 1 { "" } else { "s" }, qs.join(", "))); + }; + let mut locals: Vec<(String, V)> = Vec::new(); + for (p, &x) in d.params.iter().zip(params) { + if let Param::Var(n) = p { + locals.push((n.clone(), V::Num(x))); + } + } + let scope: Vec = d.frames.iter().filter_map(|n| match self.global(n) { + Some(V::Frame(f)) => Some(*f), + _ => None, + }).collect::>().into_iter().collect(); + // A gate starts when its qubits are free and its frames are, all at once. + let start = scope.iter().map(|&f| self.state[f].time).chain(qubits.iter().map(|q| self.qtime.get(q).copied().unwrap_or(0))).max().unwrap_or(0); + for &f in &scope { + self.state[f].time = start; + } + let first = self.instructions.len(); + let ret = self.exec(&d.body, &mut locals, false, &scope)?; + if let Some(target) = bit { + let capture = match ret { + Some(V::Bit(Some(i))) => Some(i), + Some(V::Bit(None)) => None, + Some(v) => return self.fail(diag::PULSE_CAPTURE, at, format!("the calibration of {name} returns {}, not a captured bit", v.kind())), + None => { + let made: Vec = (first..self.instructions.len()).filter(|&i| matches!(self.instructions[i].op, Op::Capture { .. })).collect(); + match made.as_slice() { + [one] => Some(*one), + _ => return self.fail(diag::PULSE_CAPTURE, at, format!("the calibration of {name} makes {} captures and returns none: end it with `return capture_v2(a0, …);`", made.len())), + } + } + }; + match (capture, target) { + (Some(i), Some(b)) => { + if let Op::Capture { bit, .. } = &mut self.instructions[i].op { + *bit = Some(b); + } + } + (None, Some(_)) => return self.fail(diag::PULSE_CAPTURE, at, format!("the calibration of {name} returns a bit no capture wrote")), + _ => {} + } + } + let end = scope.iter().map(|&f| self.state[f].time).max().unwrap_or(start).max(start); + for &q in qubits { + self.qtime.insert(q, end); + } + self.calls.push(Call { name: name.to_string(), qubits: qubits.to_vec(), params: params.to_vec(), start, end, line: at.line as u32 }); + Ok(()) + } + + // ----------------------------------------------------------------------- + // Expressions + // ----------------------------------------------------------------------- + + fn eval(&mut self, e: &Expr, locals: &[(String, V)]) -> R { + let at = e.at; + match &e.kind { + E::Int(v) => { + if *v > (1u128 << 53) { + return self.fail(diag::INTEGER_RANGE, at, format!("{v} is past 2^53, the largest whole number a calibration holds exactly")); + } + Ok(V::Num(*v as f64)) + } + E::Float(x) => Ok(V::Num(*x)), + E::Imag(x) => Ok(V::Cx(0.0, *x)), + E::Bool(b) => Ok(V::Bool(*b)), + E::Duration(x, u) => Ok(V::Dur(match u.nanoseconds() { + None => *x, + Some(ns) => x * ns / self.dt_ns, + })), + E::Name(n) => match self.lookup(n, locals) { + Some(v) => Ok(v), + None => match n.as_str() { + "pi" | "π" => Ok(V::Num(PI)), + "tau" | "τ" => Ok(V::Num(TAU)), + "euler" | "ℇ" => Ok(V::Num(EULER)), + _ => self.fail(diag::UNDECLARED, at, format!("{n} is not declared: a calibration names the ports, frames, waveforms and values its `cal` blocks declare, and its own parameters")), + }, + }, + E::Unary(op, a) => { + let v = self.eval(a, locals)?; + match (*op, v) { + ("-", V::Num(x)) => Ok(V::Num(-x)), + ("-", V::Cx(x, y)) => Ok(V::Cx(-x, -y)), + ("-", V::Dur(x)) => Ok(V::Dur(-x)), + ("+", v @ (V::Num(_) | V::Cx(..) | V::Dur(_))) => Ok(v), + ("!", V::Bool(b)) => Ok(V::Bool(!b)), + (op, v) => self.fail(diag::WRONG_TYPE, at, format!("`{op}` does not apply to {}", v.kind())), + } + } + E::Binary(op, a, b) => { + let (x, y) = (self.eval(a, locals)?, self.eval(b, locals)?); + self.arith(op, x, y, at) + } + E::Call(name, args) => self.call_value(&name.text, args, locals, at), + E::Set(items) => { + let mut s = Vec::with_capacity(items.len()); + for it in items { + match self.eval(it, locals)? { + V::Num(x) if x.is_finite() => s.push((x, 0.0)), + V::Cx(x, y) if x.is_finite() && y.is_finite() => s.push((x, y)), + v => return self.fail(diag::PULSE_WAVEFORM, it.at, format!("a waveform's sample is a real or complex number, not {}", v.kind())), + } + } + if s.is_empty() || s.len() as u64 > MAX_WAVEFORM_SAMPLES { + return self.fail(diag::PULSE_WAVEFORM, at, format!("a waveform holds 1 to {MAX_WAVEFORM_SAMPLES} samples")); + } + let key = format!("samples:{}", s.iter().map(|&(x, y)| format!("{:016x}{:016x}", x.to_bits(), y.to_bits())).collect::()); + let text = format!("samples({})", s.len()); + self.wave(key, text, s, at) + } + E::Cast(ty, a) => match (ty.word(), self.eval(a, locals)?) { + ("float" | "angle", V::Num(x)) => Ok(V::Num(x)), + ("complex", V::Num(x)) => Ok(V::Cx(x, 0.0)), + ("complex", v @ V::Cx(..)) => Ok(v), + ("duration", v @ V::Dur(_)) => Ok(v), + (t, v) => self.fail(diag::BAD_CAST, at, format!("a cast of {} to {t} in a calibration", v.kind())), + }, + _ => self.fail(diag::PULSE_STATEMENT, at, "an expression a calibration does not take: numbers, durations, names, arithmetic, sin, cos, sqrt, exp, the waveform templates and captures"), + } + } + + fn arith(&self, op: &str, x: V, y: V, at: Loc) -> R { + let cx = |v: &V| match v { + V::Num(a) => Some((*a, 0.0)), + V::Cx(a, b) => Some((*a, *b)), + _ => None, + }; + let zero = |b: f64| -> R<()> { if b == 0.0 { self.fail(diag::DIVISION_BY_ZERO, at, "division by zero") } else { Ok(()) } }; + let out = match (op, &x, &y) { + ("+", V::Num(a), V::Num(b)) => V::Num(a + b), + ("-", V::Num(a), V::Num(b)) => V::Num(a - b), + ("*", V::Num(a), V::Num(b)) => V::Num(a * b), + ("/", V::Num(a), V::Num(b)) => { + zero(*b)?; + V::Num(a / b) + } + ("**", V::Num(a), V::Num(b)) => { + if b.fract() != 0.0 || b.abs() > 64.0 { + return self.fail(diag::PULSE_STATEMENT, at, "a power in a calibration has a whole exponent of at most 64 (write sqrt for a square root)"); + } + let mut r = 1.0; + for _ in 0..b.abs() as u32 { + r *= a; + } + if *b < 0.0 { + zero(r)?; + r = 1.0 / r; + } + V::Num(r) + } + ("+", V::Dur(a), V::Dur(b)) => V::Dur(a + b), + ("-", V::Dur(a), V::Dur(b)) => V::Dur(a - b), + ("*", V::Dur(a), V::Num(b)) | ("*", V::Num(b), V::Dur(a)) => V::Dur(a * b), + ("/", V::Dur(a), V::Num(b)) => { + zero(*b)?; + V::Dur(a / b) + } + ("/", V::Dur(a), V::Dur(b)) => { + zero(*b)?; + V::Num(a / b) + } + ("+" | "-" | "*" | "/", _, _) if cx(&x).is_some() && cx(&y).is_some() => { + let (a, b) = (cx(&x).unwrap_or_default(), cx(&y).unwrap_or_default()); + match op { + "+" => V::Cx(a.0 + b.0, a.1 + b.1), + "-" => V::Cx(a.0 - b.0, a.1 - b.1), + "*" => { + let p = cmul(a, b); + V::Cx(p.0, p.1) + } + _ => { + let d = b.0 * b.0 + b.1 * b.1; + zero(d)?; + V::Cx((a.0 * b.0 + a.1 * b.1) / d, (a.1 * b.0 - a.0 * b.1) / d) + } + } + } + ("==" | "!=" | "<" | "<=" | ">" | ">=", V::Num(a), V::Num(b)) => V::Bool(match op { + "==" => a == b, + "!=" => a != b, + "<" => a < b, + "<=" => a <= b, + ">" => a > b, + _ => a >= b, + }), + _ => return self.fail(diag::WRONG_TYPE, at, format!("`{op}` between {} and {}", x.kind(), y.kind())), + }; + Ok(out) + } + + fn call_value(&mut self, name: &str, args: &[Expr], locals: &[(String, V)], at: Loc) -> R { + let num = |s: &mut Self, i: usize| -> R { + match s.eval(&args[i], locals)? { + V::Num(x) if x.is_finite() => Ok(x), + v => s.fail(diag::WRONG_TYPE, args[i].at, format!("{name} takes a number, not {}", v.kind())), + } + }; + match name { + "sin" | "cos" | "sqrt" | "exp" => { + if args.len() != 1 { + return self.fail(diag::ARITY, at, format!("{name} takes one argument")); + } + let x = num(self, 0)?; + Ok(V::Num(match name { + "sin" => repro::sin_cos(x).0, + "cos" => repro::sin_cos(x).1, + "exp" => repro::exp(x), + _ => { + if x < 0.0 { + return self.fail(diag::WRONG_TYPE, args[0].at, "the square root of a negative number"); + } + x.sqrt() + } + })) + } + n if templates().contains(&n) => self.template(n, args, locals, at), + n if CAPTURES.contains(&n) => { + if args.len() != 2 { + return self.fail(diag::PULSE_CAPTURE, at, format!("{n} takes a frame and a duration or a waveform")); + } + let f = match self.eval(&args[0], locals)? { + V::Frame(f) => f, + v => return self.fail(diag::PULSE_CAPTURE, args[0].at, format!("a capture is on a frame, not {}", v.kind())), + }; + let p = &self.ports[self.frames[f].port]; + if p.kind != PortKind::Acquire { + return self.fail(diag::PULSE_CAPTURE, args[0].at, format!("a capture is on a frame of an acquisition port (a0 for qubit 0); {} is on {}, a {} port", self.frames[f].name, p.name, p.kind.as_str())); + } + let len = match self.eval(&args[1], locals)? { + V::Wave(w) => self.waveforms[w].samples.len() as u64, + d @ V::Dur(_) => self.samples(d, args[1].at, "the capture")?, + v => return self.fail(diag::PULSE_CAPTURE, args[1].at, format!("a capture lasts a duration or a waveform's length, not {}", v.kind())), + }; + if len == 0 { + return self.fail(diag::PULSE_CAPTURE, args[1].at, "a capture lasts at least one sample"); + } + let i = self.push(f, Op::Capture { samples: len, bit: None }, at)?; + self.advance(f, len, at)?; + Ok(V::Bit(Some(i))) + } + "newframe" => self.fail(diag::PULSE_STATEMENT, at, "newframe makes a frame in a declaration: `frame f = newframe(d0, 5.0e9, 0);` in a `cal` block"), + "get_phase" | "get_frequency" => self.fail(diag::PULSE_STATEMENT, at, format!("{name}: the reader does not read a frame's state back; keep the value in a constant")), + _ => self.fail(diag::PULSE_STATEMENT, at, format!("{name} is not a function a calibration takes: sin, cos, sqrt, exp, the waveform templates ({}) and the captures", templates().join(", "))), + } + } + + /// A call in a waveform's place names a template the reader samples. + fn known_template(&self, e: &Expr) -> R<()> { + match &e.kind { + E::Call(n, _) if !templates().contains(&n.text.as_str()) && !matches!(n.text.as_str(), "sin" | "cos" | "sqrt" | "exp") => { + self.fail(diag::PULSE_WAVEFORM, n.at, format!("{} is not a waveform template the reader samples: {} are, and explicit samples {{s0, s1, …}}", n.text, templates().join(", "))) + } + _ => Ok(()), + } + } + + fn wave(&mut self, key: String, text: String, samples: Vec<(f64, f64)>, at: Loc) -> R { + if let Some(&w) = self.keys.get(&key) { + return Ok(V::Wave(w)); + } + if self.held + samples.len() > MAX_HELD_SAMPLES { + return self.fail(diag::PULSE_WAVEFORM, at, format!("the waveforms hold more than {MAX_HELD_SAMPLES} samples in all")); + } + self.held += samples.len(); + self.waveforms.push(Waveform { name: text, samples }); + self.named.push(false); + let w = self.waveforms.len() - 1; + self.keys.insert(key, w); + Ok(V::Wave(w)) + } + + fn template(&mut self, name: &str, args: &[Expr], locals: &[(String, V)], at: Loc) -> R { + let want = match name { + "constant" => 2, + "gaussian" | "sech" => 3, + _ => 4, + }; + if args.len() != want { + return self.fail(diag::PULSE_WAVEFORM, at, format!("{name} takes {want} arguments, not {}", args.len())); + } + let mut vals = Vec::with_capacity(want); + for a in args { + vals.push(self.eval(a, locals)?); + } + let amp = match vals[0] { + V::Num(x) if x.is_finite() => (x, 0.0), + V::Cx(x, y) if x.is_finite() && y.is_finite() => (x, y), + ref v => return self.fail(diag::PULSE_WAVEFORM, args[0].at, format!("a waveform's amplitude is a real or complex number, not {}", v.kind())), + }; + let n = self.samples(vals[1].clone(), args[1].at, "a waveform's length")?; + if n == 0 || n > MAX_WAVEFORM_SAMPLES { + return self.fail(diag::PULSE_WAVEFORM, args[1].at, format!("a waveform is 1 to {MAX_WAVEFORM_SAMPLES} samples long, not {n}")); + } + let dur = |s: &Self, i: usize, what: &str, positive: bool| -> R { + match vals[i] { + V::Dur(x) if x.is_finite() && (x > 0.0 || (!positive && x >= 0.0)) => Ok(x), + V::Dur(x) => s.fail(diag::PULSE_WAVEFORM, args[i].at, format!("{what} is {x} samples; it is {}", if positive { "more than 0" } else { "0 or more" })), + ref v => s.fail(diag::PULSE_WAVEFORM, args[i].at, format!("{what} is a duration (`40dt`), not {}", v.kind())), + } + }; + let real = |s: &Self, i: usize, what: &str| -> R { + match vals[i] { + V::Num(x) if x.is_finite() => Ok(x), + ref v => s.fail(diag::PULSE_WAVEFORM, args[i].at, format!("{what} is a number, not {}", v.kind())), + } + }; + let c = (n as f64 - 1.0) / 2.0; + let gauss = |t: f64, s: f64| repro::exp(-(t * t) / (2.0 * s * s)); + let mut key = format!("{name}:{:016x}{:016x}:{n}", amp.0.to_bits(), amp.1.to_bits()); + let samples: Vec<(f64, f64)> = match name { + "constant" => vec![amp; n as usize], + "gaussian" | "sech" => { + let s = dur(self, 2, "sigma", true)?; + key.push_str(&format!(":{:016x}", s.to_bits())); + (0..n) + .map(|k| { + let t = k as f64 - c; + let g = if name == "gaussian" { + gauss(t, s) + } else { + let y = repro::exp(-(t / s).abs()); + 2.0 * y / (1.0 + y * y) + }; + (amp.0 * g, amp.1 * g) + }) + .collect() + } + "gaussian_square" => { + let w = dur(self, 2, "the flat width", false)?; + let s = dur(self, 3, "sigma", true)?; + if w > n as f64 { + return self.fail(diag::PULSE_WAVEFORM, args[2].at, format!("the flat width, {w} samples, is longer than the waveform, {n}")); + } + key.push_str(&format!(":{:016x}:{:016x}", w.to_bits(), s.to_bits())); + (0..n) + .map(|k| { + let t = (k as f64 - c).abs(); + let g = if t <= w / 2.0 { 1.0 } else { gauss(t - w / 2.0, s) }; + (amp.0 * g, amp.1 * g) + }) + .collect() + } + "drag" => { + let s = dur(self, 2, "sigma", true)?; + let beta = real(self, 3, "beta")?; + key.push_str(&format!(":{:016x}:{:016x}", s.to_bits(), beta.to_bits())); + (0..n) + .map(|k| { + let t = k as f64 - c; + let g = gauss(t, s); + cmul(amp, (g, -(beta * t / (s * s)) * g)) + }) + .collect() + } + _ => { + let f = real(self, 2, "the frequency")?; + let ph = real(self, 3, "the phase")?; + key.push_str(&format!(":{:016x}:{:016x}", f.to_bits(), ph.to_bits())); + let per = f * self.dt_ns * 1e-9; + (0..n) + .map(|k| { + let s = repro::sin_cos(TAU * fract(per * k as f64) + ph).0; + (amp.0 * s, amp.1 * s) + }) + .collect() + } + }; + if samples.iter().any(|s| !(s.0.is_finite() && s.1.is_finite())) { + return self.fail(diag::PULSE_WAVEFORM, at, format!("{name} has a sample that is not a finite number")); + } + let text = self.src.get(at.start..at.end).unwrap_or(name).to_string(); + self.wave(key, text, samples, at) + } +} + +/// The names a calibration statement uses as frames: each frame call's +/// first argument, a delay's and a barrier's frames, and a capture's frame +/// wherever it stands. +fn names_as_frames(c: &C, out: &mut Vec) { + fn in_expr(e: &Expr, out: &mut Vec) { + if let E::Call(n, args) = &e.kind { + if FRAME_CALLS.contains(&n.text.as_str()) || CAPTURES.contains(&n.text.as_str()) { + if let Some(Expr { kind: E::Name(f), .. }) = args.first() { + if !out.contains(f) { + out.push(f.clone()); + } + } + } + for a in args { + in_expr(a, out); + } + } + } + let names = |es: &[Expr], out: &mut Vec| { + for e in es { + if let E::Name(f) = &e.kind { + if !out.contains(f) { + out.push(f.clone()); + } + } + } + }; + match c { + C::Call(e) | C::Return(e) | C::Assign(_, e) | C::Let(_, _, e) | C::Bit(_, Some(e)) => in_expr(e, out), + C::Delay(_, fs) | C::Barrier(fs) => names(fs, out), + _ => {} + } +} + +#[cfg(test)] +mod tests; diff --git a/wai/wai-quantum/src/qasm/pulse/tests.rs b/wai/wai-quantum/src/qasm/pulse/tests.rs new file mode 100644 index 000000000..b3b532347 --- /dev/null +++ b/wai/wai-quantum/src/qasm/pulse/tests.rs @@ -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::>(), [("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::>(), [("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::>())).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::>(), g.iter().map(|x| (x.0.to_bits(), x.1.to_bits())).collect::>()); +} + +#[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::>(), [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"); +} -- 2.54.0 (Apple Git-157) From fd289c5afbe36609254cf03bf7390e1e61dc6b5b Mon Sep 17 00:00:00 2001 From: Transaction Science Date: Fri, 30 Oct 2026 10:00:41 -0400 Subject: [PATCH 02/10] wai-quantum: a pulse simulator under a Lindblad master equation, the same bits everywhere, its error bounded quantum_pulse (feature quantum_pulse, in quantum_engine and full) runs a pulse schedule on the qubits its ports reach: two or three levels each in its rotating frame, the drive as the frames' carriers set it, anharmonicity, static couplings, and relaxation and dephasing from T1 and T2. Each sample is integrated in the frame of each qubit's tone, where the generator is constant: a fixed number of steps of a twelfth-order Taylor series, then a diagonal phase back. Only + - * / and sqrt are used in a fixed order with the crate's own sine and cosine, so the density matrix is the same bits on every platform; a test pins its digest, which x86_64 and arm64 reproduce. The plan states a proved bound on the trace distance from the model's exact solution and an estimate of rounding beside it, and refuses a step longer than 1/lambda or a schedule whose error would pass its tolerance, naming the steps that would meet it. Tests hold it to closed forms within the bound it states: Rabi turns, the axis a quarter turn of phase gives, detuned drives, T1 decay and T2 dephasing, a detuned play split in two; and to joint captures, leakage with three levels, and the model's refusals. Co-Authored-By: Claude Opus 5.5 --- wai/wai-quantum/Cargo.toml | 9 +- wai/wai-quantum/src/lib.rs | 5 + wai/wai-quantum/src/quantum_pulse.rs | 834 +++++++++++++++++++++ wai/wai-quantum/src/quantum_pulse/tests.rs | 198 +++++ 4 files changed, 1044 insertions(+), 2 deletions(-) create mode 100644 wai/wai-quantum/src/quantum_pulse.rs create mode 100644 wai/wai-quantum/src/quantum_pulse/tests.rs diff --git a/wai/wai-quantum/Cargo.toml b/wai/wai-quantum/Cargo.toml index 908839161..af2da66c2 100644 --- a/wai/wai-quantum/Cargo.toml +++ b/wai/wai-quantum/Cargo.toml @@ -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", diff --git a/wai/wai-quantum/src/lib.rs b/wai/wai-quantum/src/lib.rs index fb7cbafcf..16cca6176 100644 --- a/wai/wai-quantum/src/lib.rs +++ b/wai/wai-quantum/src/lib.rs @@ -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; diff --git a/wai/wai-quantum/src/quantum_pulse.rs b/wai/wai-quantum/src/quantum_pulse.rs new file mode 100644 index 000000000..38e233a43 --- /dev/null +++ b/wai/wai-quantum/src/quantum_pulse.rs @@ -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, + pub t2_ns: Option, +} + +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, t2_ns: Option) -> 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, + 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, +} + +impl Default for Settings { + fn default() -> Settings { + Settings { tolerance: DEFAULT_TOLERANCE, substeps: None } + } +} + +impl Settings { + pub fn new(tolerance: f64, substeps: Option) -> 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, + 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, + /// Each simulated qubit's final probability of level 2. + pub leakage: Vec, + pub captures: Vec, + /// Over the captures, index bit j the j-th capture. + pub joint: Option>, + /// 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, + levels: usize, + dim: usize, + plays: Vec>, + /// Ω_q, rad/ns. + omega: Vec, + /// Each basis state's level of each qubit: `digit[i * n + j]`. + digit: Vec, + /// The static diagonal of H: anharmonicity and couplings, rad/ns. + static_diag: Vec, + /// The relaxation jumps: for each, its entries (row, column, value). + jumps: Vec>, + /// The diagonal of the dissipator: −½(D_i + D_j) less the dephasing, per (i, j). + damp: Vec, + /// ‖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 { + 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 = 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::>()?; + for (q, p) in qubits.iter().zip(¶ms) { + 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![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 = 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, + active: Vec>, +} + +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 { + let m = model(s, d)?; + plan_model(s, &m, set) +} + +fn plan_model(s: &Schedule, m: &Model, set: &Settings) -> Result { + 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 = 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 { + 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 { + 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 { + 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 = 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 = Vec::new(); + let mut sw = Sweep::new(n); + let stride: Vec = (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> = 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::>())); + } + 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 = (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::>())); + } + let all = read_out(m, &rho, &m.qubits); + let leakage: Vec = (0..n).map(|j| (0..dim).filter(|&b| m.digit[b * n + j] == 2).map(|b| rho[b * dim + b].re).sum::()).collect(); + let trace: f64 = (0..dim).map(|b| rho[b * dim + b].re).sum(); + let distinct = { + let mut q: Vec = 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 { + let (n, dim) = (m.qubits.len(), m.dim); + let pos: Vec = 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; diff --git a/wai/wai-quantum/src/quantum_pulse/tests.rs b/wai/wai-quantum/src/quantum_pulse/tests.rs new file mode 100644 index 000000000..2ddedfcfe --- /dev/null +++ b/wai/wai-quantum/src/quantum_pulse/tests.rs @@ -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, t2: Option) -> 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::>(), b.rho.iter().map(|x| (x.0.to_bits(), x.1.to_bits())).collect::>()); + 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::>(), [(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::(), 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::(); + 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"); +} -- 2.54.0 (Apple Git-157) From 2f44231e1a54d38beb8e15d38319e0517ea02bd0 Mon Sep 17 00:00:00 2001 From: Transaction Science Date: Fri, 30 Oct 2026 10:15:38 -0400 Subject: [PATCH 03/10] wai-quantum: pulse runs as jobs, their receipts binding the program, calibrations, device, target and integrator engine::pulse makes a pulse program a job of the method pulse, planned and run through catalog::plan and run::run like every job, so every host that runs jobs runs it: the device from a target's calibration snapshot and the request's pulse options over the defaults, each parameter saying where it came from; counts sampled from the captures' joint reading (into the program's bits) or the final state; the accuracy class bounded, with the proved truncation bound and the rounding estimate. The result's pulse.calibration holds the control program's digest, its calibrations', the schedule's and the device's, and the target snapshot's name, hash and calibration id; pulse.integrator the integrator and its step; pulse.density_hash the final state's bits. The job's receipt seals them with the result. engine::pulse::check holds a sealed pulse job to its request: the signature, the program's digest, and the request run again, each differing field named. engine::pulse::schedule draws the schedule for a view; tools() gives the pulse_schedule and pulse_check tools for both servers. J040 and J041 join the job layer's refusals; the verdict of a pulse run is its own (executed, bounded). schemas/pulse.v1.json is new (FILES grows from 22 to 23); the job request and result schemas gain the pulse options and the pulse result, and the accuracy its bound. Co-Authored-By: Claude Opus 5.5 --- wai/wai-quantum/docs/job-refusals.md | 2 + wai/wai-quantum/schemas/job-request.v1.json | 117 ++- wai/wai-quantum/schemas/job-result.v1.json | 249 +++++++ wai/wai-quantum/schemas/pulse.v1.json | 413 +++++++++++ wai/wai-quantum/src/engine/catalog.rs | 8 + wai/wai-quantum/src/engine/mod.rs | 9 + wai/wai-quantum/src/engine/pulse.rs | 753 ++++++++++++++++++++ wai/wai-quantum/src/engine/pulse/tests.rs | 195 +++++ wai/wai-quantum/src/engine/refusal.rs | 4 + wai/wai-quantum/src/engine/run.rs | 4 + wai/wai-quantum/src/engine/schema.rs | 7 +- wai/wai-quantum/src/engine/verdict.rs | 24 + 12 files changed, 1783 insertions(+), 2 deletions(-) create mode 100644 wai/wai-quantum/schemas/pulse.v1.json create mode 100644 wai/wai-quantum/src/engine/pulse.rs create mode 100644 wai/wai-quantum/src/engine/pulse/tests.rs diff --git a/wai/wai-quantum/docs/job-refusals.md b/wai/wai-quantum/docs/job-refusals.md index 2bb980397..e01c0c04d 100644 --- a/wai/wai-quantum/docs/job-refusals.md +++ b/wai/wai-quantum/docs/job-refusals.md @@ -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 `; 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. | diff --git a/wai/wai-quantum/schemas/job-request.v1.json b/wai/wai-quantum/schemas/job-request.v1.json index 7a9918088..8fc6d70c0 100644 --- a/wai/wai-quantum/schemas/job-request.v1.json +++ b/wai/wai-quantum/schemas/job-request.v1.json @@ -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": { diff --git a/wai/wai-quantum/schemas/job-result.v1.json b/wai/wai-quantum/schemas/job-result.v1.json index 26d716230..4c9f8917f 100644 --- a/wai/wai-quantum/schemas/job-result.v1.json +++ b/wai/wai-quantum/schemas/job-result.v1.json @@ -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" } } }, diff --git a/wai/wai-quantum/schemas/pulse.v1.json b/wai/wai-quantum/schemas/pulse.v1.json new file mode 100644 index 000000000..710be67b1 --- /dev/null +++ b/wai/wai-quantum/schemas/pulse.v1.json @@ -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" + } + } + } + } +} diff --git a/wai/wai-quantum/src/engine/catalog.rs b/wai/wai-quantum/src/engine/catalog.rs index 46be2166e..8e90b84a7 100644 --- a/wai/wai-quantum/src/engine/catalog.rs +++ b/wai/wai-quantum/src/engine/catalog.rs @@ -118,6 +118,7 @@ pub fn nominal_rate(method: &str) -> Option { "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 { + // 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")] diff --git a/wai/wai-quantum/src/engine/mod.rs b/wai/wai-quantum/src/engine/mod.rs index 1f0cbc353..1a71286a5 100644 --- a/wai/wai-quantum/src/engine/mod.rs +++ b/wai/wai-quantum/src/engine/mod.rs @@ -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")] diff --git a/wai/wai-quantum/src/engine/pulse.rs b/wai/wai-quantum/src/engine/pulse.rs new file mode 100644 index 000000000..9bcf867e0 --- /dev/null +++ b/wai/wai-quantum/src/engine/pulse.rs @@ -0,0 +1,753 @@ +//! 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; 7] = ["qasm", "method", "shots", "seed", "pulse", "target", "dry_run"]; +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 { + 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, + ports: Vec, + qubits: BTreeMap>, + couplings: Vec<(u32, u32, f64)>, +} + +fn number(v: &Value, at: &str) -> Result { + 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 { + 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 { + 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 = 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, + 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), 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, from_target: Option, default: Option| -> (Option, &'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, Option, Option) = { 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::>(), + "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::>().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)" + ))); + } + 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::>() }), + 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 { + 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 { + 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> = 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, Option>) = 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 = 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 = 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::>()), + "populations": out.populations.iter().enumerate().map(|(x, &pr)| (key(x, n), json!(pr))).collect::>(), + "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::>(), + "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 { + 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 = 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 { + 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 { + 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; diff --git a/wai/wai-quantum/src/engine/pulse/tests.rs b/wai/wai-quantum/src/engine/pulse/tests.rs new file mode 100644 index 000000000..4c581eba9 --- /dev/null +++ b/wai/wai-quantum/src/engine/pulse/tests.rs @@ -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, 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::() * 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"]); +} diff --git a/wai/wai-quantum/src/engine/refusal.rs b/wai/wai-quantum/src/engine/refusal.rs index 1d2873587..1cbbbf7b0 100644 --- a/wai/wai-quantum/src/engine/refusal.rs +++ b/wai/wai-quantum/src/engine/refusal.rs @@ -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 `; 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)."; diff --git a/wai/wai-quantum/src/engine/run.rs b/wai/wai-quantum/src/engine/run.rs index 6847ada8e..0941488ce 100644 --- a/wai/wai-quantum/src/engine/run.rs +++ b/wai/wai-quantum/src/engine/run.rs @@ -198,6 +198,10 @@ pub fn provenance(request: &Value, plan: &Plan, result: &Value, passes: Option) -> Result { + // 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) { diff --git a/wai/wai-quantum/src/engine/schema.rs b/wai/wai-quantum/src/engine/schema.rs index 609c78130..e60588aef 100644 --- a/wai/wai-quantum/src/engine/schema.rs +++ b/wai/wai-quantum/src/engine/schema.rs @@ -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, diff --git a/wai/wai-quantum/src/engine/verdict.rs b/wai/wai-quantum/src/engine/verdict.rs index 42343c1ff..aaa90d28b 100644 --- a/wai/wai-quantum/src/engine/verdict.rs +++ b/wai/wai-quantum/src/engine/verdict.rs @@ -158,9 +158,11 @@ pub fn for_tool(tool: &str, args: &Value, answer: &Value) -> Option { 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 { -- 2.54.0 (Apple Git-157) From 290c170d06a3dff0c3649a2c2bd322ad810f24d3 Mon Sep 17 00:00:00 2001 From: Transaction Science Date: Fri, 30 Oct 2026 10:39:00 -0400 Subject: [PATCH 04/10] wai-quantum-cli, wai-quantum-service: pulse runs, schedules and checks on the command line, both MCP servers and the job service A pulse program runs as a job of the method pulse everywhere jobs run: wai-quantum run --method pulse --pulse OPTIONS (and plan), the MCP tools run_quantum_circuit and plan_quantum_circuit on both servers (their method and pulse options in the input schema), and the job service's /v1/jobs and /v1/run, through its queue, workers and receipts. A pulse run accepts the verdict hosts ask for and carries its own, and a run's printed result shows its integrator, captures, device and what its receipt binds. wai-quantum pulse schedule draws a program's frames as timelines with their plays, captures and barriers, its gate calls and waveforms; wai-quantum pulse check holds a sealed pulse job to its request by running it again. Both MCP servers serve pulse_schedule and pulse_check, and the service POST /v1/pulse/schedule and /v1/pulse/check, a check held to the work a call there takes. The API description, both smoke scripts, the committed tool lists and the agent docs are regenerated; the Simplified Chinese help summarises the new command. Co-Authored-By: Claude Opus 5.5 --- wai/wai-quantum-cli/docs/llms-full.txt | 194 +- wai/wai-quantum-cli/docs/llms.txt | 4 + wai/wai-quantum-cli/docs/local-mcp.json | 3755 ++++++++++++- wai/wai-quantum-cli/docs/service-mcp.json | 4874 ++++++++++++++++- wai/wai-quantum-cli/locales/zh-CN.json | 1 + .../skills/wai-quantum/reference/tools.md | 4 + wai/wai-quantum-cli/smoke.sh | 31 + wai/wai-quantum-cli/src/main.rs | 21 +- wai/wai-quantum-cli/src/mcp.rs | 24 +- wai/wai-quantum-cli/src/pulse.rs | 263 + wai/wai-quantum-service/smoke.sh | 19 +- wai/wai-quantum-service/src/analysis.rs | 31 + wai/wai-quantum-service/src/discovery.rs | 26 +- wai/wai-quantum-service/src/http.rs | 32 + wai/wai-quantum-service/src/mcp.rs | 43 +- wai/wai-quantum/src/engine/pulse.rs | 6 +- wai/wai-quantum/src/engine/run.rs | 3 +- 17 files changed, 9212 insertions(+), 119 deletions(-) create mode 100644 wai/wai-quantum-cli/src/pulse.rs diff --git a/wai/wai-quantum-cli/docs/llms-full.txt b/wai/wai-quantum-cli/docs/llms-full.txt index c161abaf7..401f80d03 100644 --- a/wai/wai-quantum-cli/docs/llms-full.txt +++ b/wai/wai-quantum-cli/docs/llms-full.txt @@ -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 @@ -1008,6 +1047,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 +1078,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 +1401,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 +1537,10 @@ Options: pea, pec, cdr, vd or readout, or its options as JSON ({"method": "pea", "factors": [1, 1.5, 2]}) + --pulse + 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 +1667,10 @@ Options: pea, pec, cdr, vd or readout, or its options as JSON ({"method": "pea", "factors": [1, 1.5, 2]}) + --pulse + 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 +2296,10 @@ Options: pea, pec, cdr, vd or readout, or its options as JSON ({"method": "pea", "factors": [1, 1.5, 2]}) + --pulse + 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 +4827,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] + +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 + 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 + 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 + 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 + The pulse options, as JSON or a file of it (only dt_ns and ports matter here) + + --lang + 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 + 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 + 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 + 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 + Sign with the key in this file (64 hex digits) + + --lang + 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 diff --git a/wai/wai-quantum-cli/docs/llms.txt b/wai/wai-quantum-cli/docs/llms.txt index 6b1f92e4b..996556f9f 100644 --- a/wai/wai-quantum-cli/docs/llms.txt +++ b/wai/wai-quantum-cli/docs/llms.txt @@ -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 diff --git a/wai/wai-quantum-cli/docs/local-mcp.json b/wai/wai-quantum-cli/docs/local-mcp.json index 3f7297fe6..8416d4036 100644 --- a/wai/wai-quantum-cli/docs/local-mcp.json +++ b/wai/wai-quantum-cli/docs/local-mcp.json @@ -34,7 +34,7 @@ }, "method": { "type": "string", - "description": "auto (the default: the strongest method the program admits), or one of list_methods' names." + "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", @@ -356,6 +356,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 + } + } + } + } + } } } }, @@ -448,7 +563,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", @@ -1002,6 +1117,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 + } + } + } + } + } } } }, @@ -1123,6 +1353,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" } } }, @@ -2636,6 +2884,237 @@ } } } + }, + "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" + } + } + } } } }, @@ -2910,7 +3389,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", @@ -3464,6 +3943,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 + } + } + } + } + } } } }, @@ -3583,6 +4177,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" } } }, @@ -5096,6 +5708,237 @@ } } } + }, + "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" + } + } + } } } }, @@ -5979,7 +6822,7 @@ }, "method": { "type": "string", - "description": "auto (the default: the strongest method the program admits), or one of list_methods' names." + "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", @@ -6301,6 +7144,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 + } + } + } + } + } } } }, @@ -25828,7 +26786,7 @@ }, "method": { "type": "string", - "description": "auto (the default: the strongest method the program admits), or one of list_methods' names." + "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", @@ -26150,6 +27108,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 + } + } + } + } + } } } }, @@ -26242,7 +27315,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", @@ -26796,6 +27869,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 + } + } + } + } + } } } }, @@ -26917,6 +28105,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" } } }, @@ -28430,6 +29636,237 @@ } } } + }, + "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" + } + } + } } } }, @@ -28704,7 +30141,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", @@ -29258,6 +30695,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 + } + } + } + } + } } } }, @@ -29377,6 +30929,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" } } }, @@ -30890,6 +32460,237 @@ } } } + }, + "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" + } + } + } } } }, @@ -33145,6 +34946,1214 @@ ] } }, + { + "name": "pulse_schedule", + "title": "Read a pulse program into its schedule", + "description": "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\".", + "inputSchema": { + "type": "object", + "required": [ + "qasm" + ], + "properties": { + "qasm": { + "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." + }, + "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 + } + } + } + } + } + } + } + }, + "annotations": { + "readOnlyHint": true, + "idempotentHint": true, + "openWorldHint": false + }, + "outputSchema": { + "$id": "wai-quantum://schemas/pulse_schedule/1", + "type": "object", + "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.", + "anyOf": [ + { + "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": { + "description": "A schedule or a check, with its receipt.", + "anyOf": [ + { + "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": { + "title": "wai.quantum.receipt/2", + "description": "A result's receipt: an Ed25519 signature, by the key it names, over digest = BLAKE3(\"wai:quantum-service-receipt\\x02\" ‖ len ‖ request ‖ len ‖ result), each of request and result in its RFC 8785 canonical text (the result without its receipt member), each length 8 bytes big-endian. Whatever members the request and the result hold are sealed, the result's engine and schema among them. The first receipts (no canon member, $defs/first) signed serde_json's own text under the domain ending \\x01; they still verify.", + "type": "object", + "required": [ + "alg", + "canon", + "key", + "digest", + "signature" + ], + "properties": { + "alg": { + "const": "ed25519" + }, + "canon": { + "const": "rfc8785", + "description": "The canonical form the digest is taken over." + }, + "key": { + "type": "string", + "minLength": 64, + "maxLength": 64, + "description": "The signing key, 32 bytes in hex." + }, + "digest": { + "type": "string", + "minLength": 64, + "maxLength": 64, + "description": "The digest signed, 32 bytes in hex." + }, + "signature": { + "type": "string", + "minLength": 128, + "maxLength": 128, + "description": "The signature, 64 bytes in hex." + } + } + } + } + }, + { + "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": { + "title": "wai.quantum.receipt/2", + "description": "A result's receipt: an Ed25519 signature, by the key it names, over digest = BLAKE3(\"wai:quantum-service-receipt\\x02\" ‖ len ‖ request ‖ len ‖ result), each of request and result in its RFC 8785 canonical text (the result without its receipt member), each length 8 bytes big-endian. Whatever members the request and the result hold are sealed, the result's engine and schema among them. The first receipts (no canon member, $defs/first) signed serde_json's own text under the domain ending \\x01; they still verify.", + "type": "object", + "required": [ + "alg", + "canon", + "key", + "digest", + "signature" + ], + "properties": { + "alg": { + "const": "ed25519" + }, + "canon": { + "const": "rfc8785", + "description": "The canonical form the digest is taken over." + }, + "key": { + "type": "string", + "minLength": 64, + "maxLength": 64, + "description": "The signing key, 32 bytes in hex." + }, + "digest": { + "type": "string", + "minLength": 64, + "maxLength": 64, + "description": "The digest signed, 32 bytes in hex." + }, + "signature": { + "type": "string", + "minLength": 128, + "maxLength": 128, + "description": "The signature, 64 bytes in hex." + } + } + } + } + } + ] + } + } + }, + { + "type": "object", + "description": "A refusal as data: the message, every code in it (the last is the code), its outcome and meaning as the code's table gives them. A host that refuses for its own reasons gives the error alone.", + "required": [ + "error" + ], + "properties": { + "error": { + "type": "string" + }, + "message": { + "type": "string" + }, + "code": { + "type": [ + "string", + "null" + ] + }, + "codes": { + "type": "array", + "items": { + "type": "string" + } + }, + "outcome": { + "enum": [ + "refused", + "retry", + "failed", + "lowered", + "carried", + "preserved", + "warned", + "dropped", + "defaulted" + ] + }, + "meaning": { + "type": "string" + } + } + } + ] + } + }, + { + "name": "pulse_check", + "title": "Check a pulse run by running it again", + "description": "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.", + "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." + } + } + }, + "annotations": { + "readOnlyHint": true, + "idempotentHint": true, + "openWorldHint": false + }, + "outputSchema": { + "$id": "wai-quantum://schemas/pulse_check/1", + "type": "object", + "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.", + "anyOf": [ + { + "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": { + "description": "A schedule or a check, with its receipt.", + "anyOf": [ + { + "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": { + "title": "wai.quantum.receipt/2", + "description": "A result's receipt: an Ed25519 signature, by the key it names, over digest = BLAKE3(\"wai:quantum-service-receipt\\x02\" ‖ len ‖ request ‖ len ‖ result), each of request and result in its RFC 8785 canonical text (the result without its receipt member), each length 8 bytes big-endian. Whatever members the request and the result hold are sealed, the result's engine and schema among them. The first receipts (no canon member, $defs/first) signed serde_json's own text under the domain ending \\x01; they still verify.", + "type": "object", + "required": [ + "alg", + "canon", + "key", + "digest", + "signature" + ], + "properties": { + "alg": { + "const": "ed25519" + }, + "canon": { + "const": "rfc8785", + "description": "The canonical form the digest is taken over." + }, + "key": { + "type": "string", + "minLength": 64, + "maxLength": 64, + "description": "The signing key, 32 bytes in hex." + }, + "digest": { + "type": "string", + "minLength": 64, + "maxLength": 64, + "description": "The digest signed, 32 bytes in hex." + }, + "signature": { + "type": "string", + "minLength": 128, + "maxLength": 128, + "description": "The signature, 64 bytes in hex." + } + } + } + } + }, + { + "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": { + "title": "wai.quantum.receipt/2", + "description": "A result's receipt: an Ed25519 signature, by the key it names, over digest = BLAKE3(\"wai:quantum-service-receipt\\x02\" ‖ len ‖ request ‖ len ‖ result), each of request and result in its RFC 8785 canonical text (the result without its receipt member), each length 8 bytes big-endian. Whatever members the request and the result hold are sealed, the result's engine and schema among them. The first receipts (no canon member, $defs/first) signed serde_json's own text under the domain ending \\x01; they still verify.", + "type": "object", + "required": [ + "alg", + "canon", + "key", + "digest", + "signature" + ], + "properties": { + "alg": { + "const": "ed25519" + }, + "canon": { + "const": "rfc8785", + "description": "The canonical form the digest is taken over." + }, + "key": { + "type": "string", + "minLength": 64, + "maxLength": 64, + "description": "The signing key, 32 bytes in hex." + }, + "digest": { + "type": "string", + "minLength": 64, + "maxLength": 64, + "description": "The digest signed, 32 bytes in hex." + }, + "signature": { + "type": "string", + "minLength": 128, + "maxLength": 128, + "description": "The signature, 64 bytes in hex." + } + } + } + } + } + ] + } + } + }, + { + "type": "object", + "description": "A refusal as data: the message, every code in it (the last is the code), its outcome and meaning as the code's table gives them. A host that refuses for its own reasons gives the error alone.", + "required": [ + "error" + ], + "properties": { + "error": { + "type": "string" + }, + "message": { + "type": "string" + }, + "code": { + "type": [ + "string", + "null" + ] + }, + "codes": { + "type": "array", + "items": { + "type": "string" + } + }, + "outcome": { + "enum": [ + "refused", + "retry", + "failed", + "lowered", + "carried", + "preserved", + "warned", + "dropped", + "defaulted" + ] + }, + "meaning": { + "type": "string" + } + } + } + ] + } + }, { "name": "providers_list", "title": "List the person's quantum providers", @@ -33494,7 +36503,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", @@ -34048,6 +37057,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 + } + } + } + } + } } } }, @@ -34169,6 +37293,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" } } }, @@ -35682,6 +38824,237 @@ } } } + }, + "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" + } + } + } } } }, @@ -35956,7 +39329,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", @@ -36510,6 +39883,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 + } + } + } + } + } } } }, @@ -36629,6 +40117,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" } } }, @@ -38142,6 +41648,237 @@ } } } + }, + "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" + } + } + } } } }, diff --git a/wai/wai-quantum-cli/docs/service-mcp.json b/wai/wai-quantum-cli/docs/service-mcp.json index 3d30ab94e..287f33231 100644 --- a/wai/wai-quantum-cli/docs/service-mcp.json +++ b/wai/wai-quantum-cli/docs/service-mcp.json @@ -70,9 +70,10 @@ "cutrank", "cutrank_single", "density_matrix", - "trajectories" + "trajectories", + "pulse" ], - "description": "Leave as auto unless you need a particular method." + "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", @@ -352,6 +353,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 + } + } + } + } + } } } }, @@ -452,7 +568,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", @@ -1006,6 +1122,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 + } + } + } + } + } } } }, @@ -1127,6 +1358,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" } } }, @@ -2640,6 +2889,237 @@ } } } + }, + "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" + } + } + } } } }, @@ -2914,7 +3394,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", @@ -3468,6 +3948,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 + } + } + } + } + } } } }, @@ -3587,6 +4182,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" } } }, @@ -5100,6 +5713,237 @@ } } } + }, + "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" + } + } + } } } }, @@ -7372,9 +8216,10 @@ "cutrank", "cutrank_single", "density_matrix", - "trajectories" + "trajectories", + "pulse" ], - "description": "Leave as auto unless you need a particular method." + "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", @@ -7655,6 +8500,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 + } + } + } + } + } + }, "programs": { "type": "array", "minItems": 1, @@ -7784,7 +8744,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", @@ -8338,6 +9298,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 + } + } + } + } + } } } }, @@ -8459,6 +9534,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" } } }, @@ -9972,6 +11065,237 @@ } } } + }, + "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" + } + } + } } } }, @@ -10246,7 +11570,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", @@ -10800,6 +12124,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 + } + } + } + } + } } } }, @@ -10919,6 +12358,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" } } }, @@ -12432,6 +13889,237 @@ } } } + }, + "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" + } + } + } } } }, @@ -14704,9 +16392,10 @@ "cutrank", "cutrank_single", "density_matrix", - "trajectories" + "trajectories", + "pulse" ], - "description": "Leave as auto unless you need a particular method." + "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", @@ -14987,6 +16676,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 + } + } + } + } + } + }, "programs": { "type": "array", "minItems": 1, @@ -16120,7 +17924,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", @@ -16674,6 +18478,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 + } + } + } + } + } } } }, @@ -16795,6 +18714,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" } } }, @@ -18308,6 +20245,237 @@ } } } + }, + "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" + } + } + } } } }, @@ -18582,7 +20750,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", @@ -19136,6 +21304,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 + } + } + } + } + } } } }, @@ -19255,6 +21538,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" } } }, @@ -20768,6 +23069,237 @@ } } } + }, + "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" + } + } + } } } }, @@ -34607,85 +37139,120 @@ } } }, - "mitigate": { - "description": "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.", - "anyOf": [ - { - "type": "string", - "enum": [ - "zne", - "pea", - "pec", - "cdr", - "vd", - "readout" - ] + "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)." }, - { - "type": "object", - "required": [ - "method" + "levels": { + "type": "integer", + "enum": [ + 2, + 3 ], - "properties": { - "method": { - "type": "string", - "enum": [ - "zne", - "pea", - "pec", - "cdr", - "vd", - "readout" - ] - }, - "shots": { - "type": "integer", - "minimum": 1, - "maximum": 1000000, - "description": "Shots per circuit (default 10000; 2000 per sampled circuit for pec)." - }, - "seed": { - "type": "integer", - "minimum": 0 - }, - "factors": { - "type": "array", - "items": { - "type": "number" + "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." }, - "description": "zne: odd whole numbers (default 1, 3, 5); pea: factors from 1 to 10 (default 1, 1.5, 2)." - }, - "extrapolation": { - "type": "string", - "enum": [ - "richardson", - "linear", - "exponential" - ] - }, - "samples": { - "type": "integer", - "minimum": 2, - "description": "pec: sampled circuits (default 200)." - }, - "training": { - "type": "integer", - "minimum": 3, - "description": "cdr: training circuits (default 16)." - }, - "keep": { - "type": "integer", - "minimum": 0, - "description": "cdr: non-Clifford gates each training circuit keeps (default 2, and always one fewer than the program has)." - }, - "learn_shots": { - "type": "integer", - "minimum": 100, - "description": "pea, pec: shots per depth of the cycle benchmarking that learns the noise (default 60000)." + "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 + } } } } - ] + } }, "seed": { "type": "integer", @@ -34885,6 +37452,86 @@ }, "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": { + "description": "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.", + "anyOf": [ + { + "type": "string", + "enum": [ + "zne", + "pea", + "pec", + "cdr", + "vd", + "readout" + ] + }, + { + "type": "object", + "required": [ + "method" + ], + "properties": { + "method": { + "type": "string", + "enum": [ + "zne", + "pea", + "pec", + "cdr", + "vd", + "readout" + ] + }, + "shots": { + "type": "integer", + "minimum": 1, + "maximum": 1000000, + "description": "Shots per circuit (default 10000; 2000 per sampled circuit for pec)." + }, + "seed": { + "type": "integer", + "minimum": 0 + }, + "factors": { + "type": "array", + "items": { + "type": "number" + }, + "description": "zne: odd whole numbers (default 1, 3, 5); pea: factors from 1 to 10 (default 1, 1.5, 2)." + }, + "extrapolation": { + "type": "string", + "enum": [ + "richardson", + "linear", + "exponential" + ] + }, + "samples": { + "type": "integer", + "minimum": 2, + "description": "pec: sampled circuits (default 200)." + }, + "training": { + "type": "integer", + "minimum": 3, + "description": "cdr: training circuits (default 16)." + }, + "keep": { + "type": "integer", + "minimum": 0, + "description": "cdr: non-Clifford gates each training circuit keeps (default 2, and always one fewer than the program has)." + }, + "learn_shots": { + "type": "integer", + "minimum": 100, + "description": "pea, pec: shots per depth of the cycle benchmarking that learns the noise (default 60000)." + } + } + } + ] } } }, @@ -34985,7 +37632,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", @@ -35539,6 +38186,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 + } + } + } + } + } } } }, @@ -35660,6 +38422,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" } } }, @@ -37173,6 +39953,237 @@ } } } + }, + "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" + } + } + } } } }, @@ -37447,7 +40458,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", @@ -38001,6 +41012,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 + } + } + } + } + } } } }, @@ -38120,6 +41246,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" } } }, @@ -39633,6 +42777,237 @@ } } } + }, + "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" + } + } + } } } }, @@ -40089,7 +43464,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", @@ -40643,6 +44018,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 + } + } + } + } + } } } } @@ -42834,6 +46324,1214 @@ } ] } + }, + { + "name": "pulse_schedule", + "title": "Read a pulse program into its schedule", + "description": "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\".", + "inputSchema": { + "type": "object", + "required": [ + "qasm" + ], + "properties": { + "qasm": { + "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." + }, + "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 + } + } + } + } + } + } + } + }, + "annotations": { + "readOnlyHint": true, + "idempotentHint": true, + "openWorldHint": false + }, + "outputSchema": { + "$id": "wai-quantum://schemas/pulse_schedule/1", + "type": "object", + "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.", + "anyOf": [ + { + "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": { + "description": "A schedule or a check, with its receipt.", + "anyOf": [ + { + "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": { + "title": "wai.quantum.receipt/2", + "description": "A result's receipt: an Ed25519 signature, by the key it names, over digest = BLAKE3(\"wai:quantum-service-receipt\\x02\" ‖ len ‖ request ‖ len ‖ result), each of request and result in its RFC 8785 canonical text (the result without its receipt member), each length 8 bytes big-endian. Whatever members the request and the result hold are sealed, the result's engine and schema among them. The first receipts (no canon member, $defs/first) signed serde_json's own text under the domain ending \\x01; they still verify.", + "type": "object", + "required": [ + "alg", + "canon", + "key", + "digest", + "signature" + ], + "properties": { + "alg": { + "const": "ed25519" + }, + "canon": { + "const": "rfc8785", + "description": "The canonical form the digest is taken over." + }, + "key": { + "type": "string", + "minLength": 64, + "maxLength": 64, + "description": "The signing key, 32 bytes in hex." + }, + "digest": { + "type": "string", + "minLength": 64, + "maxLength": 64, + "description": "The digest signed, 32 bytes in hex." + }, + "signature": { + "type": "string", + "minLength": 128, + "maxLength": 128, + "description": "The signature, 64 bytes in hex." + } + } + } + } + }, + { + "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": { + "title": "wai.quantum.receipt/2", + "description": "A result's receipt: an Ed25519 signature, by the key it names, over digest = BLAKE3(\"wai:quantum-service-receipt\\x02\" ‖ len ‖ request ‖ len ‖ result), each of request and result in its RFC 8785 canonical text (the result without its receipt member), each length 8 bytes big-endian. Whatever members the request and the result hold are sealed, the result's engine and schema among them. The first receipts (no canon member, $defs/first) signed serde_json's own text under the domain ending \\x01; they still verify.", + "type": "object", + "required": [ + "alg", + "canon", + "key", + "digest", + "signature" + ], + "properties": { + "alg": { + "const": "ed25519" + }, + "canon": { + "const": "rfc8785", + "description": "The canonical form the digest is taken over." + }, + "key": { + "type": "string", + "minLength": 64, + "maxLength": 64, + "description": "The signing key, 32 bytes in hex." + }, + "digest": { + "type": "string", + "minLength": 64, + "maxLength": 64, + "description": "The digest signed, 32 bytes in hex." + }, + "signature": { + "type": "string", + "minLength": 128, + "maxLength": 128, + "description": "The signature, 64 bytes in hex." + } + } + } + } + } + ] + } + } + }, + { + "type": "object", + "description": "A refusal as data: the message, every code in it (the last is the code), its outcome and meaning as the code's table gives them. A host that refuses for its own reasons gives the error alone.", + "required": [ + "error" + ], + "properties": { + "error": { + "type": "string" + }, + "message": { + "type": "string" + }, + "code": { + "type": [ + "string", + "null" + ] + }, + "codes": { + "type": "array", + "items": { + "type": "string" + } + }, + "outcome": { + "enum": [ + "refused", + "retry", + "failed", + "lowered", + "carried", + "preserved", + "warned", + "dropped", + "defaulted" + ] + }, + "meaning": { + "type": "string" + } + } + } + ] + } + }, + { + "name": "pulse_check", + "title": "Check a pulse run by running it again", + "description": "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.", + "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." + } + } + }, + "annotations": { + "readOnlyHint": true, + "idempotentHint": true, + "openWorldHint": false + }, + "outputSchema": { + "$id": "wai-quantum://schemas/pulse_check/1", + "type": "object", + "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.", + "anyOf": [ + { + "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": { + "description": "A schedule or a check, with its receipt.", + "anyOf": [ + { + "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": { + "title": "wai.quantum.receipt/2", + "description": "A result's receipt: an Ed25519 signature, by the key it names, over digest = BLAKE3(\"wai:quantum-service-receipt\\x02\" ‖ len ‖ request ‖ len ‖ result), each of request and result in its RFC 8785 canonical text (the result without its receipt member), each length 8 bytes big-endian. Whatever members the request and the result hold are sealed, the result's engine and schema among them. The first receipts (no canon member, $defs/first) signed serde_json's own text under the domain ending \\x01; they still verify.", + "type": "object", + "required": [ + "alg", + "canon", + "key", + "digest", + "signature" + ], + "properties": { + "alg": { + "const": "ed25519" + }, + "canon": { + "const": "rfc8785", + "description": "The canonical form the digest is taken over." + }, + "key": { + "type": "string", + "minLength": 64, + "maxLength": 64, + "description": "The signing key, 32 bytes in hex." + }, + "digest": { + "type": "string", + "minLength": 64, + "maxLength": 64, + "description": "The digest signed, 32 bytes in hex." + }, + "signature": { + "type": "string", + "minLength": 128, + "maxLength": 128, + "description": "The signature, 64 bytes in hex." + } + } + } + } + }, + { + "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": { + "title": "wai.quantum.receipt/2", + "description": "A result's receipt: an Ed25519 signature, by the key it names, over digest = BLAKE3(\"wai:quantum-service-receipt\\x02\" ‖ len ‖ request ‖ len ‖ result), each of request and result in its RFC 8785 canonical text (the result without its receipt member), each length 8 bytes big-endian. Whatever members the request and the result hold are sealed, the result's engine and schema among them. The first receipts (no canon member, $defs/first) signed serde_json's own text under the domain ending \\x01; they still verify.", + "type": "object", + "required": [ + "alg", + "canon", + "key", + "digest", + "signature" + ], + "properties": { + "alg": { + "const": "ed25519" + }, + "canon": { + "const": "rfc8785", + "description": "The canonical form the digest is taken over." + }, + "key": { + "type": "string", + "minLength": 64, + "maxLength": 64, + "description": "The signing key, 32 bytes in hex." + }, + "digest": { + "type": "string", + "minLength": 64, + "maxLength": 64, + "description": "The digest signed, 32 bytes in hex." + }, + "signature": { + "type": "string", + "minLength": 128, + "maxLength": 128, + "description": "The signature, 64 bytes in hex." + } + } + } + } + } + ] + } + } + }, + { + "type": "object", + "description": "A refusal as data: the message, every code in it (the last is the code), its outcome and meaning as the code's table gives them. A host that refuses for its own reasons gives the error alone.", + "required": [ + "error" + ], + "properties": { + "error": { + "type": "string" + }, + "message": { + "type": "string" + }, + "code": { + "type": [ + "string", + "null" + ] + }, + "codes": { + "type": "array", + "items": { + "type": "string" + } + }, + "outcome": { + "enum": [ + "refused", + "retry", + "failed", + "lowered", + "carried", + "preserved", + "warned", + "dropped", + "defaulted" + ] + }, + "meaning": { + "type": "string" + } + } + } + ] + } } ] } diff --git a/wai/wai-quantum-cli/locales/zh-CN.json b/wai/wai-quantum-cli/locales/zh-CN.json index b3d60a03a..d97c1f501 100644 --- a/wai/wai-quantum-cli/locales/zh-CN.json +++ b/wai/wai-quantum-cli/locales/zh-CN.json @@ -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`)" }, diff --git a/wai/wai-quantum-cli/skills/wai-quantum/reference/tools.md b/wai/wai-quantum-cli/skills/wai-quantum/reference/tools.md index dc4179a38..7e479769b 100644 --- a/wai/wai-quantum-cli/skills/wai-quantum/reference/tools.md +++ b/wai/wai-quantum-cli/skills/wai-quantum/reference/tools.md @@ -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 diff --git a/wai/wai-quantum-cli/smoke.sh b/wai/wai-quantum-cli/smoke.sh index 2d8910809..5f94c4401 100644 --- a/wai/wai-quantum-cli/smoke.sh +++ b/wai/wai-quantum-cli/smoke.sh @@ -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" diff --git a/wai/wai-quantum-cli/src/main.rs b/wai/wai-quantum-cli/src/main.rs index a3c994f82..b862f319b 100644 --- a/wai/wai-quantum-cli/src/main.rs +++ b/wai/wai-quantum-cli/src/main.rs @@ -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, + /// 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, } /// `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 = 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(), ) } diff --git a/wai/wai-quantum-cli/src/mcp.rs b/wai/wai-quantum-cli/src/mcp.rs index e8d7a2d42..dac5659e7 100644 --- a/wai/wai-quantum-cli/src/mcp.rs +++ b/wai/wai-quantum-cli/src/mcp.rs @@ -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::>(), "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); diff --git a/wai/wai-quantum-cli/src/pulse.rs b/wai/wai-quantum-cli/src/pulse.rs new file mode 100644 index 000000000..6e89fadae --- /dev/null +++ b/wai/wai-quantum-cli/src/pulse.rs @@ -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, + /// 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, + /// 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, + #[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 { + 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!( + "{: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::(), + 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 = c["qubits"].as_array().into_iter().flatten().map(|q| format!("${q}")).collect(); + let ps: Vec = 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::>().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::>().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 { + 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()); + } +} diff --git a/wai/wai-quantum-service/smoke.sh b/wai/wai-quantum-service/smoke.sh index ebf3f3c6a..60f4e3c71 100644 --- a/wai/wai-quantum-service/smoke.sh +++ b/wai/wai-quantum-service/smoke.sh @@ -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 @@ -399,6 +399,23 @@ d=json.load(open(sys.argv[1])); m=d["result"]["mitigation"]; o=m["observables"][ assert d["status"]=="done" and m["method"]=="zne" and abs(o["mitigated"]-o["ideal"]) < abs(o["raw"]-o["ideal"]), m print("mitigated job: ok, raw", round(o["raw"], 4), "mitigated", round(o["mitigated"], 4), "ideal", round(o["ideal"], 4))' "$tmp/mitigated.out" +# A pulse program: its schedule, its run as a job (method pulse, the error +# bounded, the receipt binding the program), and the job checked by running +# it again. +py 'import json,sys +p = "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" +json.dump({"qasm": p}, open(sys.argv[1], "w")); json.dump({"qasm": p, "method": "pulse", "shots": 200}, open(sys.argv[2], "w"))' "$tmp/pulse-schedule.json" "$tmp/pulse-job.json" +post /v1/pulse/schedule "$tmp/pulse-schedule.json" > "$tmp/pulse-schedule.out" +py 'import json,sys; r=json.load(open(sys.argv[1]))["result"]; assert r["duration"]==164 and [i["op"] for i in r["instructions"]]==["play","capture"] and r["receipt"], r; print("pulse schedule: ok,", r["duration"], "samples")' "$tmp/pulse-schedule.out" +run "$tmp/pulse-job.json" "$tmp/pulse-job.out" +py 'import json,sys +d=json.load(open(sys.argv[1])); r=d["result"] +assert d["status"]=="done" and r["method"]=="pulse" and r["accuracy"]["class"]=="bounded" and r["accuracy"]["bound"] <= 1e-6 and len(r["pulse"]["calibration"]["program"])==64, d +json.dump({"request": d["request"], "result": r}, open(sys.argv[2], "w")) +print("pulse job: ok, P(1)", round(r["pulse"]["distribution"]["1"], 6), "within", r["accuracy"]["bound"])' "$tmp/pulse-job.out" "$tmp/pulse-check.json" +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, r; print("pulse check: ok, reproduced bit for bit")' "$tmp/pulse-check.out" + # An estimate: the program's counts, the frontier, each estimate sealed with # the service's key in the resource-estimate receipt family, POST /v1/verify # checking the seals and re-running it, and one too fine sent to the command diff --git a/wai/wai-quantum-service/src/analysis.rs b/wai/wai-quantum-service/src/analysis.rs index f99ba135e..7dcf4be0d 100644 --- a/wai/wai-quantum-service/src/analysis.rs +++ b/wai/wai-quantum-service/src/analysis.rs @@ -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(app: &App, who: &str, work: F) -> Result diff --git a/wai/wai-quantum-service/src/discovery.rs b/wai/wai-quantum-service/src/discovery.rs index 253631f5b..9e1b5868e 100644 --- a/wai/wai-quantum-service/src/discovery.rs +++ b/wai/wai-quantum-service/src/discovery.rs @@ -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"); diff --git a/wai/wai-quantum-service/src/http.rs b/wai/wai-quantum-service/src/http.rs index 7575e2d2b..9e2a743b7 100644 --- a/wai/wai-quantum-service/src/http.rs +++ b/wai/wai-quantum-service/src/http.rs @@ -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 { + 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, 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, 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, ConnectInfo(p): ConnectInfo, h: HeaderMap, b| qec_route(a, p, h, b, "code"))) .route("/v1/qec/decode", post(|State(a): State, ConnectInfo(p): ConnectInfo, h: HeaderMap, b| qec_route(a, p, h, b, "decode"))) .route("/v1/qec/threshold", post(|State(a): State, ConnectInfo(p): ConnectInfo, h: HeaderMap, b| qec_route(a, p, h, b, "threshold"))) + .route("/v1/pulse/schedule", post(|State(a): State, ConnectInfo(p): ConnectInfo, h: HeaderMap, b| pulse_route(a, p, h, b, "schedule"))) + .route("/v1/pulse/check", post(|State(a): State, ConnectInfo(p): ConnectInfo, h: HeaderMap, b| pulse_route(a, p, h, b, "check"))) .route("/v1/estimate", post(estimate_route)) .route("/v1/repair", post(|State(a): State, ConnectInfo(p): ConnectInfo, h: HeaderMap, b| analysis(a, p, h, b, crate::analysis::Check::Repair))) .route("/v1/examples", get(examples)) diff --git a/wai/wai-quantum-service/src/mcp.rs b/wai/wai-quantum-service/src/mcp.rs index a4a0cc72f..7da681260 100644 --- a/wai/wai-quantum-service/src/mcp.rs +++ b/wai/wai-quantum-service/src/mcp.rs @@ -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 = 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"]; diff --git a/wai/wai-quantum/src/engine/pulse.rs b/wai/wai-quantum/src/engine/pulse.rs index 9bcf867e0..fa493aad8 100644 --- a/wai/wai-quantum/src/engine/pulse.rs +++ b/wai/wai-quantum/src/engine/pulse.rs @@ -61,7 +61,7 @@ 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; 7] = ["qasm", "method", "shots", "seed", "pulse", "target", "dry_run"]; +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"]; @@ -387,6 +387,10 @@ fn prepare(v: &Value) -> Result<(String, Prepared), String> { "\"{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() { diff --git a/wai/wai-quantum/src/engine/run.rs b/wai/wai-quantum/src/engine/run.rs index 0941488ce..b58d98e2a 100644 --- a/wai/wai-quantum/src/engine/run.rs +++ b/wai/wai-quantum/src/engine/run.rs @@ -121,7 +121,8 @@ pub fn run(request: &Value, plan: &Plan) -> Result { fn run_noting(request: &Value, plan: &Plan, passes: &mut Option) -> Result { 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); -- 2.54.0 (Apple Git-157) From 074f3282862d13cf79143d3edd9f21c9b3505201 Mon Sep 17 00:00:00 2001 From: Transaction Science Date: Fri, 30 Oct 2026 10:46:15 -0400 Subject: [PATCH 05/10] wai-web, CI: pulse calls in the page, and the pulse layer's bits held on every platform QuantumEngine.pulse gives the page the schedule and check calls, sealed with the page's key; a pulse run is the page's run with method pulse. A native test and a wasm32 test run the same pulse job to one pinned digest of its final density matrix, the engine's own; wai-web-wasm.yml's pins move from 26 to 27 (native) and from 18 to 19 (wasm32). wai-quantum-pulse.yml runs the pulse reader's, the simulator's and the pulse jobs' 26 tests by name in a release build on Linux, builds each feature set that holds them without warnings, runs the pinned digests in debug and release builds on arm64 Linux and on macOS, and runs the 26 tests under wasm32-wasip2 by wasmtime. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/wai-quantum-pulse.yml | 201 ++++++++++++++++++++++++ .github/workflows/wai-web-wasm.yml | 14 +- wai/wai-web/src/quantum_engine.rs | 55 +++++++ 3 files changed, 264 insertions(+), 6 deletions(-) create mode 100644 .github/workflows/wai-quantum-pulse.yml diff --git a/.github/workflows/wai-quantum-pulse.yml b/.github/workflows/wai-quantum-pulse.yml new file mode 100644 index 000000000..c4021b295 --- /dev/null +++ b/.github/workflows/wai-quantum-pulse.yml @@ -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; } diff --git a/.github/workflows/wai-web-wasm.yml b/.github/workflows/wai-web-wasm.yml index 1cb33706a..7c8dbd883 100644 --- a/.github/workflows/wai-web-wasm.yml +++ b/.github/workflows/wai-web-wasm.yml @@ -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 diff --git a/wai/wai-web/src/quantum_engine.rs b/wai/wai-web/src/quantum_engine.rs index 0460956b3..9513ee879 100644 --- a/wai/wai-web/src/quantum_engine.rs +++ b/wai/wai-web/src/quantum_engine.rs @@ -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::(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(); -- 2.54.0 (Apple Git-157) From 13151abfd7abf7980eac3ceff0647abdfb4f3d51 Mon Sep 17 00:00:00 2001 From: Transaction Science Date: Fri, 30 Oct 2026 11:05:42 -0400 Subject: [PATCH 06/10] 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 --- wai/tools/quantum-docs/docs.json | 1 + wai/tools/quantum-docs/fixtures/x-pulse.qasm | 13 ++ wai/wai-quantum-cli/README.md | 13 ++ wai/wai-quantum-cli/README.zh-CN.md | 13 ++ wai/wai-quantum-cli/docs/llms-full.txt | 12 +- wai/wai-quantum-service/README.md | 7 +- wai/wai-quantum/README.md | 15 ++ wai/wai-quantum/README.zh-CN.md | 5 + wai/wai-quantum/docs/README.md | 3 + wai/wai-quantum/docs/README.zh-CN.md | 1 + wai/wai-quantum/docs/pulse.md | 145 +++++++++++++++++++ wai/wai-quantum/docs/qasm-feature-matrix.md | 12 +- wai/wai-quantum/src/qasm/pulse.rs | 10 +- 13 files changed, 238 insertions(+), 12 deletions(-) create mode 100644 wai/tools/quantum-docs/fixtures/x-pulse.qasm create mode 100644 wai/wai-quantum/docs/pulse.md diff --git a/wai/tools/quantum-docs/docs.json b/wai/tools/quantum-docs/docs.json index b96fdc53e..f604c6f5d 100644 --- a/wai/tools/quantum-docs/docs.json +++ b/wai/tools/quantum-docs/docs.json @@ -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", diff --git a/wai/tools/quantum-docs/fixtures/x-pulse.qasm b/wai/tools/quantum-docs/fixtures/x-pulse.qasm new file mode 100644 index 000000000..748e73843 --- /dev/null +++ b/wai/tools/quantum-docs/fixtures/x-pulse.qasm @@ -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; diff --git a/wai/wai-quantum-cli/README.md b/wai/wai-quantum-cli/README.md index 803af6aaa..2c571345b 100644 --- a/wai/wai-quantum-cli/README.md +++ b/wai/wai-quantum-cli/README.md @@ -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 diff --git a/wai/wai-quantum-cli/README.zh-CN.md b/wai/wai-quantum-cli/README.zh-CN.md index 534e76637..4c89b3d9b 100644 --- a/wai/wai-quantum-cli/README.zh-CN.md +++ b/wai/wai-quantum-cli/README.zh-CN.md @@ -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 diff --git a/wai/wai-quantum-cli/docs/llms-full.txt b/wai/wai-quantum-cli/docs/llms-full.txt index 401f80d03..739c20fc9 100644 --- a/wai/wai-quantum-cli/docs/llms-full.txt +++ b/wai/wai-quantum-cli/docs/llms-full.txt @@ -903,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 diff --git a/wai/wai-quantum-service/README.md b/wai/wai-quantum-service/README.md index 7058d4f5a..5cb2203f6 100644 --- a/wai/wai-quantum-service/README.md +++ b/wai/wai-quantum-service/README.md @@ -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. diff --git a/wai/wai-quantum/README.md b/wai/wai-quantum/README.md index b3e6d8b17..bbd60a79a 100644 --- a/wai/wai-quantum/README.md +++ b/wai/wai-quantum/README.md @@ -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 diff --git a/wai/wai-quantum/README.zh-CN.md b/wai/wai-quantum/README.zh-CN.md index 6edc721f9..14dd7e3be 100644 --- a/wai/wai-quantum/README.zh-CN.md +++ b/wai/wai-quantum/README.zh-CN.md @@ -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 diff --git a/wai/wai-quantum/docs/README.md b/wai/wai-quantum/docs/README.md index 5bec91556..b4f2d5732 100644 --- a/wai/wai-quantum/docs/README.md +++ b/wai/wai-quantum/docs/README.md @@ -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 diff --git a/wai/wai-quantum/docs/README.zh-CN.md b/wai/wai-quantum/docs/README.zh-CN.md index 34d34a040..3e01f27af 100644 --- a/wai/wai-quantum/docs/README.zh-CN.md +++ b/wai/wai-quantum/docs/README.zh-CN.md @@ -22,6 +22,7 @@ - [读取器的诊断](qasm-diagnostics.md),以及[它如何处理每种构造](qasm-feature-matrix.md)。 - [作业层的拒绝代码](job-refusals.md)。 - [纠错与误差缓解](qec.md):码及其经过认证的距离、解码器、阈值,以及作业中的误差缓解。 +- [脉冲与动力学](pulse.md):读入为帧上时序的脉冲程序,在主方程下积分并给出误差上界,在每个平台上比特一致,以及其回执所绑定的内容。 ## 其他入口 diff --git a/wai/wai-quantum/docs/pulse.md b/wai/wai-quantum/docs/pulse.md new file mode 100644 index 000000000..e274c9421 --- /dev/null +++ b/wai/wai-quantum/docs/pulse.md @@ -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. diff --git a/wai/wai-quantum/docs/qasm-feature-matrix.md b/wai/wai-quantum/docs/qasm-feature-matrix.md index 0d5748ee1..7f176c81a 100644 --- a/wai/wai-quantum/docs/qasm-feature-matrix.md +++ b/wai/wai-quantum/docs/qasm-feature-matrix.md @@ -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 diff --git a/wai/wai-quantum/src/qasm/pulse.rs b/wai/wai-quantum/src/qasm/pulse.rs index 402f2eb4c..e020dcb65 100644 --- a/wai/wai-quantum/src/qasm/pulse.rs +++ b/wai/wai-quantum/src/qasm/pulse.rs @@ -21,10 +21,12 @@ //! which starts at 0. A play starts at its frame's time and moves it on by //! the waveform's length; `delay[d] f` moves `f` on by `d`; `barrier f, g` //! (`barrier;`: every frame in scope) moves each to the latest of them. A -//! gate's calibration starts its frames together: every frame its body -//! names is moved to the latest of them before the body runs. At the top -//! level, `barrier $0, $1` and `delay[d] $0` act on every frame on those -//! qubits' ports (`barrier;` on every frame). +//! gate starts when its qubits are free (each qubit's time is the end of +//! the last gate on it, or of what a `cal` block played on its frames) and +//! its frames are, all at once: every frame its calibration names is moved +//! to that time before the body runs. At the top level, `barrier $0, $1` +//! and `delay[d] $0` act on every frame on those qubits' ports and on the +//! qubits' times (`barrier;` on every frame). //! - **The carrier** of a frame of frequency f and phase φ at time t is //! e^{i(2πft + φ)}. Its phase runs on continuously when the frequency //! changes (`set_frequency`, `shift_frequency`); `set_phase` and -- 2.54.0 (Apple Git-157) From 6fea0b484e7beb3f2d44f7d7bb22bdcddf85a169 Mon Sep 17 00:00:00 2001 From: Transaction Science Date: Fri, 30 Oct 2026 11:12:59 -0400 Subject: [PATCH 07/10] quantum-studio, wai-quantum-desktop: the pulse pane, and the changelog of the pulse layer The Studio's Pulse pane draws a pulse program's schedule as a timeline: a lane per frame with its port and qubit, each play its waveform's envelope at its samples, captures hatched, barriers as rules, phase and frequency changes as ticks, and the gate calls as brackets above, each mark titled with its samples and its line. A program the pulse reader refuses is answered with its code; an example loads a DRAG x and sx and a measurement. A run on the pulse simulator is a job of the method pulse in the page, giving each capture's reading, the counts, the bound and what the receipt binds, every answer's seal checked there. The tab, the service and the desktop app each answer the pane (the desktop app's new pulse command), and studio.test.mjs drives it end to end. wai/CHANGELOG.md records the pulse layer under Unreleased, with the one breaking change the gate reports: schema::FILES grew from 22 to 23. Co-Authored-By: Claude Opus 5.5 --- wai/CHANGELOG.md | 42 ++++++ wai/quantum-studio/README.md | 4 + wai/quantum-studio/src/engine.js | 6 + wai/quantum-studio/src/main.js | 5 +- wai/quantum-studio/src/pulse-view.js | 170 ++++++++++++++++++++++++ wai/quantum-studio/src/studio.css | 22 +++ wai/quantum-studio/src/worker.js | 1 + wai/quantum-studio/test/studio.test.mjs | 23 ++++ wai/wai-quantum-desktop/src/main.rs | 27 +++- 9 files changed, 298 insertions(+), 2 deletions(-) create mode 100644 wai/quantum-studio/src/pulse-view.js diff --git a/wai/CHANGELOG.md b/wai/CHANGELOG.md index 3a65dfcba..f8d8971c6 100644 --- a/wai/CHANGELOG.md +++ b/wai/CHANGELOG.md @@ -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`): diff --git a/wai/quantum-studio/README.md b/wai/quantum-studio/README.md index 6532f31e6..639e34e01 100644 --- a/wai/quantum-studio/README.md +++ b/wai/quantum-studio/README.md @@ -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 diff --git a/wai/quantum-studio/src/engine.js b/wai/quantum-studio/src/engine.js index fcf26f744..3c2ced4c4 100644 --- a/wai/quantum-studio/src/engine.js +++ b/wai/quantum-studio/src/engine.js @@ -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 ?? {}) }); } diff --git a/wai/quantum-studio/src/main.js b/wai/quantum-studio/src/main.js index cd034b25e..d0ef918ca 100644 --- a/wai/quantum-studio/src/main.js +++ b/wai/quantum-studio/src/main.js @@ -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) }); diff --git a/wai/quantum-studio/src/pulse-view.js b/wai/quantum-studio/src/pulse-view.js new file mode 100644 index 000000000..60f4e1dc5 --- /dev/null +++ b/wai/quantum-studio/src/pulse-view.js @@ -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 }; +} diff --git a/wai/quantum-studio/src/studio.css b/wai/quantum-studio/src/studio.css index bf3e3d25b..33d5dc42b 100644 --- a/wai/quantum-studio/src/studio.css +++ b/wai/quantum-studio/src/studio.css @@ -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; } diff --git a/wai/quantum-studio/src/worker.js b/wai/quantum-studio/src/worker.js index 6b673c0c8..622363cf4 100644 --- a/wai/quantum-studio/src/worker.js +++ b/wai/quantum-studio/src/worker.js @@ -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; diff --git a/wai/quantum-studio/test/studio.test.mjs b/wai/quantum-studio/test/studio.test.mjs index b66db20f4..04f7ab5d8 100644 --- a/wai/quantum-studio/test/studio.test.mjs +++ b/wai/quantum-studio/test/studio.test.mjs @@ -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'); diff --git a/wai/wai-quantum-desktop/src/main.rs b/wai/wai-quantum-desktop/src/main.rs index 74482db78..074aa754c 100644 --- a/wai/wai-quantum-desktop/src/main.rs +++ b/wai/wai-quantum-desktop/src/main.rs @@ -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 { + 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"); -- 2.54.0 (Apple Git-157) From 8fc12cf8b8e978011dcd49d12fc85891550539bf Mon Sep 17 00:00:00 2001 From: Transaction Science Date: Fri, 30 Oct 2026 11:12:59 -0400 Subject: [PATCH 08/10] wai-quantum-service smoke, schema contract: pulse runs within the submission window and under a second schema checker The service smoke stays within the 30 submissions a client makes in a window (29 now, 30 on a host with a GPU): its set gains a pulse item, run as a job of the method pulse, and POST /v1/pulse/check runs that item again and holds it to its result, in place of separate schedule and run calls. The schema contract script calls pulse_schedule and the pulse run and plan tools over the command line's MCP server, and checks the run with pulse_check, each answer held to its output schema by the second JSON Schema implementation. Co-Authored-By: Claude Opus 5.5 --- wai/wai-quantum-service/smoke.sh | 39 ++++++++++++----------------- wai/wai-quantum/ci/check_schemas.py | 15 +++++++++-- 2 files changed, 29 insertions(+), 25 deletions(-) diff --git a/wai/wai-quantum-service/smoke.sh b/wai/wai-quantum-service/smoke.sh index 60f4e3c71..950de0600 100644 --- a/wai/wai-quantum-service/smoke.sh +++ b/wai/wai-quantum-service/smoke.sh @@ -399,23 +399,6 @@ d=json.load(open(sys.argv[1])); m=d["result"]["mitigation"]; o=m["observables"][ assert d["status"]=="done" and m["method"]=="zne" and abs(o["mitigated"]-o["ideal"]) < abs(o["raw"]-o["ideal"]), m print("mitigated job: ok, raw", round(o["raw"], 4), "mitigated", round(o["mitigated"], 4), "ideal", round(o["ideal"], 4))' "$tmp/mitigated.out" -# A pulse program: its schedule, its run as a job (method pulse, the error -# bounded, the receipt binding the program), and the job checked by running -# it again. -py 'import json,sys -p = "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" -json.dump({"qasm": p}, open(sys.argv[1], "w")); json.dump({"qasm": p, "method": "pulse", "shots": 200}, open(sys.argv[2], "w"))' "$tmp/pulse-schedule.json" "$tmp/pulse-job.json" -post /v1/pulse/schedule "$tmp/pulse-schedule.json" > "$tmp/pulse-schedule.out" -py 'import json,sys; r=json.load(open(sys.argv[1]))["result"]; assert r["duration"]==164 and [i["op"] for i in r["instructions"]]==["play","capture"] and r["receipt"], r; print("pulse schedule: ok,", r["duration"], "samples")' "$tmp/pulse-schedule.out" -run "$tmp/pulse-job.json" "$tmp/pulse-job.out" -py 'import json,sys -d=json.load(open(sys.argv[1])); r=d["result"] -assert d["status"]=="done" and r["method"]=="pulse" and r["accuracy"]["class"]=="bounded" and r["accuracy"]["bound"] <= 1e-6 and len(r["pulse"]["calibration"]["program"])==64, d -json.dump({"request": d["request"], "result": r}, open(sys.argv[2], "w")) -print("pulse job: ok, P(1)", round(r["pulse"]["distribution"]["1"], 6), "within", r["accuracy"]["bound"])' "$tmp/pulse-job.out" "$tmp/pulse-check.json" -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, r; print("pulse check: ok, reproduced bit for bit")' "$tmp/pulse-check.out" - # An estimate: the program's counts, the frontier, each estimate sealed with # the service's key in the resource-estimate receipt family, POST /v1/verify # checking the seals and re-running it, and one too fine sent to the command @@ -457,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. @@ -473,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") diff --git a/wai/wai-quantum/ci/check_schemas.py b/wai/wai-quantum/ci/check_schemas.py index ab58216ee..e2f6e306b 100644 --- a/wai/wai-quantum/ci/check_schemas.py +++ b/wai/wai-quantum/ci/check_schemas.py @@ -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 -- 2.54.0 (Apple Git-157) From 3f2f4d9ce912bef2214626088a7bb8cb804eb876 Mon Sep 17 00:00:00 2001 From: Transaction Science Date: Fri, 30 Oct 2026 12:16:21 -0400 Subject: [PATCH 09/10] wai-quantum views: the page rebuilt with the pulse pane's styles, in the image CI rebuilds it in The pulse pane's styles changed studio.css, which the views page is built from, so build-views --check refused the page main holds. Rebuilt with wai/wai-quantum-views/build.sh in catthehacker/ubuntu:act-22.04 with Rust 1.98.0, as the views workflow rebuilds it. Its script is unchanged and its styles carry the pane's. The engine inside it is rebuilt from the current sources too. The page on main held an older engine, which CI warned of on #549. Co-Authored-By: Claude Opus 5.5 --- wai/wai-quantum/views/manifest.json | 16 ++++++++-------- wai/wai-quantum/views/wai-quantum-views.html | 4 ++-- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/wai/wai-quantum/views/manifest.json b/wai/wai-quantum/views/manifest.json index d95c260f5..951273c3d 100644 --- a/wai/wai-quantum/views/manifest.json +++ b/wai/wai-quantum/views/manifest.json @@ -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", diff --git a/wai/wai-quantum/views/wai-quantum-views.html b/wai/wai-quantum/views/wai-quantum-views.html index c987f62a4..785ff3d72 100644 --- a/wai/wai-quantum/views/wai-quantum-views.html +++ b/wai/wai-quantum/views/wai-quantum-views.html @@ -6,12 +6,12 @@ WAI Quantum -
-