Voltage offsets and long waveforms#

In this section we introduce how to use voltage offsets and build up long waveforms using Qblox Cluster modules.

Voltage offsets#

Qblox modules can set and hold a voltage on their outputs using the VoltageOffset operation. The operation supports real and complex outputs, and it has effectively zero duration, meaning it takes effect at the exact moment you schedule it, and you can schedule other operations simultaneously. It can be used as follows:

 1from quantify_scheduler.operations.pulse_library import VoltageOffset
 2
 3
 4voltage_offset_real = VoltageOffset(
 5    offset_path_I=0.5,
 6    offset_path_Q=0.0,
 7    port="q3:mw",
 8    clock="q3.01",
 9)
10
11voltage_offset_complex = VoltageOffset(
12    offset_path_I=0.5,
13    offset_path_Q=0.5,
14    port="q3:mw",
15    clock="q3.01",
16)
17
18sched = Schedule("offset_sched")
19sched.add_resource(ClockResource(name="q3.01", freq=9e9))
20
21ref_op = sched.add(voltage_offset_real)
22
23# It's possible to schedule a voltage offset simultaneously with a pulse
24sched.add(voltage_offset_complex, ref_op=ref_op, rel_time=1e-7)
25sched.add(SquarePulse(amp=1, duration=1e-7, port="q3:mw", clock="q3.01"))
26
27compiler = SerialCompiler(name="compiler")
28compiled_sched = compiler.compile(
29    schedule=sched, config=quantum_device.generate_compilation_config()
30)

Note that the offset will remain on the output until the schedule execution ends, or until a new voltage offset is set.

Important

While voltage offsets have effectively zero duration, the hardware does require 4 ns of “buffer” time after they are scheduled. That is, voltage offsets cannot be scheduled right at the end of the schedule, or at the end of a Control flow block (e.g., a loop).

If you do want a voltage offset at those moments, it is necessary to leave some time (at least 4 ns) by inserting an IdlePulse.

For example, the following is not possible:

 1from quantify_scheduler.operations.pulse_library import IdlePulse
 2
 3sched = Schedule("offset_sched")
 4sched.add_resource(ClockResource(name="q3.01", freq=9e9))
 5
 6sched.add(
 7    VoltageOffset(offset_path_I=0.5, offset_path_Q=0.0, port="q3:mw", clock="q3.01")
 8)
 9sched.add(SquarePulse(amp=1, duration=1e-7, port="q3:mw", clock="q3.01"))
10sched.add(
11    VoltageOffset(offset_path_I=0.0, offset_path_Q=0.0, port="q3:mw", clock="q3.01")
12)
13
14compiler = SerialCompiler(name="compiler")
15try:
16    compiled_sched = compiler.compile(
17        schedule=sched, config=quantum_device.generate_compilation_config()
18    )
19except RuntimeError as err:
20    print(err)
Parameter operation Pulse "UpdateParameters" (t0=1.0000000000000001e-07, duration=0) with start time 1.0000000000000001e-07 cannot be scheduled at the very end of a Schedule. The Schedule can be extended by adding an IdlePulse operation with a duration of at least 4 ns, or the Parameter operation can be replaced by another operation.

Instead, some time should be left at the end like so:

 1sched = Schedule("offset_sched")
 2sched.add_resource(ClockResource(name="q3.01", freq=9e9))
 3
 4sched.add(
 5    VoltageOffset(offset_path_I=0.5, offset_path_Q=0.0, port="q3:mw", clock="q3.01")
 6)
 7sched.add(SquarePulse(amp=1, duration=1e-7, port="q3:mw", clock="q3.01"))
 8sched.add(
 9    VoltageOffset(offset_path_I=0.0, offset_path_Q=0.0, port="q3:mw", clock="q3.01")
10)
11sched.add(IdlePulse(4e-9))
12
13compiler = SerialCompiler(name="compiler")
14compiled_sched = compiler.compile(
15    schedule=sched, config=quantum_device.generate_compilation_config()
16)

Long waveforms via StitchedPulse#

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.

Factory functions#

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.backends.qblox.operations 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,
)

quantum_device = QuantumDevice("quantum_device")
device_compiler = SerialCompiler("Device compiler", quantum_device)

comp_sched = device_compiler.compile(sched)
comp_sched.plot_pulse_diagram(
    plot_backend="plotly", combine_waveforms_on_same_port=True
)

Tip

Add the argument combine_waveforms_on_same_port=True to plot_pulse_diagram to show the appearance of the final hardware output (default combine_waveforms_on_same_port=False shows individual pulse elements).

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.

Builder class#

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.backends.qblox.operations 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_I=1.0, path_Q=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)

comp_sched = device_compiler.compile(sched)
comp_sched.plot_pulse_diagram(
    plot_backend="plotly", combine_waveforms_on_same_port=True
)

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_I=1.0, path_Q=0.0, duration=constant_duration)
    .add_pulse(
        NumericalPulse(samples=hann_up, t_samples=ramp_t, port=port, clock=clock)
    )
    .build()
)

Voltage offsets can be specified with or without a duration. If a duration is specified, the builder class will automatically insert 0 Volt offsets after the specified duration. If no duration is specified, a 0 Volt offset operation will be inserted at the very end of the StitchedPulse. Please take this into account when specifying a StitchedPulse as the last operation of a control-flow block or Schedule (see the Voltage offsets section).

Adding a voltage offset with no duration, followed by a pulse, will play that pulse with the specified offset. This can allow you to re-use waveforms. An example is shown below:

 1from quantify_scheduler.operations.pulse_library import RampPulse
 2from quantify_scheduler.backends.qblox.operations import StitchedPulseBuilder
 3
 4
 5repeat_pulse_with_offset = (
 6    StitchedPulseBuilder(port="q0:mw", clock="q0.01")
 7    .add_pulse(RampPulse(amp=0.2, duration=8e-6, port="q0:mw"))
 8    .add_voltage_offset(path_I=0.4, path_Q=0.0)
 9    .add_pulse(RampPulse(amp=0.2, duration=8e-6, port="q0:mw"))
10    .build()
11)

Pulses and offsets are appended to the end of the last added operation by default. By specifying the append=False keyword argument in the add_pulse and add_voltage_offset methods, in combination with the rel_time argument, you can insert an operation at the specified time relative to the start of the StitchedPulse. The example below uses this to generate a series of square pulses of various durations and amplitudes:

 1from quantify_scheduler.backends.qblox.operations import StitchedPulseBuilder
 2
 3offsets = [0.3, 0.4, 0.5]
 4durations = [1e-6, 2e-6, 1e-6]
 5start_times = [0.0, 2e-6, 6e-6]
 6
 7builder = StitchedPulseBuilder(port="q0:mw", clock="q0.01")
 8
 9for offset, duration, t_start in zip(offsets, durations, start_times):
10    builder.add_voltage_offset(
11        path_I=offset, path_Q=0.0, duration=duration, append=False, rel_time=t_start
12    )
13
14pulse = builder.build()