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`_:cite:`mckay2018`, however, is also inspired directly or indirectly through other `efforts in the field`_:cite:`alexanderQiskitPulseProgramming2020a,DiCarloLabDelftPycQEDPy32021,ExperimentalMicroarchitectureSuperconducting,nguyenEnablingPulselevelProgramming2020,QuillangQuil2021`. 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 :ref:`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 :ref:`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. .. code-block:: openpulse 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 :math:`e^{i\left(2\pi f t + \theta\right)}`, where `f` is frequency and :math:`\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 the ``frame``). 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: 1. A ``port`` to which it is attached. This can only be set upon initialization, and never changed subsequently. 2. A frequency ``frequency`` of type ``float``. 3. A phase ``phase`` of type ``angle``. 4. A time of type ``duration`` which is manipulated implicitly and cannot be modified other than through the existing timing instructions of ``delay``, ``play``, ``capture``, and ``barrier``. The time increment is determined by the port on which the frame is played (see :ref:`Timing` section). A ``frame`` from an existing calibration can also be accessed via an ``extern`` identifier .. code-block:: openpulse 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. .. code-block:: openpulse 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 :ref:`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. .. code-block:: openpulse 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. .. code-block:: openpulse 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``. .. code-block:: openpulse 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. .. code-block:: openpulse 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``. .. code-block:: openpulse 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``: .. code-block:: openpulse :force: // 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: .. code-block:: openpulse 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 (note this syntax is still under [active development](https://github.com/Qiskit/openqasm/pull/301) and is subject to change.) 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. .. code-block:: openpulse :force: // 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). .. code-block:: openpulse // 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``). .. code-block:: openpulse play(frame fr, waveform wfm) For example, .. code-block:: openpulse :force: 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: .. code-block:: openpulse // 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. .. code-block:: 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 :ref:`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 :ref:`Timing` section for more details) e.g. .. code-block:: 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. .. code-block:: openpulse // 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. .. code-block:: openpulse 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. .. code-block:: 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. .. code-block:: 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 :ref:`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. .. code-block:: 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. .. code-block:: 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. .. code-block:: 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 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. .. code-block:: 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 ~~~~~~~~~~~~~~~~~~~~ .. code-block:: 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 ~~~~~~~~~~~~~~ .. code-block:: :force: 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. .. code-block:: :force: 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 τ 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 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). .. code-block:: :force: 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?