Pulse Library and Sampled Pulses¶
This notebook demonstrates the features of the pulse library and will cover:
- How to use pre-defined pulses out of the box, and how to sweep their parameters here
- How to define your own, parameterized pulses and sweep their parameters here
- How to define sampled pulses, e.g., from a
numpyarray here
A demonstration of this notebook is also available on our Youtube channel here
Setting up: Imports, Device Setup, and Calibration¶
Imports¶
# LabOne Q:
import matplotlib.pyplot as plt
import numpy as np
# Helpers:
from laboneq.contrib.example_helpers.generate_device_setup import (
generate_device_setup_qubits,
)
from laboneq.contrib.example_helpers.plotting.plot_helpers import plot_simulation
from laboneq.simple import *
Define the Device Setup¶
We'll load a descriptor file to define our device setup and logical signal lines. We could, instead, explicitly include the descriptor here as a string and then use DeviceSetup.from_descriptor() below. Choose the best method that works for you!
# specify the number of qubits you want to use
number_of_qubits = 2
# generate the device setup and the qubit objects using a helper function
device_setup, qubits = generate_device_setup_qubits(
number_qubits=number_of_qubits,
shfqc=[
{
"serial": "DEV12001",
"number_of_channels": 6,
"readout_multiplex": 6,
"options": None,
}
],
include_flux_lines=False,
server_host="localhost",
setup_name=f"my_{number_of_qubits}_fixed_qubit_setup",
)
Create the Session and Connect to it¶
use_emulation = True # set to False to run on real hardware
session = Session(device_setup=device_setup)
session.connect(do_emulation=use_emulation)
Sweep Parameters Of an Out-of-the-Box Pulse¶
In a first step, we will create a drag pulse and sweep its amplitude. In a second step, we sweep the drag parameter beta instead.
Define Pulses¶
# qubit drive pulse
x90 = pulse_library.drag(uid="drag_pulse", length=400e-9, amplitude=1.0, beta=0.3)
# measure pulse
readout_pulse = pulse_library.const(uid="readout_pulse", length=200e-9, amplitude=1.0)
# readout integration weights
readout_weighting_function = pulse_library.const(
uid="readout_weighting_function", length=200e-9, amplitude=1.0
)
start = 0.1
stop = 1
count = 5
amplitude_sweep = LinearSweepParameter(
uid="amplitude", start=start, stop=stop, count=count
)
Define Experiment¶
Note that the amplitude parameter is an argument of the Experiment.play command.
# Create Experiment
exp = Experiment(
uid="Amplitude Rabi",
signals=[
ExperimentSignal("drive"),
ExperimentSignal("measure"),
ExperimentSignal("acquire"),
],
)
## experimental pulse sequence
# outer loop - real-time, cyclic averaging in standard integration mode
with exp.acquire_loop_rt(
uid="shots",
count=2**5,
averaging_mode=AveragingMode.CYCLIC,
acquisition_type=AcquisitionType.INTEGRATION,
):
# inner loop - real-time sweep of qubit drive pulse amplitude
with exp.sweep(
uid="sweep", parameter=amplitude_sweep, alignment=SectionAlignment.RIGHT
):
# qubit excitation - pulse amplitude will be swept
with exp.section(uid="qubit_excitation", alignment=SectionAlignment.RIGHT):
exp.play(signal="drive", pulse=x90, amplitude=amplitude_sweep)
# qubit readout pulse and data acquisition
with exp.section(uid="qubit_readout"):
exp.reserve(signal="drive")
# play readout pulse
exp.play(signal="measure", pulse=readout_pulse)
# signal data acquisition
exp.acquire(
signal="acquire",
handle="ac_0",
kernel=readout_weighting_function,
)
# relax time after readout - for signal processing and qubit relaxation to ground state
with exp.section(uid="relax", length=1e-6):
exp.reserve(signal="measure")
Define Signal Map and Run Experiment¶
# define signal maps for qubit 0
q0 = device_setup.qubits["q0"]
map_q0 = {
"drive": q0.signals["drive"],
"measure": q0.signals["measure"],
"acquire": q0.signals["acquire"],
}
q0_calibration = q0.calibration()
calibration = Calibration(
{
"drive": q0_calibration["q0/drive"],
"measure": q0_calibration["q0/measure"],
"acquire": q0_calibration["q0/acquire"],
}
)
exp.set_signal_map(map_q0)
exp.set_calibration(calibration)
# run experiment on qubit 0
my_results = session.run(exp)
# Plot simulated output signals
plot_simulation(session.compiled_experiment, start_time=0, length=10e-6)
start_beta = 0.0
stop_beta = 1
count = 5
beta_sweep = LinearSweepParameter(uid="beta", start=start, stop=stop, count=count)
Define Experiment¶
Note that the parameter beta is unique to the drag pulse (Only the parameters length and amplitude are shared by all pulses).
Therefore, the parameters handled by the Experiment.play command slightly differ from amplitude sweep above.
# Create Experiment
exp = Experiment(
uid="Amplitude Rabi",
signals=[
ExperimentSignal("drive"),
ExperimentSignal("measure"),
ExperimentSignal("acquire"),
],
)
## experimental pulse sequence
# outer loop - real-time, cyclic averaging in standard integration mode
with exp.acquire_loop_rt(
uid="shots",
count=2**5,
averaging_mode=AveragingMode.CYCLIC,
acquisition_type=AcquisitionType.INTEGRATION,
):
# inner loop - real-time sweep of qubit drive pulse drag parameter
with exp.sweep(uid="sweep", parameter=beta_sweep, alignment=SectionAlignment.RIGHT):
# qubit excitation - pulse amplitude will be swept
with exp.section(uid="qubit_excitation", alignment=SectionAlignment.RIGHT):
exp.play(signal="drive", pulse=x90, pulse_parameters={"beta": beta_sweep})
# qubit readout pulse and data acquisition
with exp.section(uid="qubit_readout"):
exp.reserve(signal="drive")
# play readout pulse
exp.play(signal="measure", pulse=readout_pulse)
# signal data acquisition
exp.acquire(
signal="acquire",
handle="ac_0",
kernel=readout_weighting_function,
)
# relax time after readout - for signal processing and qubit relaxation to ground state
with exp.section(uid="relax", length=1e-6):
exp.reserve(signal="measure")
Define Signal Map and Run the Experiment¶
# define signal maps for qubit 0
map_q0 = {
"drive": device_setup.logical_signal_groups["q0"].logical_signals["drive"],
"measure": device_setup.logical_signal_groups["q0"].logical_signals["measure"],
"acquire": device_setup.logical_signal_groups["q0"].logical_signals["acquire"],
}
q0_calibration = q0.calibration()
calibration = Calibration(
{
"drive": q0_calibration["q0/drive"],
"measure": q0_calibration["q0/measure"],
"acquire": q0_calibration["q0/acquire"],
}
)
exp.set_signal_map(map_q0)
exp.set_calibration(calibration)
# run experiment on qubit 0
my_results = session.run(exp)
# Plot simulated output signals
plot_simulation(session.compiled_experiment, start_time=0, length=10e-6)
Define a new Pulse Type and Sweep it¶
You can define arbitrary pulse types. All you need to do is to decorate your functional with the pulse_library.register_pulse_functional decorator delivered with LabOne Q. Please refer to the API Documentation for more technical details.
Here, we create an example with a flat-top Gaussian pulse.
When you define your own pulses, just a few constraints exist:
- The first argument of your function is
x, and ranges from -1 to 1. - The last keyword argument
**_receives the pulse lengthlengthin absolute time, sampling ratesampling_rate, and relative amplitudeamplitudeused when the pulse is actually played. The definition offlattop_gaussian_v2further down shows how to use them. - In between, you can define an arbitrary number of your own pulses.
@pulse_library.register_pulse_functional
def flattop_gaussian(x, relative_length_flat=0.8, **_):
sigma = (1 - relative_length_flat) / 3
res = np.ones(len(x))
res[x <= -relative_length_flat] = np.exp(
-((x[x <= -relative_length_flat] + relative_length_flat) ** 2) / (2 * sigma**2)
)
res[x >= relative_length_flat] = np.exp(
-((x[x >= relative_length_flat] - relative_length_flat) ** 2) / (2 * sigma**2)
)
return res
Visualising pulse functionals¶
The PulseFunctional that is created and registered in the pulse library can be directly evaluated and visualised with the following methods:
evaluate()- evaluates the function as it was defined, i.e. with the x-axis extending from -1 to 1,lengthandamplitudeparameters have no effect, additional pulse parameters can be suppliedgenerate_sampled_pulse()- evaluates the pulse functional as it would be used in the LabOne Q DSL -lengthandamplitudeare required parameters, additional pulse parameters can be supplied. Returns both the sampled pulse functional and the corresponding sampled time
x0 = np.linspace(-1, 1, 501)
plt.plot(
x0,
flattop_gaussian(relative_length_flat=0.8).evaluate(x=x0),
)
plt.plot(
x0,
flattop_gaussian(relative_length_flat=0.6).evaluate(x=x0),
)
plt.plot(
x0,
flattop_gaussian(relative_length_flat=0.4).evaluate(x=x0),
)
plt.show()
time, wfm = flattop_gaussian(
length=100e-9, amplitude=0.6, relative_length_flat=0.8
).generate_sampled_pulse()
plt.plot(time * 1e9, wfm.real)
time, wfm = flattop_gaussian(
length=100e-9, amplitude=0.4, relative_length_flat=0.6
).generate_sampled_pulse()
plt.plot(time * 1e9, wfm.real)
time, wfm = flattop_gaussian(
length=100e-9, amplitude=0.2, relative_length_flat=0.4
).generate_sampled_pulse()
plt.plot(time * 1e9, wfm.real)
plt.xlabel("Time, $t$ (ns)")
plt.ylabel("Amplitude, $A$ (a.u.)")
plt.show()
Instead of defining everything relative to the span from -1 to 1, the length argument can be used to obtain an absolute reference in the pulse definition.
For a flat-top Gaussian, this could be used to define constant, absolute rise and fall times of the pulse:
@pulse_library.register_pulse_functional
def flattop_gaussian_v2(x, slope_length=15e-9, length=..., **_):
# assume that the rising and falling Gaussian parts should have 3*sigma width
sigma = slope_length / 3
sigma_rel = sigma / length
relative_length_flat = 1 - 6 * sigma_rel # subtract 3*sigma from the left,
res = np.ones(len(x))
res[x <= -relative_length_flat] = np.exp(
-((x[x <= -relative_length_flat] + relative_length_flat) ** 2)
/ (2 * sigma_rel**2)
)
res[x >= relative_length_flat] = np.exp(
-((x[x >= relative_length_flat] - relative_length_flat) ** 2)
/ (2 * sigma_rel**2)
)
return res
flattop = flattop_gaussian(
uid="flattop", length=400e-9, amplitude=1, relative_length_flat=0.9
)
sweep_rel_flat = LinearSweepParameter(
uid="sweep_rel_flat", start=0.05, stop=0.95, count=5
)
# Create Experiment
exp = Experiment(
uid="Amplitude Rabi",
signals=[
ExperimentSignal("drive"),
ExperimentSignal("measure"),
ExperimentSignal("acquire"),
],
)
## experimental pulse sequence
# outer loop - real-time, cyclic averaging in standard integration mode
with exp.acquire_loop_rt(
uid="shots",
count=8,
averaging_mode=AveragingMode.CYCLIC,
acquisition_type=AcquisitionType.INTEGRATION,
):
# inner loop - real-time sweep of the relative length of the flat part of the drive pulse
with exp.sweep(
uid="sweep", parameter=sweep_rel_flat, alignment=SectionAlignment.RIGHT
):
# qubit excitation - pulse amplitude will be swept
with exp.section(uid="qubit_excitation", alignment=SectionAlignment.RIGHT):
exp.play(
signal="drive",
pulse=flattop,
pulse_parameters={"relative_length_flat": sweep_rel_flat},
)
# qubit readout pulse and data acquisition
with exp.section(uid="qubit_readout"):
exp.reserve(signal="drive")
# play readout pulse
exp.play(
signal="measure", pulse=readout_pulse
) # , pulse_parameters={"relative_length_flat": sweep_rel_flat})
# signal data acquisition
exp.acquire(
signal="acquire",
handle="ac_0",
kernel=readout_weighting_function,
)
# relax time after readout - for signal processing and qubit relaxation to ground state
with exp.section(uid="relax", length=1e-6):
exp.reserve(signal="measure")
# define signal maps for qubit 0
map_q0 = {
"drive": device_setup.logical_signal_groups["q0"].logical_signals["drive"],
"measure": device_setup.logical_signal_groups["q0"].logical_signals["measure"],
"acquire": device_setup.logical_signal_groups["q0"].logical_signals["acquire"],
}
q0_calibration = q0.calibration()
calibration = Calibration(
{
"drive": q0_calibration["q0/drive"],
"measure": q0_calibration["q0/measure"],
"acquire": q0_calibration["q0/acquire"],
}
)
exp.set_signal_map(map_q0)
exp.set_calibration(calibration)
# run experiment on qubit 0
my_results = session.run(exp)
# Plot simulated output signals
plot_simulation(session.compiled_experiment, start_time=0, length=10e-6)
Same Experiment with Absolute Timings of sigma¶
flattop_v2 = flattop_gaussian_v2(
uid="flattop_v2", length=400e-9, amplitude=1, slope_length=15e-9
)
slope_sweep = LinearSweepParameter(uid="slope_sweep", start=5e-9, stop=180e-9, count=5)
# Create Experiment
exp = Experiment(
uid="Amplitude Rabi",
signals=[
ExperimentSignal("drive"),
ExperimentSignal("measure"),
ExperimentSignal("acquire"),
],
)
## experimental pulse sequence
# outer loop - real-time, cyclic averaging in standard integration mode
with exp.acquire_loop_rt(
uid="shots",
count=8,
averaging_mode=AveragingMode.CYCLIC,
acquisition_type=AcquisitionType.INTEGRATION,
):
# inner loop - real-time sweep of the slope sweep
with exp.sweep(
uid="sweep", parameter=slope_sweep, alignment=SectionAlignment.RIGHT
):
# qubit excitation - pulse amplitude will be swept
with exp.section(uid="qubit_excitation", alignment=SectionAlignment.RIGHT):
exp.play(
signal="drive",
pulse=flattop_v2,
pulse_parameters={"slope_length": slope_sweep},
)
# qubit readout pulse and data acquisition
with exp.section(uid="qubit_readout"):
exp.reserve(signal="drive")
# play readout pulse
exp.play(signal="measure", pulse=readout_pulse)
# signal data acquisition
exp.acquire(
signal="acquire",
handle="ac_0",
kernel=readout_weighting_function,
)
# relax time after readout - for signal processing and qubit relaxation to ground state
with exp.section(uid="relax", length=1e-6):
exp.reserve(signal="measure")
# define signal maps for qubit 0
map_q0 = {
"drive": device_setup.logical_signal_groups["q0"].logical_signals["drive"],
"measure": device_setup.logical_signal_groups["q0"].logical_signals["measure"],
"acquire": device_setup.logical_signal_groups["q0"].logical_signals["acquire"],
}
q0_calibration = q0.calibration()
calibration = Calibration(
{
"drive": q0_calibration["q0/drive"],
"measure": q0_calibration["q0/measure"],
"acquire": q0_calibration["q0/acquire"],
}
)
exp.set_signal_map(map_q0)
exp.set_calibration(calibration)
# run experiment on qubit 0
my_results = session.run(exp)
# Plot simulated output signals
plot_simulation(session.compiled_experiment, start_time=0, length=10e-6)
Create a Sampled Pulse from an Array of Sampling Points¶
In case you want to play a list of sampling points, you can create a pulse containing these points. The samples can either be real or complex numbers.
When you use sampled pulses, please keep two points in mind:
- If you want to use sampled pulses, you need to know the sampling rate of your system. Please refer to our documentation to learn more.
- By definition, sampled pulses are not parameterized. However, their length and phase can be swept as demonstrated in the example below.
Define a Sampled Pulse¶
n_samples_real = 1024
n_samples_complex = 128
sample_list_real = np.random.randn(n_samples_real)
sample_list_complex = np.random.randn(n_samples_complex) + 1j * np.random.randn(
n_samples_complex
)
sampled_pulse_real = pulse_library.PulseSampled(
samples=sample_list_real, uid="real_pulse"
)
sampled_pulse_complex = pulse_library.PulseSampled(
samples=sample_list_complex, uid="complex_pulse"
)
Create Experiment and Amplitude Sweep¶
# create two parameter sweeps
amplitude_sweep = LinearSweepParameter(
uid="amplitude_sweep", start=0.1, stop=1, count=5
)
phase_sweep = LinearSweepParameter(uid="phase_sweep", start=0, stop=np.pi / 2, count=5)
# Create Experiment
exp = Experiment(
uid="Amplitude Rabi",
signals=[
ExperimentSignal("drive"),
ExperimentSignal("measure"),
ExperimentSignal("acquire"),
],
)
## experimental pulse sequence
# outer loop - real-time, cyclic averaging in standard integration mode
with exp.acquire_loop_rt(
uid="shots",
count=8,
averaging_mode=AveragingMode.CYCLIC,
acquisition_type=AcquisitionType.INTEGRATION,
):
# inner loop - real-time sweep of drive amplitudes
with exp.sweep(
uid="sweep",
parameter=[phase_sweep, amplitude_sweep],
alignment=SectionAlignment.RIGHT,
):
# qubit excitation - pulse amplitude and phase will be swept
with exp.section(uid="qubit_excitation", alignment=SectionAlignment.RIGHT):
exp.play(signal="drive", pulse=sampled_pulse_real, phase=phase_sweep)
exp.delay(signal="drive", time=100e-9)
exp.play(
signal="drive", pulse=sampled_pulse_complex, amplitude=amplitude_sweep
)
# qubit readout pulse and data acquisition
with exp.section(uid="qubit_readout"):
exp.reserve(signal="drive")
# play readout pulse
exp.play(signal="measure", pulse=readout_pulse)
# signal data acquisition
exp.acquire(
signal="acquire",
handle="ac_0",
kernel=readout_weighting_function,
)
# relax time after readout - for signal processing and qubit relaxation to ground state
with exp.section(uid="relax", length=1e-6):
exp.reserve(signal="measure")
# define signal maps for qubit 0
map_q0 = {
"drive": device_setup.logical_signal_groups["q0"].logical_signals["drive"],
"measure": device_setup.logical_signal_groups["q0"].logical_signals["measure"],
"acquire": device_setup.logical_signal_groups["q0"].logical_signals["acquire"],
}
q0_calibration = q0.calibration()
calibration = Calibration(
{
"drive": q0_calibration["q0/drive"],
"measure": q0_calibration["q0/measure"],
"acquire": q0_calibration["q0/acquire"],
}
)
exp.set_signal_map(map_q0)
exp.set_calibration(calibration)
# run experiment on qubit 0
my_results = session.run(exp)
# Plot simulated output signals
plot_simulation(session.compiled_experiment, start_time=0, length=10e-6)