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 <https://en.wikipedia.org/wiki/Carrier_wave>`_ 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) <https://en.wikipedia.org/wiki/Numerically-controlled_oscillator>`_).
  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 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 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.

.. 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 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).

.. 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?
