Quil enables users to specify high-level, gate-based, timing-agnostic quantum programs. Only a subset of experiments can be specified this way (albeit a large subset), others require lower level control.
In particular there is a desire to:
- be able to define custom waveforms
- have full control over pulse timing and length
- be able to introduce more sophisticated translation for ideas eg. dynamic decoupling, crosstalk correction, gate decompositions, etc.
- define new gates (e.g. Toffoli and Fredkin) by composing pulses on frames and/or that exploit levels beyond the two level qubit approximation
- remove channels where discrepancies between internal and external performance can be introduced
This RFC proposes adding analog control to Quil which will be accessible through Quil language bindings and used by compilers and simulators.
See the diff on Quil.md for the syntax of the introduction of a number of new Quil instruction types:
- DEFCAL
- DEFFRAME, DEFWAVEFORM
- DELAY
- FENCE
- PULSE
- CAPTURE, RAW-CAPTURE
- SET-SCALE
- SET-FREQUENCY, SHIFT-FREQUENCY
- SET-PHASE, SHIFT-PHASE, SWAP-PHASES
Each qubit can have multiple frames, denoted by string names such as "xy", "cz", or "ro". A frame is an abstraction that captures the instantaneous frequency and phase that will be mixed into the control signal. The frame frequencies are with respect to the absolute "lab frame".
Quilt has two notions of waveforms. Custom waveforms are defined using DEFWAVEFORM as a list of complex numbers which represent the desired waveform envelope, along with a sample rate. Each complex number represents one sample of the waveform. The exact time to play a waveform can be determined by dividing by the sample rate, which is in units of samples per second.
NOTE: Quilt frames also have an associated sample rate, which may be
specified in the corresponding DEFFRAME
block, and are ultimately
determined/enforced at link time by the underlying control hardware which the
frame is associated to. If a custom waveform is applied (via PULSE
or
CAPTURE
) to a frame for which it has an incompatible sample rate, the behavior
is undefined.
There are also some built-in waveform generators which take as a parameter the duration of the waveform in seconds, alleviating the need to know the sample rate to calculate duration. These are valid for use regardless of the frame's underlying sample rate.
In order to materialize the precise waveforms to be played the waveform envelopes must be modulated by the frame's frequency, in addition to applying some scaling and phasing factors. Although in theory it would be mostly possible to simply define new waveforms that did the modulation, scaling, and phasing manually, this is both tedious and doesn't take advantage of hardware which has specialized support for tracking these things.
Therefore, each frame has associated with it a triple (frequency, phase, scale) that can be modified throughout the program using SET-* instructions (and additional instructions for phase).
Here's a table explaining the differences between these three values that are tracked through the program:
Name | Initial Value | Valid Values | Can be parameterized? |
---|---|---|---|
Frequency | (not set) | Real numbers | No |
Phase | 0.0 | Real numbers | Yes |
Scale | 1.0 | Real numbers | No |
Now that frequency, phase, and scale on a frame have been established we can play pulses. Pulses can be played by using the PULSE instruction and specifying both the qubit frame as well as the waveform.
Given a waveform my_custom_waveform
and the following program:
SET-FREQUENCY 0 "xy" 5400e6
SET-PHASE 0 "xy" pi/2
SET-SCALE 0 "xy" 1/2
PULSE 0 "xy" my_custom_waveform
A compiler would have several options depending on the hardware backend. It
could create a new waveform (eg. my_custom_waveform_2
) and apply the
(frequency, phase, scale) to it. Or it could take advantage of built-in hardware
instructions to apply those values internally. This would be the responsibility
of the compiler to make a trade-off between number of instructions and number of
waveforms, given some hardware constraints.
To readout a qubit the capture instruction is used. It takes a qubit frame, a waveform, and a classical memory reference. In this case the waveform is used as an integration kernel. The inner product of the integration kernel and the list of measured IQ values is evaluated to produce a single complex result.
This complex number needs to be stored in Quil classical memory. Quil does not currently support complex numbers, so a real array of length 2 is used instead:
# Simple capture of an IQ point
DECLARE iq REAL[2]
CAPTURE 0 "ro" flat(duration: 1e-6, iq: 2+3i) iq
Analog control instructions extend the definition of a quantum abstract machine to introduce the concept of time. In this new interpretation each instruction in Quil has an associated time to execute (which may be effectively zero for certain operations). It is up to the interpreter to provide semantics for how pulses are scheduled, as long as certain consistency requirements are met. Roughly speaking:
- "Events" on a frame happen at a well defined time since eg. updating a frame frequency means that it starts to accumulate phase at a new rate.
- Events happen in the order listed in the program.
- Pulses on a common frame may not overlap in time.
- Pulses on distinct frames which involve a common resource may not overlap in
time unless one is marked as
NONBLOCKING
.
A more precise specification of the timing semantics is provided in scheduling.md.
The duration of a pulse operation, i.e. PULSE
, CAPTURE
, or RAW-CAPTURE
, is
the duration of the associated waveform.
Each frame is defined relative to a set of qubits. The execution of a pulse operation on a frame blocks pulse operations on intersecting frames, i.e. frames with a qubit in common with the pulse frame.
In certain instances it may be desirable to support multiple concurrent pulses
on the same qubit, for example in measurements where CAPTURE
performs a
readout which may overlap with a transmission PULSE
.
A pulse operation (PULSE
, CAPTURE
, and RAW-CAPTURE
) with the NONBLOCKING
modifier does not exclude pulse operations on other frames. For example,
in
NONBLOCKING PULSE 0 "xy" flat(duration: 1.0, iq: 1.0)
PULSE 0 1 "ff" flat(duration: 1.0, iq: 1.0)
the two pulses could be emitted simultaneously. Nonetheless, a NONBLOCKING
pulse does still exclude the usage of the pulse frame, so e.g. NONBLOCKING PULSE 0 "xy" ... ; PULSE 0 "xy" ...
would require serial execution.
A DELAY
instruction is equivalent to a NONBLOCKING
no-operation on all
specified frames. For example, DELAY 0 "xy" 1.0
delays frame 0 "xy"
by one
second.
If the DELAY
instruction presents a list of qubits with no frame names, all
frames on exactly these qubits are delayed. Thus DELAY 0 1.0
delays all one
qubit frames on qubit 0, but does not affect 0 1 "cz"
.
The FENCE
instruction provides a means of synchronization of all frames
involving a set of qubits. In particular, it guarantees that all instructions
involving any of the fenced qubits preceding the FENCE
are completed before
any instructions involving the fenced qubits which follow the FENCE
. If FENCE
has no arguments, then it applies to all qubits on the device.
Single frame mutations (SET-FREQUENCY
, SHIFT-FREQUENCY
, SET-PHASE
, SHIFT-PHASE
,
SET-SCALE
) have a hardware dependent duration (which may be effectively zero).
These operations block pulses on the targeted frame.
The SWAP-PHASE
instruction introduces an implicit synchronization on the two
involved frames. In other words, any operations involving either of the swapped
frames and preceding the SWAP-PHASE
must complete prior to the SWAP-PHASE
event.
Calibrations can be associated with gates in Quil to aid the compiler in converting a list of gates into the corresponding series of pulses.
Calibrations can be parameterized and include concrete values, which are resolved in "Haskell-style", with later definitions being prioritized over earlier ones. For example, given the following list of calibration definitions in this order:
DEFCAL RX(%theta) %qubit:
DEFCAL RX(%theta) 0:
DEFCAL RX(pi/2) 0:
The instructionRX(pi/2) 0
would match (3), the instructionRX(pi) 0
would match (2), and the instructionRX(pi/2) 1
would match (1).
The body of a DEFCAL is a list of analog control instructions that ideally enacts the corresponding gate.
Below I present some pseudo-PyQuil to indicate how users would interact with analog control.
If the user simply wants to run gate-based programs, then the usage would not change.
my_program = ...
qvm_result = qvm.simulate(my_program)
# get_calibrations returns a list of DEFCAL instructions for the qubits
# specified. This would be from the most recent recalibration of the system.
# This would probably be handled behind the scenes in the QuantumComputer but
# the purpose of this example is to show how easy it is to combine calibrations
# with an existing gate-based program.
calibrations = get_calibrations([0, 1, 2], version='most_recent')
full_program = calibrations + my_program
binary = compiler.compile(full_program)
qpu_result = qpu.run(binary)
For a user writing their program completely at the level of analog control, we need to expose APIs for getting frame sample rates as well as qubit and readout frequencies.
sample_rate_rf_0 = get_sample_rate(0, 'rf')
q0_freq = get_qubit_frequency(0)
# Takes an awfully long waveform...
one_second_waveform = np.ones(sample_rate_rf_0)
# Python API TBD
my_program = Program()
my_program += DEFWAVEFORM('my_custom_waveform', one_second_waveform)
my_program += SET_FREQUENCY(0, 'rf', q0_freq)
my_program += PULSE(0, 'rf', 'my_custom_waveform')
""")
A user doing a mixture of gates and pulse control has a number of options:
- if they want custom waveforms then they can get the sample rate, if they just want built-in waveform shapes then they don't need the sample rate
- if they want to know our guesses for the qubit frequency then they will be provided, otherwise they can find it themselves
- if they want to use our calibrations they can, or they can modify them, or produce their own
- they can use gates from the PyQuil default library and this will be translated to pulses by the compiler, or they can resolve the pulses themselves
Here are some example calibrations for various types of gates and measurements.
Setting up frequencies:
SET-FREQUENCY 0 "xy" 4678266018.71412
SET-FREQUENCY 1 "xy" 3821271416.79618
SET-FREQUENCY 0 1 "cz" 137293415.829024
SET-FREQUENCY 0 "ro" 5901586914.0625
SET-FREQUENCY 1 "ro" 5721752929.6875
SET-FREQUENCY 0 "out" 5901586914.0625
SET-FREQUENCY 1 "out" 5721752929.6875
Calibrations of RX:
DEFCAL RX(%theta) 0:
SET-SCALE %theta/pi*0.936
PULSE 0 "xy" draggaussian(duration: 80e-9, fwhm: 40e-9, t0: 40e-9, anh: -210e6, alpha: 0)
DEFCAL RX(pi/2) 0:
SET-SCALE 0.468
PULSE 0 "xy" draggaussian(duration: 80e-9, fwhm: 40e-9, t0: 40e-9, anh: -210e6, alpha: 0)
# With crosstalk mitigation - no pulses on neighbors
DEFCAL RX(pi/2) 0:
FENCE 0 1 7
PULSE 0 "xy" draggaussian(duration: 80e-9, fwhm: 40e-9, t0: 40e-9, anh: -210e6, alpha: 0)
FENCE 0 1 7
RZ:
DEFCAL RZ(%theta) %qubit:
# RZ of +theta corresponds to a frame change of -theta
SHIFT-PHASE %qubit "xy" -%theta
Calibrations of CZ:
DEFCAL CZ 0 1:
PULSE 0 1 "cz" erfsquare(duration: 340e-9, risetime: 20e-9, padleft: 8e-9, padright: 8e-9)
SHIFT-PHASE 0 "xy" 0.00181362669
SHIFT-PHASE 1 "xy" 3.44695296672
# With no parallel 2Q gates
DEFCAL CZ 0 1:
FENCE 0 1 2 3 4 5 6 7 10 11 12 13 14 15 16 17
PULSE 0 1 "cz" erfsquare(duration: 340e-9, risetime: 20e-9, padleft: 8e-9, padright: 8e-9)
SHIFT-PHASE 0 "xy" 0.00181362669
SHIFT-PHASE 1 "xy" 3.44695296672
FENCE 0 1 2 3 4 5 6 7 10 11 12 13 14 15 16 17
Readout:
DEFCAL MEASURE 0 %dest:
DECLARE iq REAL[2]
PULSE 0 "ro" flat(duration: 1.2e-6, iq: ???)
CAPTURE 0 "out" flat(duration: 1.2e-6, iq: ???) iq
LT %dest iq[0] ??? # thresholding
Toffoli gate (from Colm):
SET-FREQUENCY 12 13 "cz" 283.5e6
SET-FREQUENCY 13 14 "iswap" 181e6
DEFCAL CCNOT 12 13 14:
# iSWAP_02 on 13-14
FENCE 12 13 14
PULSE 13 14 "iswap" erfsquare(tmax: 131e-9, risetime: 20e-9, padleft: 12e-9, pad_right: 13e-9)
# CZ_20 or 12-13
FENCE 12 13 14
PULSE 12 13 "cz" erfsquare(tmax: 332e-9, risetime: 20e-9, padleft: 12e-9, pad_right: 12e-9)
# iSWAP_02 on 13-14
FENCE 12 13 14
SHIFT-PHASE 13 14 "iswap" 0.5 # iSWAP_phase from the original code snippet
PULSE 13 14 "iswap" erfsquare(tmax: 131e-9, risetime: 20e-9, padleft: 12e-9, pad_right: 13e-9)
FENCE 12 13 14
Active Reset Calibration:
DEFCAL RESET %qubit:
DECLARE ro BIT
MEASURE %qubit ro[0]
JUMP-UNLESS ro @delay
RX(pi) %qubit
JUMP @end
LABEL delay
DELAY %qubit 60e-9
LABEL end
Single point of a parametric gate chevron: (parameterized in amplitude, frequency, and time)
RX(pi) 0
RX(pi) 1
SET-FREQUENCY 0 "cz" 160e6
SET-SCALE 0 "cz" 0.45
PULSE 0 1 "cz" erfsquare(duration: 100e-9, risetime: 20e-9, padleft: 0, padright: 0)
How would this interact with quilc and qvm?
QVM has a couple options:
- Just ignore any all analog control instructions since they don't change the state of the quantum abstract machine
- Ignore definitions of calibrations but throw an error on pulses and timing instructions
- Throw an error on any analog control instructions
The compiler will need to do something more sophisticated. At the level of linear algebra operations, the compiler can either throw an error on encountering analog control or it can consider them "anonymous gates" which can't be moved, permuted, or compressed in any way.
Eventually our compilation chain will need to support taking a high-level Quil program from gates all the way down to only classical control instructions.
Sample rates, local oscillators, timing, doesn't this all seem pretty hardware specific??
Sure, but I've endeavored to keep these considerations out of the language itself. The language is only tracking a triple (frequency, phase, scale) and it is left to whatever system is running/compiling the Quil to combine those values with details about the specific hardware. Timing is the same way, if a compiler wants to care about timing (for the purposes of scheduling around decoherence) then it can, otherwise it can be ignored. Sample rates are only needed when there are both custom waveforms and the system running/compiling cares about physical time.
Why not just open source Rigetti's existing IR for pulse control?
Our existing IR is difficult to use directly since it is intended to be a compiler target rather than a target for human beings. In order to accomplish the goals listed at the top of this proposal there is a good amount of manual bookkeeping needed. Also information about high-level intentions of the programmer is lost at that level of detail.
In addition, after using our internal IR for the past year, we've learned about how to make it better, those ideas are included in this proposal. In particular:
- defining a frequency per frame means that the readout detuning problem can be solved by the compiler instead of the programmer, a very common source of error (previously the user had to keep track of the difference between the LO and the readout frequency and then do some arithmetic to calculate detuning and apply the correct kernel)
- waveform shapes (flat, gaussian) will allow optimizations and ease of use that wasn't available at the level of IQ values
- using relative (with delays) instead of absolute time
- exposing small, realtime updates to calibrations that can optionally be applied
Finally by extending Quil we can take advantage of all the great existing constructions already built in to the language, rather than re-defining all of these things again at the IR level. This includes:
- a classical memory model
- control flow
- rich support for expressions and built-in mathematical functions
- file inclusion, pragmas, and circuit definitions
- existing tools such as pyquil, qvm, and quilc