Circuit timing

A key aspect of expressing code for quantum experiments is the ability to control the timing of gates and pulses. Examples include characterization of decoherence and crosstalk, dynamical decoupling, dynamically corrected gates, and gate scheduling. This can be a challenging task given the potential heterogeneity of calibrated gates and their various durations. It is useful to specify gate timing and parallelism in a way that is independent of the precise duration and implementation of gates at the pulse-level description. In other words, we want to provide the ability to capture design intent such as “space these gates evenly to implement a higher-order echo decoupling sequence” or “implement this gate as late as possible”.

Duration and stretch types

The duration type is used denote increments of time. Durations are real numbers that are manipulated at compile time. Durations must be followed by time units which can be any of the following:

  • SI units of time: ns, µs or us, ms, s

  • Backend-dependent unit, dt, equivalent to the duration of one waveform sample on the backend

Units can appear attached to the numerical value, or immediately following separated only by blanks or tabs. 1000ms is the same as 1000 ms.

It is often useful to reference the duration of other parts of the circuit. For example, we may want to delay a gate for twice the duration of a particular sub-circuit, without knowing the exact value to which that duration will resolve. Alternatively, we may want to calibrate a gate using some pulses, and use its duration as a new duration in order to delay other parts of the circuit. The durationof() intrinsic function can be used for this type of referential timing.

Below are some examples of values of type duration.

// fixed duration, in standard units
duration a = 300ns;
// fixed duration, backend dependent
duration b = 800dt;
// fixed duration, referencing the duration of a calibrated gate
duration c = durationof({x $3;});

We further introduce a stretch type which is a sub-type of duration. Stretchable durations have variable non-negative duration that are permitted to grow as necessary to satisfy constraints. Stretch variables are resolved at compile time into target-appropriate durations that satisfy a user’s specified design intent.

Instructions whose duration are specified in this way become “stretchy”, meaning they can extend beyond their “natural duration” to fill a span of time. Stretchy delay’s are the most obvious use case, but this can be extended to other instructions too, e.g. rotating a spectator qubit while another gate is in progress. Similarly, a gate whose definition contains stretchy delays will be perceived as a stretchy gate by other parts of the program.

../_images/d1.svg

a

../_images/d2.svg

b

Fig. 4 Arbitrary alignment of gates in time using stretchy delays. a) left-justified alignment b) alignment of a short gate at the 1/3 point of a longer gate.

For example, in order to ensure a sequence of gates between two barriers will be left-aligned (Fig. 4a), whatever their actual durations may be, we can do the following:

qubit[5] q;
barrier q;
cx q[0], q[1];
U(pi/4, 0, pi/2) q[2];
cx q[3], q[4];
stretch a;
stretch b;
stretch c;
delay[a] q[0], q[1];
delay[b] q[2];
delay[c] q[3], q[4];
barrier q;

We can further control the exact alignment by giving relative weights to the stretchy delays (Fig. 4b):

qubit[5] q;
stretch g;
barrier q;
cx q[0], q[1];
delay[g];
u q[2];
delay[2*g];
barrier q;

The concepts of box (see Boxed expressions) and stretch are inspired by the concept of “boxes and glues” in the TeX language [KB84]. This similarity is natural; TeX aims to resolve the spacing between characters in order to typeset a page, and the size of characters depend on the backend font. In OpenQASM we intend to resolve the timing of different instructions in order to meet high-level design intents, while the true duration of operations depend on the backend and compilation context. There are however some key differences. Quantum operations can be non-local, meaning the durations set on one qubit can have side effects on other qubits. The definition of duration-type variables and ability to define multi-qubit stretches is intended to alleviate potential problems from these side effects. Also contrary to TeX, we prohibit overlapping gates.

Operations on durations

We can add/subtract two durations, or multiply or divide them by a constant, to get a new duration. Division of two durations results in a machine-precision float (see Converting duration to other types). Negative durations are allowed, however passing a negative duration to a gate[duration] or box[duration] expression will result in an error. All operations on durations happen at compile time since ultimately all durations, including stretches, will be resolved to constants.

duration a = 300ns;
duration b = durationof({x $0;});
stretch c;
// stretchy duration with min=300ns
stretch d = a + 2 * c;
// stretchy duration with backtracking by up to half b
stretch e = -0.5 * b + c;

Delays (and other duration-based instructions)

OpenQASM and OpenPulse have a delay instruction, whose duration is defined by a duration. If the duration passed to the delay contains stretch, it will become a stretchy delay. We use square bracket notation to pass these duration parameters, to distinguish them from regular parameters (the compiler will resolve these square-bracket parameters when resolving timing).

Even though a delay instruction implements the identity operator in the ideal case, it is intended to provide explicit timing. Therefore an explicit delay instruction will prevent commutation of gates that would otherwise commute. For example in Fig. 5a , there will be an implicit delay between the cx gates on qubit 0. However, the rz gate is still free to commute on that qubit, because the delay is implicit. Once the delay becomes explicit (perhaps at lower stages of compilation), gate commutation is prohibited (Figure Fig. 5b).

../_images/d3.svg

a

../_images/d4.svg

b

Fig. 5 Implicit vs. explicit delay. a) An implicit delay exists on \(q[0]\), but it is not part of the circuit description. Thus this circuit does not care about timing and the \(RZ\) gate is free to commute on the top wire. b) An explicit delay is part of the circuit description. The timing is consistent and can be resolved if and only if this delay is exactly the same duration as \(RY\) on \([1]\). The delay is like a barrier in that it prevents commutation on that wire. However \(RZ\) can still commute before the \(CNOT\) if it has duration \(0\).

