OpenPulse Grammar¶
The OpenPulse grammar is still in active development and is liable to change. If you are working on an implementation and find this specification unclear or not supporting your use-cases, please join our community effort to improve pulse-level support in OpenQasm.
OpenQASM allows users to provide the target system’s implementation of quantum operations
with cal
and defcal
blocks . Calibration grammars are open to extension for system implementors. In
this document, we outline one such grammar, OpenPulse, which may be selected within a supporting
compiler through the declaration defcalgrammar "openpulse";
.
This grammar is primarily motivated by the original OpenPulse specification, a JSON wire-format for pulse-level quantum programs defined in the paper `Qiskit Backend Specifications for OpenQASM and OpenPulse Experiment`_[MaTAB+18], however, is also inspired directly or indirectly through other `efforts in the field`_[Exp, Qui21, DiC21, AKE+20, NM20].
The textual format described here has several advantages over the original JSON format:
Improved readability.
Pulse timing is based on instruction ordering and works with programs containing branching control flow.
Reusable gate calibrations enable more succinct calibration descriptions.
Pulse definitions are declared as a calibration for individual circuit instructions attached to physical qubits enabling the microcoding of gate level operations.
Richer ability to compose complex pulses through natural DSP-like operations.
Openpulse provides a flexible programming model that should extend to many quantum control schemes
and hardware. At the core of the OpenPulse grammar are the concepts of port
s, waveform
s and, frame
s.
A port
is a software abstraction representing any input or output component controlling qubits. It allows
a hardware vendor to provide relevant actuation knobs they wish to expose to the user in order to manipulate and observe
qubits, while hiding the complexities of the device’s underlying hardware. A waveform
is a time-dependent envelope
that can be used to emit signals on an output port or receive signals from an input port. A frame
is also a software
abstraction that acts as both a (1) clock within the quantum program with its time being incremented on each usage and
(2) a stateful carrier signal defined by a frequency and phase. As such,
when transmitting signals to the qubit, a frame
determines time at which the waveform
envelope is emitted, its
carrier frequency, and it’s phase offset (see Play section for more details). When capturing signals from a qubit,
at minimum a frame
determines the time at which the signal is captured (see Capture section
for more details).
Note that this proposal fully supports and specifies scheduling when resources map to the qubits specified within
the defcal. However, it is possible for pulse-level resources to manipulate qubits that are not specified in the
defcal
’s signature. As a future extension the language may support conveying to the scheduling layer which
resources are acted upon by a defcal
such that the scheduler may faithfully schedule the target program to
hardware resources.
Ports¶
A port is a software abstraction representing any input or output component meant to manipulate and observe qubits. Ports are ultimately mapped to some combination of hardware resources, and there are varying versions of this mapping with differing granularity. For instance, a port may directly map to a digital-to-analog converter. Alternatively, a port may map to a combination of a digital NCO, analog-to-digital converter, local oscillator, or amplifier. A single port may even map to multiple transmit (or receive) hardware that must work in synchronicity. Ultimately, it is simply a means by which a hardware vendor can provide relevant actuation knobs they wish to expose to the user in order to manipulate and observe qubits. As such, the level of granularity of the mapping is up to the hardware vendor.
There is a many-to-many relationship between qubits and ports. One qubit may have multiple ports connecting to it. Pulses on different ports would have different physical interactions with that qubit and thereby control different operations. A port may also have many qubits. For instance, a port could manipulate the coupling between two neighboring qubits, or could even reference multiple qubits in a chain.
Ports are vendor-specific and device-specific. It is expected that vendors of quantum hardware provide the appropriate port names and qubit mappings as configuration information to end users. Each port may also have associated static settings, such as local-oscillator frequencies, which do not vary throughout program execution. Again it is expected that vendors of quantum hardware provide a method for manipulating those static settings if appropriate.
Currently all ports are bidirectional, eg., transmit and receive or in and out. It is the responsibility of the hardware target to limit the operations that may be applied to a given port.
A port is only used to specify the physical resource on which to play a pulse or from which
to capture data. The hardware can be accessed as OpenPulse port
’s via extern
identifier that specifies an external linkage that will be resolved at compile-time via vendor
supplied translation units.
extern port drive_port0
It is expected that a hardware vendor provide some documentation as to the associated functionality of a port (e.g. drive_port0 referse to the XY control line of a qubit 0).
Frames¶
When interacting with qubits, it is necessary to keep track of a frame of reference, akin to the rotating
frame of a Hamiltonian, throughout the execution of a program. Openpulse provides a software abstraction of
frame
type which is responsible for tracking two properties:
Tracking time appropriately so programs do not need to deal in absolute time or with the bookkeeping of advancing time in a sequence of pulses.
Tracking accrued phase by producing a complex value given an input time (i.e. via the mathematical relationship \(e^{i\left(2\pi f t + \theta\right)}\), where f is frequency and \(\theta\) is the accrued phase). In this way, a
frame
type behaves analogously to a numerically-controlled oscillator (NCO)). One motivation for keeping track of accrued phase is to allow pulses to be defined in the rotating frame with the effect being an equivalent application in the lab frame (i.e. with the carrier supplied by theframe
). Another motivation is to more naturally implement a “virtual Z-gate”, which does not require a physical pulse but rather shifts the phase of all future pulses on that frame.
The frame is composed of four parts:
A
port
to which it is attached. This can only be set upon initialization, and never changed subsequently.A frequency
frequency
of typefloat
.A phase
phase
of typeangle
.A time of type
duration
which is manipulated implicitly and cannot be modified other than through the existing timing instructions ofdelay
,play
,capture
, andbarrier
. The time increment is determined by the port on which the frame is played (see Timing section).
A frame
from an existing calibration can also be accessed via an extern
identifier
extern frame xy_frame0
Note that a frame
type is a virtual resource and it is up to the hardware vendor’s backend compiler
to choose how to implement the required transformations to physical resources in hardware during the machine code
generation phase.
Frame Initialization¶
Frames can be initialized using the newframe
command by providing the port
, frequency
, and phase
e.g.
extern port drive0;
frame driveframe0 = newframe(drive0, 5e9, 0.0); // newframe(port pr, float[size] frequency, angle[size] phase)
would initialize a frame on the drive0
port with a frequency of 5 GHz, and phase of 0.0. Importantly,
a frame can be initializated in either a cal
or defcal
block which means that the time with which it is
initialized is the start time of the containing block (see Timing section for more details).
If a compiler toolchain is unable to support the initialization of frame
s within defcal
s, it is expected
to raise a compile-time error when such an initialization is encountered.
Note that multiple frames may address the same port e.g.
extern port measure_port;
frame measure_frame_0 = newframe(measure_port, 5e9, 0.0);
frame measure_frame_1 = newframe(measure_port, 5e9, 0.0);
frame measure_frame_2 = newframe(measure_port, 5e9, 0.0);
frame measure_frame_3 = newframe(measure_port, 5e9, 0.0);
The limitation on the number of frames that may address the same port depends entirely on hardware vendor
and how they choose to map frame
s to physical resources during the backend machine code generation phase.
For example, a hardware vendor may choose to collapse all frame
s attached to the same port into to a single
NCO in analogy to virtual to physical register allocation.
Frame Manipulation¶
The phase
and frequency
states of a frame can be manipulated throughout the program
by using set
and shift
instructions and read using a get
instruction. In particular,
the set_phase and shift_phase instructions allow one to supply the frame and a value of type
angle
representing the amount by which to set/shift the phase.
set_phase(frame fr, angle phase);
shift_phase(frame fr, angle phase);
The get_phase instruction allows one to supply the frame from which to retrieve the phase of
type angle
.
get_phase(frame fr) -> angle;
Analogously, the set_frequency and shift_frequency instructions allow one to supply the frame
and a value of type float
representing the amount by which to set/shift the frequency.
set_frequency(frame fr, float freq);
shift_frequency(frame fr, float freq);
The get_frequency instruction allows one to supply the frame from which to retrieve the frequency
of type float
.
get_frequency(frame fr) -> float;
Changing the frequency or phase behaves as an instantaneous operation (ie., its duration is zero device ticks) at the current time point of the frame. If a vendor is unable to support such instantaneous operations, it is expected that the compiler shall raise a compile-time error when encountering such frame manipulations.
The exact precision and range of the frequency is hardware specific, and it is likely hardware vendors will perform a float to fixed conversion in the backend. If the frequency is set to an out of bounds value, the compiler shall raise a compile-time error.
Here’s an example of manipulating the phase to calibrate an rz
gate on a frame called
driveframe
:
// Shift phase of the "drive" frame by pi/4, to realize a virtual rz gate with angle -pi/4
cal {
shift_phase(driveframe, pi/4);
}
// The following is an example only. Frames as arrays has not been agreed on.
// This conceptually must be compile-time arrays and treat qubits as indices
// which also has not been well-defined. We are exploring other solutions to
// the problem of mapping qubits to pulse-level resources.
// Define a calibration for the rz gate on all 8 physical qubits
cal {
array[frame, 8] rz_frames;
frame[0] = newframe(...);
// and so on
}
defcal rz(angle[20] theta) q {
shift_phase(rz_frames[q], -theta);
}
Manipulating frames based on the state of other frames is also permitted:
angle temp1 = get_phase(frame1);
angle temp2 = get_phase(frame2);
set_phase(frame1, temp2);
set_phase(frame2, temp1);
Waveforms¶
Waveforms are of type waveform
and can either be:
An array of complex samples which define the points for the waveform envelope
An abstract mathematical function representing a waveform. This will later be materialized into a list of complex samples, either by the compiler or the hardware using the parameters provided to the
extern
declared waveform template.
A value of type waveform
may be defined either by explicitly constructing the complex samples
or by calling one of the waveform template functions provided by the target device.
Note that each of these extern functions takes a type duration
as an argument,
since waveforms must have a definite duration.
Using the hardware dependent dt
unit is recommended for this duration,
since otherwise the compiler may need to down-sample a higher precision
waveform to physically realize it.
Like other extern functions, extern waveform
functions will be compiled.
But for static waveforms, the optimizing compiler should decide to execute this
at compile time and load the waveform into memory once.
For dynamic waveforms, the compiler just compiles and links this, to be executed at runtime.
We provide the waveform
type in addition to the complex list of samples to
provide more context to compilers and hardware. For example, some hardware pulse
generators may have optimized implementations of common pulse shapes like gaussians.
Providing structured gaussian parameters instead of the materialized list of complex
samples provides optimization opportunities that wouldn’t be available otherwise.
// arbitrary complex samples
waveform arb_waveform = [1+0im, 0+1im, 1/sqrt(2)+1/sqrt(2)im];
// amp is waveform amplitude at center
// d is the overall duration of the waveform
// sigma is the standard deviation of waveform
extern gaussian(complex[float[size]] amp, duration d, duration sigma) -> waveform;
// amp is waveform amplitude at center
// d is the overall duration of the waveform
// sigma is the standard deviation of waveform
extern sech(complex[float[size]] amp, duration d, duration sigma) -> waveform;
// amp is waveform amplitude at center
// d is the overall duration of the waveform
// square_width is the width of the square waveform component
// sigma is the standard deviation of waveform
extern gaussian_square(complex[float[size]] amp, duration d, duration square_width, duration sigma) -> waveform;
// amp is waveform amplitude at center
// d is the overall duration of the waveform
// sigma is the standard deviation of waveform
// beta is the Y correction amplitude, see the DRAG paper
extern drag(complex[float[size]] amp, duration d, duration sigma, float[size] beta) -> waveform;
// amp is waveform amplitude
// d is the overall duration of the waveform
extern constant(complex[float[size]] amp, duration d) -> waveform;
// amp is waveform amplitude
// d is the overall duration of the waveform
// frequency is the frequency of the waveform
// phase is the phase of the waveform
extern sine(complex[float[size]] amp, duration d, float[size] frequency, angle[size] phase) -> waveform;
We can manipulate the waveform
types using the following signal processing functions to produce
new waveforms (this list may be updated as more functionality is required).
// Multiply two input waveforms entry by entry to produce a new waveform
// :math:`wf(t_i) = wf_1(t_i) \times wf_2(t_i)`
mix(waveform wf1, waveform wf2) -> waveform;
// Sum two input waveforms entry by entry to produce a new waveform
// :math:`wf(t_i) = wf_1(t_i) + wf_2(t_i)`
sum(waveform wf1, waveform wf2) -> waveform;
// Add a relative phase to a waveform (ie multiply by :math:`e^{\imag \theta}`)
phase_shift(waveform wf, angle ang) -> waveform;
// Scale the amplitude of a waveform's samples producing a new waveform
scale(waveform wf, float factor) -> waveform;
Play instruction¶
Waveforms are scheduled using the play
instruction. These instructions may
only appear inside a defcal
block and have two required parameters:
The frame to use for the pulse.
A value of type
waveform
representing the waveform envelope.
Here, the frame
provides the time at which the waveform
envelope is scheduled (i.e. via
the frame’s current time
), its carrier frequency (i.e. via the frames current frequency
),
and its phase offset (i.e. via the frame’s current phase
).
play(frame fr, waveform wfm)
For example,
defcal play_my_pulses $0 {
// Play a 3 sample pulse on the tx0 port
play(driveframe, [1+0im, 0+1im, 1/sqrt(2)+1/sqrt(2)im]);
// Play a gaussian pulse on the tx1 port
frame f1 = newframe(tx1, q1_freq, 0.0);
play(f1, gaussian(...));
}
If the waveform
duration is not realizable by the sample rate of the associated port
,
the compiler shall raise a compile-time error.
Capture Instruction¶
Acquisition is scheduled by a capture
instruction. This is a special
extern
function which is specified by a hardware vendor. The measurement
process is difficult to describe generically due to the wide variety of
hardware and measurement methods. Like the play
instruction, these instructions
may only appear inside a defcal
or cal
block.
The minimum requirement for a capture
command is that the frame
provides the time at
which data is captured. As such, the only required parameter for a capture
instruction
is a frame
.
However, the following are possible parameters that might also be included:
A “duration” of type
duration
, if it cannot be inferred from other parameters.A “filter” of type
waveform
, which is dot product-ed with the measured IQ to distill the result into a single IQ value
Again it is up to the hardware vendor to determine the parameters and write a extern definition at the top-level, such as:
// Minimum requirement
extern capture_v0(frame output);
// A capture command that returns an iq value
extern capture_v1(frame output, waveform filter) -> complex[float[32]];
// A capture command that returns a discrimnated bit
extern capture_v2(frame output, waveform filter) -> bit;
// A capture command that returns a raw waveform data
extern capture_v3(frame output, duration len) -> waveform;
// A capture that returns a count e.g. number of photons detected
extern capture_v4(frame output, duration len) -> int
The return type of a capture
command varies. It could be a raw trace, ie., a
list of samples taken over a short period of time. It could be some averaged IQ
value. It could be a classified bit. Or it could even have no return value,
pushing the results into some buffer which is then accessed outside the program.
For example, the capture
instruction could return raw waveform data that is then
discriminated using user-defined boxcar and discrimination extern
s.
defcalgrammar "openpulse";
cal {
// Use a boxcar function to generate IQ data from raw waveform
extern boxcar(waveform input) -> complex[float[64]];
// Use a linear discriminator to generate bits from IQ data
extern discriminate(complex[float[64]] iq) -> bit;
// Define the ports
extern port m0;
extern port cap0;
}
defcal measure $0 -> bit {
// Force time of carrier to 0 for consistent phase for discrimination.
frame stimulus_frame = newframe(m0, 5e9, 0);
frame capture_frame = newframe(cap0, 5e9, 0);
// Measurement stimulus envelope
waveform meas_wf = gaussian_square(1.0, 16000dt, 262dt, 13952dt);
// Play the stimulus
play(stimulus_frame, meas_wf);
// Align measure and capture frames
barrier stimulus_frame, capture_frame;
// Capture transmitted data after interaction with measurement resonator
// extern capture_v1(frame capture_frame, duration duration) -> waveform;
waveform raw_output = capture_v1(capture_frame, 16000dt);
// Kernel and discriminate
complex[float[32]] iq = boxcar(raw_output);
bit result = discriminate(iq);
return result;
}
If the duration
argument or the waveform
duration are not realizable by the sample rate of
the associated port
, the compiler shall raise a compile-time error.
Timing¶
Each frame maintains its own “clock” of type duration
, which can only be manipulated implicitly
through the existing timing instructions of delay
, play
, capture
, and barrier
.
Initial Time¶
As briefly discussed in the Frame Initialization section, a frame
initialized via a
newframe
command has its .time
set to the time at the beginning of the containing
cal
or defcal
block. Since a cal
block is globally scoped in OpenPulse, this time
would be absolute 0. Meanwhile, a defcal
s start time is determined by when it is scheduled
(see Timing section for more details) e.g.
defcalgrammar "openpulse";
cal {
extern port d0;
// initialized with absolute time 0 because `cal` is global scope
frame driveframe1 = newframe(d0, 5.0e9, 0.0);
waveform wf = gaussian(0.5, 16ns, 4ns);
}
defcal my_gate1 $0 {
play(driveframe1, wf);
}
defcal my_gate2 $0 {
// initialized to time at beginning of `my_gate2`
frame driveframe2 = newframe(d0, 5.0e9, 0.0);
play(driveframe2, wf);
}
defcal my_gate3 $0 {
// initialized to time at beginning of `my_gate3`
frame driveframe3 = newframe(d0, 5.0e9, 0.0);
play(driveframe3, wf);
}
// driveframe1.time = 0ns when `play(driveframe1, wf)` is issued, advances to 16ns after `play`
my_gate1 $0;
// driveframe2.time = 16ns when initialized via `newframe`
my_gate2 $0;
// driveframe3.time = 32ns when initialized via `newframe`
my_gate3 $0;
Delay¶
When a delay
instruction is issued for a list of frame
s, the frame
clocks advance
by the requested duration.
// driveframe advances by 13ns
delay[13ns] driveframe;
If the duration
argument of the delay is not realizable by the sample rate of
the underlying port
, the compiler shall raise a compile-time error.
Play and Capture¶
When a play
or capture
instruction is issued, the frame
clock advances
by the duration of the associated waveform
argument.
cal {
extern port d0;
frame driveframe = newframe(d0, 5.0e9, 0.0);
waveform wf = gaussian(0.5, 16ns, 4ns);
}
delay[13ns] driveframe;
// driveframe.time is now 13ns
play(driveframe, wf);
// driveframe.time is now 29ns
Barrier¶
When a barrier
instruction is issued for a list of frame
s, the frame
clocks are
aligned to the latest time of the all frame
s listed.
defcalgrammar "openpulse";
cal {
extern port d0;
extern port d1;
driveframe1 = newframe(d0, 5.1e9, 0.0);
driveframe2 = newframe(d1, 5.2e9, 0.0);
delay[13ns] driveframe1;
// driveframe1.time == 13ns, driveframe2.time == 0ns
// Align frames
barrier driveframe1, driveframe2;
// driveframe1.time == driveframe2.time == 13ns
}
Moreover, defcal
blocks have an implicit barrier
on every frame enters the block e.g.
defcalgrammar "openpulse";
cal {
extern port tx0;
extern port tx1;
waveform p = /* ... some 100ns waveform ... */;
frame driveframe1 = newframe(tx0, 5.0e9, 0);
frame driveframe2 = newframe(tx1, 6.0e9, 0);
}
defcal two_qubit_gate $1 $2 {
// implicit: barrier driveframe1, driveframe2;
play(driveframe1, wf);
play(driveframe2, wf);
}
defcal single_qubit_gate $1 {
// implicit: barrier driveframe1;
play(driveframe1, wf);
}
single_qubit_gate $1;
// Implicit alignment of `driveframe1` and `driveframe2` when entering `two_qubit_gate` block
two_qubit_gate $1 $2;
Phase tracking¶
As discussed in the Frame Manipulation section, the accrued phase of a frame can be
manipulated throughout a program via set_phase
and shift_phase
instructions. In addition,
the phase is implicitly manipulated when the time of the frame is advanced using a delay
,
play
, or capture
instruction e.g.
defcalgrammar "openpulse";
cal {
extern port tx0;
waveform p = /* ... some 100ns waveform ... */;
// Frame initialized with accrued phase of 0
frame driveframe0 = newframe(tx0, 5.0e9, 0);
}
defcal single_qubit_gate $0 {
play(driveframe0, wf);
}
defcal single_qubit_delay $0 {
delay[13ns] driveframe0;
}
// get_phase(driveframe0) == 0
single_qubit_gate $0;
// Implicit advancement: -> shift_phase(driveframe0, 2π * get_frequency(driveframe0) * durationof(wf))
// = shift_phase(driveframe0, 2π * 5e9 * 100e-9)
// Change the frequency
cal {
set_frequency(driveframe0, 6e9);
}
single_qubit_delay $0;
// Implicit advancement: -> set_phase(driveframe0, 2π * get_frequency(driveframe0) * 13e-9)
// = set_phase(driveframe0, 2π * 6e9 * 13e-9)
This is a key property required for pulses to be defined in the rotating frame with the effect being an equivalent application in the lab frame.
Collisions¶
If a frame is scheduled or referenced simultaneously in two defcal
or cal
blocks, it is
considered a compile-time error e.g.
defcalgrammar "openpulse";
defcal single_qubit_gate $0 {
play(driveframe1, wf);
}
defcal single_qubit_gate $1 {
play(driveframe1, wf);
}
// Compile-time error when requesting parallel usage of the same frame
single_qubit_gate $0 $1;
Examples¶
Rabi Spectroscopy¶
Rabi spectroscopy experiments consist of a pulse that drives the qubit transition followed by a measurement. Exploring the response to sweeps of pulse frequency, time, amplitude, or even multi-dimensional sweeps reveals spectroscopic information about the qubit transition frequencies and the drive strength. We describe these circuits with a mixture of conventional OpenQASM for the simple pulse and measure sequence and step into cal blocks to access pulse level control. We assume that the OpenQASM text is generated by some higher level language bindings and we only write into the program the sweep where we can take advantage of the execution speed of sweeping as part of the program.
Qubit Spectroscopy
Here we want to sweep the frequency of a long pulse that saturates the qubit transition.
defcalgrammar "openpulse";
// sweep parameters would be programmed in by some higher level bindings
const float frequency_start = 4.5e9;
const float frequency_step = 1e6
const int frequency_num_steps = 301;
// define a long saturation pulse of a set duration and amplitude
defcal saturation_pulse $0 {
// assume frame can be linked from a vendor supplied `cal` block
play(driveframe, constant(0.1, 100e-6));
}
// step into a `cal` block to set the start of the frequency sweep
cal {
set_frequency(driveframe, frequency_start);
}
for int i in [1:frequency_num_steps] {
// step into a `cal` block to adjust the pulse frequency via the frame frequency
cal {
shift_frequency(driveframe, frequency_step);
}
saturation_pulse $0;
measure $0;
}
Rabi Time Spectroscopy
Here we want to sweep the time of the pulse and observe coherent Rabi flopping dynamics.
defcalgrammar "openpulse";
const duration pulse_length_start = 20dt;
const duration pulse_length_step = 1dt;
const int pulse_length_num_steps = 100;
for int i in [1:pulse_length_num_steps] {
duration pulse_length = pulse_length_start + (i-1)*pulse_length_step);
duration sigma = pulse_length / 4;
// since we are manipulating pulse lengths it is easier to define and play the waveform in a `cal` block
cal {
waveform wf = gaussian(0.5, pulse_length, sigma);
// assume frame can be linked from a vendor supplied `cal` block
play(driveframe, wf);
}
measure $0;
}
Cross-resonance gate¶
defcalgrammar "openpulse";
cal {
// Access globally (or externally) defined ports
extern port d0;
extern port d1;
frame frame0 = newframe(d0, 5.0e9, 0);
}
defcal cross_resonance $0, $1 {
waveform wf1 = gaussian_square(1., 1024dt, 128dt, 32dt);
waveform wf2 = gaussian_square(0.1, 1024dt, 128dt, 32dt);
/*** Do pre-rotation ***/
// generate new frame for second drive that is locally scoped
// initialized to time at the beginning of `cross_resonance`
frame temp_frame = newframe(d1, get_frequency(frame0), get_phase(frame0));
play(frame0, wf1);
play(temp_frame, wf2);
/*** Do post-rotation ***/
}
Geometric gate¶
defcalgrammar "openpulse";
cal {
extern port dq;
float fq_01 = 5e9; // hardcode or pull from some function
float anharm = 300e6; // hardcode or pull from some function
frame frame_01 = newframe(dq, fq_01, 0);
frame frame_12 = newframe(dq, fq_01 + anharm, 0);
}
defcal geo_gate(angle[32] theta) q {
// theta: rotation angle (about z-axis) on Bloch sphere
// Assume we have calibrated 0->1 pi pulses and 1->2 pi pulse
// envelopes (no sideband)
waveform X_01 = { ... };
waveform X_12 = { ... };
float[32] a = sin(theta/2);
float[32] b = sqrt(1-a**2);
// Double-tap
play(frame_01, scale(a, X_01));
play(frame_12, scale(b, X_12));
play(frame_01, scale(a, X_01));
play(frame_12, scale(b, X_12));
}
Neutral atoms¶
In this example, the signal chain is composed of two electro-optic modulators (EOM) and an acousto-optic deflector (AOD). The EOMs put sidebands on the laser light while the AOD diffracts the light in an amount proportional to the frequency of the RF drive. This example was chosen because it is similar in spirit to the work by Levine et al._:cite:levine2019 except that phase control is exerted using virtual Z gates on the AODs – requiring frame tracking of the qubit frequency yet application of a tone that maps to the qubit position (i.e. requires the use of a sideband).
The program aims to perform a Hahn echo sequence on q1, and a Ramsey sequence on q2 and q3.
defcalgrammar "openpulse";
// Raman transition detuning Δ from the 5S1/2 to 5P1/2 transition
const float Δ = ...;
// Hyperfine qubit frequency
const float qubit_freq = ...;
// Positional frequencies for the AODS to target the specific qubit
const float q1_pos_freq = ...;
const float q2_pos_freq = ...;
const float q3_pos_freq = ...;
// Calibrated amplitudes and durations for the Raman pulses supplied via the AOD envelopes
const float q1_π_half_amp = ...;
const float q2_π_half_amp = ...;
const float q3_π_half_amp = ...;
const duration π_half_time = ...;
// Time-proportional phase increment
const float tppi_1 = ...;
const float tppi_2 = ...;
const float tppi_3 = ...;
cal {
extern port eom_a_port;
extern port eom_b_port;
extern port aod_port;
// Define the Raman frames, which are detuned by an amount Δ from the 5S1/2 to 5P1/2 transition
// and offset from each other by the qubit_freq
frame raman_a_frame = newframe(eom_a_port, Δ, 0.0);
frame raman_b_frame = newframe(eom_b_port, Δ-qubit_freq, 0.0);
// Three frames to phase track each qubit's rotating frame of reference at it's frequency
frame q1_frame = newframe(aod_port, qubit_freq, 0)
frame q2_frame = newframe(aod_port, qubit_freq, 0)
frame q3_frame = newframe(aod_port, qubit_freq, 0)
// Generic gaussian envelope
waveform π_half_sig = gaussian(1.0, π_half_time, 100dt);
// Waveforms ultimately supplied to the AODs. We mix our general Gaussian pulse with a sine wave to
// put a sideband on the outgoing pulse. This helps us target the qubit position while maintainig the
// desired Rabi rate.
waveform q1_π_half_sig = mix(π_half_sig, sine(q1_π_half_amp, π_half_time, q1_pos_freq-qubit_freq, 0.0));
waveform q2_π_half_sig = mix(π_half_sig, sine(q2_π_half_amp, π_half_time, q2_pos_freq-qubit_freq, 0.0));
waveform q3_π_half_sig = mix(π_half_sig, sine(q3_π_half_amp, π_half_time, q3_pos_freq-qubit_freq, 0.0));
}
// π/2 pulses on all three qubits
defcal rx(π/2) $1 $2 $3 {
// Simultaneous π/2 pulses
play(raman_a_frame, constant(raman_a_amp, π_half_time));
play(raman_b_frame, constant(raman_b_amp, π_half_time));
play(q1_frame, q1_π_half_sig);
play(q2_frame, q2_π_half_sig);
play(q3_frame, q3_π_half_sig);
}
// π/2 pulse on only qubit $2
defcal rx(π/2) $2 {
play(raman_a_frame, constant(raman_a_amp, π_half_time));
play(raman_b_frame, constant(raman_b_amp, π_half_time));
play(q2_frame, q2_π_half_sig);
}
// Ramsey sequence on qubit 1 and 3, Hahn echo on qubit 2
for duration τ in [0:10us:1ms] {
// First π/2 pulse
rx(π/2) $0, $1, $2;
// First half of evolution time
cal {
delay[τ/2] raman_a_frame raman_b_frame q1_frame q2_frame q3_frame;
}
// Hahn echo π pulse composed of two π/2 pulses
for int ct in [0:1]:
rx(π/2) $2;
cal {
// Align all frames
barrier raman_a_frame raman_b_frame q1_frame q2_frame q3_frame;
// Second half of evolution time
delay[τ/2] raman_a_frame raman_b_frame q1_frame q2_frame q3_frame;
// Time-proportional phase increment signals different amount
shift_phase(q1_frame, tppi_1 * τ);
shift_phase(q2_frame, tppi_2 * τ);
shift_phase(q3_frame, tppi_3 * τ);
}
// Second π/2 pulse
rx(π/2) $0, $1, $2;
Multiplexed readout and capture¶
In this example, we want to perform readout and capture of a pair of qubits, but mediated by a single physical transmission and capture port. The example is for just two qubits, but works the same for many (just adding more frames, waveforms, plays, and captures).
defcalgrammar "openpulse";
const duration electrical_delay = ...;
const float q0_ro_freq = ...;
const float q1_ro_freq = ...;
cal {
// the transmission/captures ports are the same for $0 and $1
extern port ro_tx;
extern port ro_rx;
// readout stimulus and capture frames of different frequencies
frame q0_stimulus_frame = newframe(ro_tx, q0_ro_freq, 0);
frame q0_capture_frame = newframe(ro_rx, q0_ro_freq, 0);
frame q1_stimulus_frame = newframe(ro_tx, q1_ro_freq, 0);
frame q1_capture_frame = newframe(ro_rx, q1_ro_freq, 0);
}
defcal multiplexed_readout_and_capture $0, $1 -> bit[2] {
bit[2] b;
// flat-top readout waveforms
waveform q0_ro_wf = constant(amp=0.1, d=...);
waveform q1_ro_wf = constant(amp=0.2, d=...);
// multiplexed readout
play(q0_stimulus_frame, q0_ro_wf);
play(q1_stimulus_frame, q1_ro_wf);
// simple boxcar kernel
waveform ro_kernel = constant(amp=1, d=...);
barrier q0_stimulus_frame q1_stimulus_frame q0_capture_frame q1_capture_frame;
delay[electrical_delay] q0_capture_frame q1_capture_frame;
// multiplexed capture
// extern capture(frame capture_frame, waveform ro_kernel) -> bit;
b[1] = capture(q0_capture_frame, ro_kernel);
b[2] = capture(q1_capture_frame, ro_kernel);
return b;
}
Open Questions¶
How do we handle mapping wildcarded qubits to arbitrary pulse-level resources?
Is timing on frames, and ports as resources clear?
How will hardware attributes be handled?