Pulse-level descriptions of gates and measurement

To induce the quantum gates and measurements of a circuit, qubits are manipulated with classically-controlled stimulus fields. The details of these stimuli are typically unique per-qubit and may vary over time due to instabilities in the underlying systems. Furthermore, there is significant interest in applying optimal control methodologies to the construction of these controls in order to optimize gate and circuit performance. As a consequence, we desire to connect gate-level instructions to the underlying microcoded [Wil89] stimulus programs emitted by the controllers to implement each operation. In OpenQASM we expose access to this level of control with pulse-level definitions of gates and measurement with a user-selectable pulse grammar.

The entry point to such gate and measurement definitions is the defcal keyword analogous to the gate keyword, but where the defcal body specifies a pulse-level instruction sequence on physical qubits, e.g.

defcal rz(angle[20] theta) $0 { ... }
defcal measure $0 -> bit { ... }
defcal measure_iq q -> complex[float[32]] { ... }

We distinguish gate and measurement definitions by the presence of a return value type in the latter case, analogous to the subroutine syntax defined earlier. Furthermore, the return value type does not need to return a classified value but can return some lower level data.

The reference to physical rather than virtual qubits is critical because quantum registers are no longer interchangeable at the pulse level. Due to varying physical qubit properties a microcode definition of a gate on one qubit will not perform the equivalent operation on another qubit. To meaningfully describe gates as pulses we must bind operations to specific qubits. QASM achieves this by prefixing qubit references with $ to indicate a specific qubit on the device, e.g. $2 would refer to physical qubit 2.

One can define a defcal using a regular identifier for a qubit, which causes that calibration definition to be valid for all physical qubits. This is most likely to be useful for gates that are implemented virtually. For instance, to define an equivalent rz calibration on qubits 0 and 1, we could write

defcal rz(angle[20] theta) q { ... }
// we've defined ``rz`` on arbitrary physical qubits, so we can do:
rz(3.14) $0;
rz(3.14) $1;

As a consequence of the need for specialization of operations on particular qubits, the same symbol may be defined multiple times, e.g.

defcal h $0 { ... }
defcal h $1 { ... }

and so forth. Some operations require further specialization on parameter values, so we also allow multiple declarations on the same physical qubits with different parameter values, e.g.

defcal rx(pi) $0 { ... }
defcal rx(pi / 2) $0 { ... }

Given multiple definitions of the same symbol, the compiler will match the most specific definition found for a given operation. Thus, given,

  1. defcal rx(angle[20] theta) q  ...

  2. defcal rx(angle[20] theta) $0  ...

  3. defcal rx(pi / 2) $0  ...

the operation rx(pi/2) $0 would match to (3), rx(pi) $0 would match (2), rx(pi/2) $1 would match (1).

Users specify the grammar used inside defcal blocks with a defcalgrammar "name" declaration. One such grammar is a textual representation of OpenPulse specified by:

defcalgrammar "openpulse";

Note that defcal and gate communicate orthogonal information to the compiler. gates define unitary transformation rules to the compiler. The compiler may freely invoke such rules on operations while preserving the structure of a circuit as a collection of gates and subroutines. The defcal declarations instead define elements of a symbol lookup table. As soon as the compiler replaces a gate with a defcal definition, we have changed the fundamental structure of the circuit. Most of the time symbols in the defcal table will also have corresponding gate definitions. However, if a user provides a defcal for a symbol without a corresponding gate, then we treat such operations like the opaque gates of prior versions of OpenQASM.

Inline calibration blocks

As calibration grammars may require the ability to insert top-level configuration information, perform program setup, or make inline calls to calibration-level instructions, OpenQASM supports the ability to declare a cal block. Within the cal block the semantics of the selected defcalgrammar are valid. The cal block is of the same scope level as the enclosing block. The calibration grammar may choose to allow capturing values (with chosen syntax) from within the cal block that were declared within the containing parent scope. Values declared within the cal block are only referenceable from other cal blocks or defcal declarations that may observe that scope as defined by the calibration grammar. Values may not leak back to the block’s enclosing scope. In practice, calibration grammars such as OpenPulse may apply a global scope to all identifiers in order to declare values shared across all defcal calls thereby linking them together.

OPENQASM 3;
defcalgrammar "openpulse";

const float original_freq = 5.9e9;

