Tutorial: Schedules and Pulses#

See also

The complete source code of this tutorial can be found in

Schedules and Pulses.ipynb

The Schedule#

The main data structure that describes an experiment in the quantify-scheduler is the Schedule. We will show how the Schedule works through an example.

from quantify_scheduler import Schedule

sched = Schedule("Hello quantum world!")

sched
Schedule "Hello quantum world!" containing (0) 0  (unique) operations.

As we can see, our newly created schedule is still empty. We need to manually add operations to it. In quantify-scheduler there are three types of operations: pulses, acquisitions and gates. All of these have explicit timing control. In this tutorial, we will only cover pulses. The goal will not be to make a schedule that is physically meaningful, but to demonstrate the control over the scheduling to its fullest.

While it is possible to define a pulse completely from scratch, we will be using some of the pulse definitions provided with the quantify-scheduler. These pulses are described in the quantify_scheduler.operations.pulse_library submodule. It’s worth noting that no sampling of the data yet occurs at this stage, but the pulse is kept in a parameterized form.

We will add a square pulse from the pulse library to the schedule.

from quantify_scheduler.operations import pulse_library

square_pulse = sched.add(
    pulse_library.SquarePulse(amp=1, duration=1e-6, port="q0:res", clock="q0.ro")
)

sched
Schedule "Hello quantum world!" containing (1) 1  (unique) operations.

You may have noticed that we passed a port and a clock to the pulse. The port specifies the physical location on the quantum chip to which we are sending the pulses, whilst the clock tracks the frequency of the signal (see Ports and clocks). This clock frequency has not yet been defined, so prior to any compilation step this clock needs to be added to the schedule as a resource.

from quantify_scheduler.resources import ClockResource

readout_clock = ClockResource(name="q0.ro", freq=7e9)
sched.add_resource(readout_clock)

sched
Schedule "Hello quantum world!" containing (1) 1  (unique) operations.

quantify-scheduler provides several visualization tools to show a visual representation of the schedule we made. First, however, we need to instruct the scheduler to calculate the pulse timings. We can accomplish this using the determine_absolute_timing() function. In the cell below we call this function, and draw the schedule using a pulse diagram.

Note that these plots are interactive and modulation is not shown by default.

from quantify_scheduler import compilation

timed_sched = compilation.determine_absolute_timing(sched)
timed_sched.plot_pulse_diagram(plot_backend="plotly")

Explicit timing control#

What we see in the pulse diagram is only a flat line, corresponding to our single square pulse. To make our schedule more interesting, we should add more pulses to it. We will add another square pulse, but with a 500 ns delay.

sched.add(
    pulse_library.SquarePulse(amp=1, duration=1e-6, port="q0:res", clock="q0.ro"),
    ref_op=square_pulse,
    rel_time=500e-9,
)

timed_sched = compilation.determine_absolute_timing(sched)
timed_sched.plot_pulse_diagram(plot_backend="plotly")

We can see that rel_time=500e-9 schedules the pulse 500 ns shifted relative to the end of the ref_op. If no additional arguments are passed, operations are added directly after the operation that was added last.

Let’s now instead align a pulse to start at the same time as the first square pulse. Before, we specified the timing relative to the end of a different pulse, but we can choose to instead specify it relative to the beginning. This is done by passing ref_pt="start".

sched.add(
    pulse_library.DRAGPulse(
        G_amp=0.5, D_amp=0.5, duration=1e-6, phase=0, port="q0:mw", clock="q0.01"
    ),
    ref_op=square_pulse,
    ref_pt="start",
)
sched.add_resource(ClockResource(name="q0.01", freq=7e9))

timed_sched = compilation.determine_absolute_timing(sched)
timed_sched.plot_pulse_diagram(plot_backend="plotly")

We see that we added a DRAG pulse to the schedule. Two things stand out:

  1. The DRAG pulse is plotted separately from the square pulse, this is because we specified a different port for this pulse than we did for the square pulse.

  2. The DRAG pulse shows two lines instead of one. This is because a DRAG pulse is specified as a complex-valued pulse, so we have to plot both the I and Q components of the signal. The real part of the waveform is shown in color, whereas the imaginary component is shown in grayscale.

Parameterized schedules#

In an experiment, often the need arises to vary one of the parameters of a schedule programmatically. Currently, the canonical way of achieving this is by defining a function that returns a generated schedule. We will use this to generate a pulse train, where we can specify the timing parameters separately.

from quantify_scheduler.resources import BasebandClockResource