../_images/d5.svg

a

../_images/d6.svg

b

Fig. 6 Dynamically corrected CNOT gate where the spectator has a rotary pulse. The rotary gates are stretchy, and the design intent is to interleave a “winding” and “unwinding” that is equal to the total duration of the CNOT. We do this without knowledge of the CNOT duration, and the compiler resolves them to the correct duration during lowering to the target backend.

../_images/d7.svg

Fig. 7 Dynamical decoupling of a spectator qubit using finite-duration DD pulses. The boxes are intentionally drawn to scale to give a sense of how finite gate durations affect circuit timing. This design intent can be expressed by defining a single stretch variable “equal” that corresponds to the distance between equidistant gate centers. The other durations which correspond to actual circuit delays are derived by simple arithmetic. Given a target system with calibrated X and Y gates, the solution to the stretch problem can be found.

Instructions other than delay can also have variable duration, if they are explicitly defined as such. They can be called by passing a valid duration as their duration. Consider for example a rotation called rotary that is applied for the entire duration of some other gate.

const amp = /* number */;
stretch a;
rotary(amp)[250ns] q;   // square brackets indicates duration
rotary(amp)[a] q;       // a rotation that will stretch as needed

A multi-qubit delay instruction is not equivalent to multiple single-qubit delay instructions. Instead a multi-qubit delay acts as a synchronization point on the qubits, where the delay begins from the latest non-idle time across all qubits, and ends simultaneously across all qubits.

cx q[0], q[1];
cx q[2], q[3];
// delay for 200 samples starting from the end of the longest cx
delay[200dt] q[0:3];

A duration can be composed of positive or negative durations, and of positive stretches. After resolving the stretches, the instruction must end up with non-negative duration.

For example, the code below inserts a dynamical decoupling sequence where the *centers* of pulses are equidistant from each other. We specify correct durations for the delays by using backtracking operations to properly take into account the finite duration of each gate.

stretch a;
stretch b;
duration start_stretch = a - .5 * durationof({x $0;});
duration middle_stretch = a - .5 * duration0({x $0;}) - .5 * durationof({y $0;});
duration end_stretch = a - .5 * durationof({y $0;});

delay[start_stretch] $0;
x $0;
delay[middle_stretch] $0;
y $0;
delay[middle_stretch] $0;
x $0;
delay[middle_stretch] $0;
y $0;
delay[end_stretch] $0;

cx $2, $3;
delay[b] $1;
cx $1, $2;
u $3;

Boxed expressions

We introduce a box statement for scoping the timing of a particular part of the circuit. A boxed subcircuit is different from a gate or def subroutine, in that it is merely an enclosure to a piece of code within the larger scope which constrains it. This can be used to signal permissible logical-level optimizations to the compiler: optimizing operations within a box definition is permitted, and optimizations that move operations from one side to the other side of a box are permitted, but moving operations either into or out of the box as part of an optimization is forbidden. The compiler can also infer a description of the operation which a box definition is meant to realise, allowing it to re-order gates around the box. For example, consider a dynamical decoupling sequence inserted in a part of the circuit:

rx(2*π/12) q;
box {
    delay[ddt] q;
    x q;
    delay[ddt] q;
    x q;
    delay[ddt] q;
}
rx(3*π/12) q;

By boxing the sequence, we create a box that implements the identity. The compiler is now free to commute a gate past the box by knowing the unitary implemented by the box:

rx(5*π/12) q;
box {
    delay[ddt] q;
    x q;
    delay[ddt] q;
    x q;
    delay[ddt] q;
}

The compiler can thus perform optimizations without interfering with the implmentation of the dynamical decoupling sequence.

As with other operations, we may use square brakets to assign a duration to a box: this can be used to put hard constraints on the execution of a particular sub-circuit by requiring it to have the assigned duration. This can be useful in scenarios where the exact duration of a piece of code is unknown (e.g., if it is runtime dependent), but where it would be helpful to impose a duration on it for the purpose of scheduling the larger circuit. For example, if the duration of the parameterized gates mygate1(a, b), mygate2(a, b) depend on values of the variables a and b in a complex way, but an offline calculation has shown that the total will never require more than 150ns for all valid combinations:

// some complicated circuit that gives runtime values to a, b
box [150ns] {
    delay[str1] q1; // Schedule as late as possible within the box
    mygate1(a, a+b) q[0], q[1];
    mygate2(a, a-b) q[1], q[2];
    mygate1(a-b, b) q[0], q[1];
}

Barrier instruction

The barrier instruction of OpenQASM 2 prevents commutation and gate reordering on a set of qubits across its source line. The syntax is barrier qregs|qubits; and can be seen in the following example

cx r[0], r[1];
h q[0];
h a[0];
barrier r, q[0];
h a[0];
cx r[1], r[0];
cx r[0], r[1];

This will prevent an attempt to combine the CNOT gates but will not constrain the pair of h a[0]; gates, which might be executed before or after the barrier, or cancelled by a compiler.

A barrier is similar to delay[0]. The main difference is that delay indicates a fully scheduled series of instructions, whereas barrier implies an ordering constraint that will be resolved by the compiler at a later stage.

A barrier can also be invoked without arguments, in which case the argument is assumed to be all qubits.