cal {
   // Defined within `cal`, so it may not leak back out to the enclosing blocks scope
   float new_freq = 5.2e9;
   // declare global port
   extern port d0;
   // reference `freq` variable from enclosing blocks scope
   frame d0f = newframe(d0, freq, 0.0);

}

defcal x $0 {
   waveform xp = gaussian(1.0, 160t, 40dt);
   // References frame and `new_freq` declared in top-level cal block
   play(d0f, xp);
   set_frequency(d0f, new_freq);
   play(d0f, xp);
}

Restrictions on defcal bodies

The contents of defcal bodies are subject to the restriction they must have a definite duration known at compile time, regardless of the parameters passed in or the state of the system when called. This allows the compiler to properly resolve durationof(...) calls and allows for optimizations. If there is to be control flow in the defcal, each branch of the control flow must have definite and equivalent duration resolvable at compile time. Similarly, loops must be have a resolvable definite duration at compile time.

For example, consider the case of a reset gate. The defcal for a reset gate can be composed of a single if statement, provided each branch of the if statement has definite and equivalent duration.

defcal reset $0 {
   bit res = // measure qubit $0
   if (res == 1) {
      // flip the qubit
   } else {
      // delay for an equivalent amount of time
   }
}

Calibrations in practice

By their very nature calibrations are transient and unique to a target system. They are typically generated by automatic calibration routines that are periodically run on the target system, that are in turn bootstrapped from previous calibrations. The majority of OpenQASM users will use the default calibrations, however, for those that want more control, but do not want to bootstrap calibrations for an entire system. It is expected that the target system provider will provide an include file to the user. This will contain the declaration of the defcalgrammar, constants, defcals and other grammar and system specific components such as ports, waveforms and frames in the OpenPulse defcalgrammar <openpulse.html>. The user may then plugin to the existing calibrations by defining new calibrations, or overwriting existing ones by using the same ports and frames. The example below demonstrates this in practice for a two-qubit, cross-resonance device using a backend.inc include file. The name backend.inc is arbitrary - it’s just a file to be included using the existing include mechanism.

// backend.inc for openpulse two-qubit device

defcalgrammar "openpulse";

const float q0_freq = 5.0e9;
const float q1_freq = 5.1e9;

cal {

   extern drag(complex[size] amp, duration l, duration sigma, float[size] beta) -> waveform;
   extern gaussian_square(complex[size] amp, duration l, duration square_width, duration sigma) -> waveform;

   extern port q0;
   extern port q1;

   frame q0_frame = newframe(q0, q0_freq, 0);
   frame q1_frame = newframe(q1, q1_freq, 0);
}

defcal rz(angle theta) $0 {
   shift_phase(q0_frame, theta);
}

defcal rz(angle theta) $1 {
   shift_phase(q1_frame, theta);
}

defcal sx $0 {
   waveform sx_wf = drag(0.2+0.1im, 160dt, 40dt, 0.05);
   play(q0_frame, sx_wf);
}

defcal sx $1 {
   waveform sx_wf = drag(0.1+0.05im, 160dt, 40dt, 0.1);
   play(q1_frame, sx_wf);
}

defcal cx $1, $0 {
   waveform CR90p = gaussian_square(0.2+0.05im, 560dt, 240dt, 40dt);
   waveform CR90m = gaussian_square(-0.2-0.05im, 560dt, 240dt, 40dt);

   rz(pi/2) $0; rz(-pi/2) $1;
   sx $0; sx $1;
   barrier $0, $1;
   play(q0_frame, CR90p);
   barrier $0, $1;
   sx $0;
   sx $0;
   barrier $0, $1;
   rz(-pi/2) $0; rz(pi/2) $1;
   sx $0; sx $1;
   play(q0_frame, CR90m);
}

The user would then include the backend.inc in their own program and use them as demonstrated below

OPENQASM 3.0;

include "backend.inc"

// Defcal using frames from backend.inc enabling the calibration
// to "plugin" to the existing calibrations.
defcal Y90p $0 {
   waveform y90p = drag(0.1-0.2im, 160dt, 40dt, 0.05);
   play(q0_frame, y90p);
}

// Teach the compiler what the unitary of a Y90p is
gate Y90p q {
   rz(-pi/2) q;
   sx q;
}

// Use this defcal explicitly
Y90p $0;
cx $1, $0;