def pulse_train_schedule(
    amp: float, time_high: float, time_low: float, amount_of_pulses: int
) -> Schedule:
    sched = Schedule("Pulse train schedule")
    square_pulse = sched.add(
        pulse_library.SquarePulse(
            amp=amp,
            duration=time_high,
            port="q0:fl",
            clock=BasebandClockResource.IDENTITY,
        ),
    )
    for _ in range(amount_of_pulses - 1):
        square_pulse = sched.add(
            pulse_library.SquarePulse(
                amp=amp,
                duration=time_high,
                port="q0:fl",
                clock=BasebandClockResource.IDENTITY,
            ),
            rel_time=time_low,
            ref_op=square_pulse,
        )
    return sched


sched = pulse_train_schedule(1, 200e-9, 300e-9, 5)
timed_sched = compilation.determine_absolute_timing(sched)
timed_sched.plot_pulse_diagram(plot_backend="plotly")

Note that we used the BasebandClockResource as a clock, which is always at 0 Hz and was added automatically to the schedule for convenience. We can see that the pulses start every 500 ns and are 200 ns long.

Long waveforms via StitchedPulse#

Note

This feature is only available for Qblox hardware. More information about this feature can also be found in section Long waveform support.

The sequencers in Qblox modules have a waveform sample limit of MAX_SAMPLE_SIZE_WAVEFORMS. Trying to play many (long) waveforms might cause you to exceed this limit. For certain waveforms, however, it is possible to use the available memory more efficiently. This section explains how to do this with the StitchedPulse.

For convenience, quantify-scheduler provides helper functions for the square (long_square_pulse()), ramp (long_ramp_pulse()) and staircase (staircase_pulse()) waveforms for when they become too long to fit into the waveform memory of the hardware.

from quantify_scheduler.operations.pulse_factories import (
    long_ramp_pulse,
    long_square_pulse,
    staircase_pulse,
)


sched = Schedule("Basic long pulses")
sched.add(
    long_square_pulse(
        amp=0.5,
        duration=10e-6,
        port="q0:fl",
        clock=BasebandClockResource.IDENTITY,
    ),
)
sched.add(
    long_ramp_pulse(
        amp=1.0,
        duration=10e-6,
        port="q0:fl",
        offset=-0.5,
        clock=BasebandClockResource.IDENTITY,
    ),
    rel_time=5e-7,
)
sched.add(
    staircase_pulse(
        start_amp=-0.5,
        final_amp=0.5,
        num_steps=20,
        duration=10e-6,
        port="q0:fl",
        clock=BasebandClockResource.IDENTITY,
    ),
    rel_time=5e-7,
)

timed_sched = compilation.determine_absolute_timing(sched)
timed_sched.plot_pulse_diagram(plot_backend="plotly")

Using these factory functions, the resulting square and staircase pulses use no waveform memory at all. The ramp pulse uses waveform memory for a short section of the waveform, which is repeated multiple times.

For more complicated shapes, the StitchedPulseBuilder makes it possible to stitch together pulse shapes yourself. In the following example, we create a long soft square pulse where the constant-voltage middle part is created with a voltage offset instruction, using no waveform memory.

import numpy as np

from quantify_scheduler.operations.pulse_library import NumericalPulse
from quantify_scheduler.operations.stitched_pulse import StitchedPulseBuilder


# Define a few constants
port = "q0:fl"
clock = BasebandClockResource.IDENTITY
ramp_duration = 4e-6
constant_duration = 8e-6

ramp_t = np.arange(0, round(ramp_duration * 1e9) + 1) * 1e-9

# Define the waveforms for the up and down ramps
hann_up = ramp_t / ramp_duration - 1 / 2 / np.pi * np.sin(
    2 * np.pi / ramp_duration * ramp_t
)
hann_down = 1 - hann_up

# Make the stitched pulse
builder = StitchedPulseBuilder(port=port, clock=BasebandClockResource.IDENTITY)
builder.add_pulse(
    NumericalPulse(samples=hann_up, t_samples=ramp_t, port=port, clock=clock)
)
builder.add_voltage_offset(path_0=1.0, path_1=0.0, duration=constant_duration)
builder.add_pulse(
    NumericalPulse(samples=hann_down, t_samples=ramp_t, port=port, clock=clock)
)
pulse = builder.build()

sched = Schedule("Long soft square pulse")
sched.add(pulse)

timed_sched = compilation.determine_absolute_timing(sched)
timed_sched.plot_pulse_diagram(plot_backend="plotly")

Alternatively, the building methods of the StitchedPulseBuilder can be conveniently chained to create a StitchedPulse via more elegant syntax:

pulse = (
    StitchedPulseBuilder(port=port, clock=BasebandClockResource.IDENTITY)
    .add_pulse(
        NumericalPulse(samples=hann_up, t_samples=ramp_t, port=port, clock=clock)
    )
    .add_voltage_offset(path_0=1.0, path_1=0.0, duration=constant_duration)
    .add_pulse(
        NumericalPulse(samples=hann_up, t_samples=ramp_t, port=port, clock=clock)
    )
    .build()
)