Section Tutorial¶
In this notebook you'll build up experiments with the LabOne Q concept of Sections, following along with the Section chapter in the Manual. In the first example, you'll go step by step through each part of defining and running your experiment. In the subsequent examples, you'll focus on the differences between the sections themselves.
By the end of this notebook, you will have constructed a Ramsey sequence, and you'll see how you can control the timing of your experiment by manipulating sections, their properties, and their contents.
At the end of each step, a pulse sheet will be generated to visualize the behavior. Feel free to modify the experimental sequences and observe the changes in the pulse sheet.
Imports¶
# Import required packages
from laboneq.simple import *
import zhinst.core as zi
import numpy as np
import matplotlib.pyplot as plt
from laboneq.contrib.example_helpers.descriptors.shfqc import descriptor_shfqc
from laboneq.contrib.example_helpers.descriptors.hdawg_uhfqa_pqsc import (
descriptor_hdawg_uhfqa_pqsc,
)
from laboneq.contrib.example_helpers.descriptors.shfsg_shfqa_pqsc import (
descriptor_shfsg_shfqa_pqsc,
)
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!
# Define and Load our Device Setup
device_setup = DeviceSetup.from_descriptor(
descriptor_shfsg_shfqa_pqsc,
server_host="my_ip_address", # ip address of the LabOne dataserver used to communicate with the instruments
server_port="8004", # port number of the dataserver - default is 8004
setup_name="my_QCCS_setup", # setup name
)
We can look at which logical lines and physical lines are available and if they are calibrated after defining our device setup. They are currently uncalibrated until we provide a calibration later.
device_setup.list_calibratables()
Drive Line Calibration¶
We'll start with a basic calibration - providing the intermediate (IF) and local oscillator (LO) frequencies used on our drive line, and specifying the output range of the instrument:
# Basic calibration of IF and LO frequencies
drive_q0_if = Oscillator(
uid="drive" + "q0" + "if", # each oscillator object has a unique id
frequency=1.0e8,
modulation_type=ModulationType.HARDWARE,
)
drive_q0_q1_lo = Oscillator(
uid="drive" + "q0" + "lo",
frequency=5.0e9,
)
def calibrate_devices(device_setup):
## qubit 0
# calibration setting for drive line for qubit 0
device_setup.logical_signal_groups["q0"].logical_signals[
"drive_line"
].calibration = SignalCalibration(
# oscillator settings - frequency and type of oscillator used to modulate the pulses applied through this signal line
oscillator=drive_q0_if,
local_oscillator=drive_q0_q1_lo,
range=10,
)
We can set the calibration:
calibrate_devices(device_setup)
And list our calibrated lines. We'll calibrate the unused ones later on.
device_setup.list_calibratables()
First Experiment - Left Aligned Section¶
Now we define our first experiment with a pulse:
# A pulse to be used in the Experiment
x90 = pulse_library.gaussian(uid="x90", length=100e-9, amplitude=0.66)
# Define the Experiment
exp = Experiment(
uid="SectionIntro",
signals=[
ExperimentSignal("drive"),
],
)
## The pulse sequence:
# Real time loop
with exp.acquire_loop_rt(uid="RT_shots", count=1):
# Left-aligned section of fixed length
with exp.section(uid="excitation", length=2e-6, alignment=SectionAlignment.LEFT):
# Section contents
exp.play(signal="drive", pulse=x90)
Signal Map¶
We define and map our experiment signal line to the appropriate logical signal line:
# define signal map
map_q0_drive = {
"drive": device_setup.logical_signal_groups["q0"].logical_signals["drive_line"]
}
# set signal map
exp.set_signal_map(map_q0_drive)
We can check that our lines are mapped how we expect them to be in the following way:
exp.get_signal_map()
Session and Compilation¶
Now we'll start a Session. If you're running without hardware, no problem! Just connect to the session using do_emulation=True
. Once you're ready to go on your instruments, change True
to False
.
After connecting to the Session, we can compile or compile and run the experiment. Here, we break it down into separate steps:
session = Session(device_setup=device_setup)
session.connect(do_emulation=True)
compiled_exp = session.compile(exp)
Pulse Sheet Viewer¶
Once the experiment is compiled, we can view the pulses in a Pulse Sheet Viewer, a HTML file which shows the sections, pulses, and their relative timings.
show_pulse_sheet("1_Section_Intro", compiled_exp)
Source Code¶
We can also view the source code that gets uploaded and then compiled on the instrument.
print(compiled_exp.src[0]["text"])
Running the Experiment¶
Finally, we can run the experiment.
my_results = session.run(compiled_exp)
We'll now move into other experiments, condensing the signal mapping, compilation, and running of the experiments, and we'll focus on how sections modify our experimental pulse sequence.
Alignment¶
Right Aligned Section¶
Section alignment is a extremely useful and powerful way to control pulse timing. Here, we show how changing the alignment of the section changes when the pulse is played.
# Define the Experiment
exp = Experiment(
uid="Right_Alignment",
signals=[
ExperimentSignal("drive"),
],
)
## The pulse sequence:
# Real time loop
with exp.acquire_loop_rt(uid="RT_shots", count=1):
# Left-aligned section of fixed length
with exp.section(uid="excitation", length=2e-6, alignment=SectionAlignment.RIGHT):
# Section contents
exp.play(signal="drive", pulse=x90)
# set experiment calibration and signal map
exp.set_signal_map(map_q0_drive)
# compile
compiled_exp = session.compile(exp)
# run the experiment
my_results = session.run()
# generate the pulse sheet
show_pulse_sheet("2_Right_Alignment", session.compiled_experiment)
No Specified Section Length¶
If no section length is specified, the section length will be determined by the section's contents. In the below case with a single pulse, left or right alignment will result in the same timing of the pulse.
# Define the Experiment
exp = Experiment(
uid="No_Specified_Section_Length",
signals=[
ExperimentSignal("drive"),
],
)
## The pulse sequence:
# Real time loop
with exp.acquire_loop_rt(uid="RT_shots", count=1):
# Left Aligned section of fixed length
with exp.section(uid="excitation", alignment=SectionAlignment.RIGHT):
# Section contents
exp.play(signal="drive", pulse=x90)
# set experiment calibration and signal map
exp.set_signal_map(map_q0_drive)
# compile
compiled_exp = session.compile(exp)
# run the experiment
my_results = session.run()
# show pulse sheet
show_pulse_sheet("3_No_Specified_Length", session.compiled_experiment)
Signal Delays¶
Here, we add a second pulse to the same section played 100 ns after the first using the delay
command.
# Define the Experiment
exp = Experiment(
uid="Signal_Delay",
signals=[
ExperimentSignal("drive"),
],
)
## The pulse sequence:
# Real time loop
with exp.acquire_loop_rt(uid="RT_shots", count=1):
# Right Aligned section of fixed length
with exp.section(uid="excitation", alignment=SectionAlignment.RIGHT, length=1e-6):
# Section contents
exp.play(signal="drive", pulse=x90)
exp.delay(signal="drive", time=100e-9)
exp.play(signal="drive", pulse=x90)
# set experiment calibration and signal map
exp.set_signal_map(map_q0_drive)
# compile
compiled_exp = session.compile(exp)
# run the experiment
my_results = session.run()
# show pulse sheet
show_pulse_sheet("4_Signal_Delay", session.compiled_experiment)
Second Drive Line¶
We now introduce a second drive line. Note that we must create a new signal map, as we have introduced a second experiment signal.
# Define a second pulse in addition to x90
x180 = pulse_library.gaussian(uid="x180", length=200e-9, amplitude=0.66)
# Define the Experiment
exp = Experiment(
uid="Section_Two_Lines",
signals=[ExperimentSignal("drive"), ExperimentSignal("drive1")],
)
## The pulse sequence:
# Real time loop
with exp.acquire_loop_rt(uid="RT_shots", count=1):
# Right-aligned section with 1 microsecond length
with exp.section(uid="excitation", alignment=SectionAlignment.RIGHT, length=1e-6):
# Section contents
exp.play(signal="drive", pulse=x90)
exp.delay(signal="drive", time=100e-9)
exp.play(signal="drive", pulse=x90)
exp.play(signal="drive1", pulse=x180)
exp.delay(signal="drive1", time=50e-9)
exp.play(signal="drive1", pulse=x90)
# define signal map
map_q0_q1_drive = {
"drive": device_setup.logical_signal_groups["q0"].logical_signals["drive_line"],
"drive1": device_setup.logical_signal_groups["q1"].logical_signals["drive_line"],
}
# Basic calibration of q1 IF and LO frequencies
drive_q1_if = Oscillator(
uid="drive" + "q1" + "if", frequency=0.5e8, modulation_type=ModulationType.HARDWARE
)
def calibrate_devices_drive1(device_setup):
## qubit 1
# calibration setting for drive line for qubit 1
device_setup.logical_signal_groups["q1"].logical_signals[
"drive_line"
].calibration = SignalCalibration(
# oscillator settings - frequency and type of oscillator used to modulate the pulses applied through this signal line
oscillator=drive_q1_if,
local_oscillator=drive_q0_q1_lo,
range=10,
)
# apply the new calibration
calibrate_devices_drive1(device_setup)
device_setup.list_calibratables()
# set experiment calibration and signal map
exp.set_signal_map(map_q0_q1_drive)
# compile
compiled_exp = session.compile(exp)
# run the experiment
my_results = session.run()
# show pulse sheet
show_pulse_sheet("5_Section_Two_Lines", session.compiled_experiment)
Multiple and Nested Sections¶
An experiment can have multiple sections. If the sections do not comprise the same signal lines (as in the example below), they will be played in parallel.
# Define the Experiment
exp = Experiment(
uid="Two_Sections",
signals=[ExperimentSignal("drive"), ExperimentSignal("drive1")],
)
## The pulse sequence:
# Real time loop
with exp.acquire_loop_rt(uid="RT_shots", count=1):
# Right-aligned section with 1 microsecond length
with exp.section(uid="excitation", alignment=SectionAlignment.RIGHT, length=1e-6):
# Section contents
exp.play(signal="drive", pulse=x90)
exp.delay(signal="drive", time=100e-9)
exp.play(signal="drive", pulse=x90)
# Left-aligned section with 500 ns length
with exp.section(uid="excitation1", alignment=SectionAlignment.LEFT, length=500e-9):
# Section contents
exp.play(signal="drive1", pulse=x180)
exp.delay(signal="drive1", time=50e-9)
exp.play(signal="drive1", pulse=x90)
# set experiment calibration and signal map
exp.set_signal_map(map_q0_q1_drive)
# compile
compiled_exp = session.compile(exp)
# run the experiment
my_results = session.run()
# show pulse sheet
show_pulse_sheet("6_Two_Sections", session.compiled_experiment)
Sections can be nested within parent sections. This is a powerful way to define the timing behavior of more complex experiments.
# Define the Experiment
exp = Experiment(
uid="Nested_Sections",
signals=[ExperimentSignal("drive"), ExperimentSignal("drive1")],
)
## The pulse sequence:
# Real time loop
with exp.acquire_loop_rt(uid="RT_shots", count=1):
# Parent section with right alignment
with exp.section(uid="parent", alignment=SectionAlignment.RIGHT):
# Right-aligned section with 1 microsecond length
with exp.section(
uid="excitation", alignment=SectionAlignment.RIGHT, length=1e-6
):
# Section contents
exp.play(signal="drive", pulse=x90)
exp.delay(signal="drive", time=100e-9)
exp.play(signal="drive", pulse=x90)
# Left-aligned section with 50 ns length
with exp.section(
uid="excitation1", alignment=SectionAlignment.LEFT, length=500e-9
):
# Section contents
exp.play(signal="drive1", pulse=x180)
exp.delay(signal="drive1", time=50e-9)
exp.play(signal="drive1", pulse=x90)
# set experiment calibration and signal map
exp.set_signal_map(map_q0_q1_drive)
# compile
compiled_exp = session.compile(exp)
# run the experiment
my_results = session.run()
# show pulse sheet
show_pulse_sheet("7_Nested_Sections", session.compiled_experiment)
Reusing Sections¶
Sections can be reused by referring to their uid
. A use case is to define a quantum gate within a section and apply it several times.
# Define the Experiment
exp = Experiment(
uid="Reusing_Sections",
signals=[ExperimentSignal("drive"), ExperimentSignal("drive1")],
)
## The pulse sequence:
# Real time loop
with exp.acquire_loop_rt(uid="RT_shots", count=1):
# Parent section with right alignment
with exp.section(uid="parent", alignment=SectionAlignment.RIGHT):
# Right-aligned section with 1 microsecond length
with exp.section(
uid="excitation", alignment=SectionAlignment.RIGHT, length=1e-6
):
# Section contents
exp.play(signal="drive", pulse=x90)
exp.delay(signal="drive", time=100e-9)
exp.play(signal="drive", pulse=x90)
# Left-aligned section with 50 ns length
with exp.section(
uid="excitation1", alignment=SectionAlignment.LEFT, length=500e-9
) as excitation1:
# Section contents
exp.play(signal="drive1", pulse=x180)
exp.delay(signal="drive1", time=50e-9)
exp.play(signal="drive1", pulse=x90)
exp.add(section=excitation1)
exp.add(section=excitation1)
# set experiment calibration and signal map
exp.set_signal_map(map_q0_q1_drive)
# compile
compiled_exp = session.compile(exp)
# run the experiment
my_results = session.run()
# show pulse sheet
show_pulse_sheet("8_Reusing_Sections", session.compiled_experiment)
Instead of using sections defined within the experiment, they can be defined explicitly before the experimental sequence starts:
my_section = Section(uid="my_section", alignment=SectionAlignment.LEFT, length=500e-9)
my_section.play(signal="drive1", pulse=x180)
my_section.delay(signal="drive1", time=50e-9)
my_section.play(signal="drive1", pulse=x90)
# Define the Experiment
exp = Experiment(
uid="Reusing_Sections_Alternative",
signals=[ExperimentSignal("drive"), ExperimentSignal("drive1")],
)
## The pulse sequence:
# Real time loop
with exp.acquire_loop_rt(uid="RT_shots", count=1):
# Parent section with right alignment
with exp.section(uid="parent", alignment=SectionAlignment.RIGHT):
# Right-aligned section with 1 microsecond length
with exp.section(
uid="excitation", alignment=SectionAlignment.RIGHT, length=1e-6
):
# Section contents
exp.play(signal="drive", pulse=x90)
exp.delay(signal="drive", time=100e-9)
exp.play(signal="drive", pulse=x90)
# Left-aligned section with 50 ns length
exp.add(section=my_section)
exp.add(section=my_section)
exp.add(section=my_section)
# set experiment calibration and signal map
exp.set_signal_map(map_q0_q1_drive)
# compile
compiled_exp = session.compile(exp)
# run the experiment
my_results = session.run()
# show pulse sheet
show_pulse_sheet("9_Reusing_Sections_Alternative", session.compiled_experiment)
The play_after
Command¶
The play_after
command enforces a temporal ordering between sections. We could use it to make sure that a qubit is measured only after the drive section has finished.
# Define the Experiment
exp = Experiment(
uid="Play_after",
signals=[ExperimentSignal("drive"), ExperimentSignal("drive1")],
)
## The pulse sequence:
# Real time loop
with exp.acquire_loop_rt(uid="RT_shots", count=1):
# Parent section with right alignment
with exp.section(uid="parent", alignment=SectionAlignment.RIGHT):
# Right-aligned section with 1 microsecond length
with exp.section(
uid="excitation", alignment=SectionAlignment.RIGHT, length=1e-6
):
# Section contents
exp.play(signal="drive", pulse=x90)
exp.delay(signal="drive", time=100e-9)
exp.play(signal="drive", pulse=x90)
# Left-aligned section with 50 ns length
with exp.section(
uid="excitation1",
alignment=SectionAlignment.LEFT,
length=500e-9,
play_after="excitation",
) as excitation1:
# Section contents
exp.play(signal="drive1", pulse=x180)
exp.delay(signal="drive1", time=50e-9)
exp.play(signal="drive1", pulse=x90)
exp.add(section=excitation1)
exp.add(section=excitation1)
# set experiment calibration and signal map
exp.set_signal_map(map_q0_q1_drive)
# compile
compiled_exp = session.compile(exp)
# run the experiment
my_results = session.run()
# show pulse sheet
show_pulse_sheet("10_Play_After", session.compiled_experiment)
Reserving Signals¶
We can reserve a signal line within a section to make sure that it cannot be used anywhere else in the experiment at the same time:
# Define the Experiment
exp = Experiment(
uid="Reserving_Signals",
signals=[ExperimentSignal("drive"), ExperimentSignal("drive1")],
)
## The pulse sequence:
# Real time loop
with exp.acquire_loop_rt(uid="RT_shots", count=1):
# Parent section with right alignment
with exp.section(uid="parent", alignment=SectionAlignment.RIGHT):
# Right-aligned section with 1 microsecond length
with exp.section(
uid="excitation", alignment=SectionAlignment.RIGHT, length=1e-6
):
# Section contents
exp.play(signal="drive", pulse=x90)
exp.delay(signal="drive", time=100e-9)
exp.play(signal="drive", pulse=x90)
exp.reserve(signal="drive1")
# Left-aligned section with 50 ns length
with exp.section(
uid="excitation1", alignment=SectionAlignment.LEFT, length=500e-9
) as excitation1:
# Section contents
exp.play(signal="drive1", pulse=x180)
exp.delay(signal="drive1", time=50e-9)
exp.play(signal="drive1", pulse=x90)
exp.add(section=excitation1)
# set experiment calibration and signal map
exp.set_signal_map(map_q0_q1_drive)
# compile
compiled_exp = session.compile(exp)
# run the experiment
my_results = session.run()
# show pulse sheet
show_pulse_sheet("11_Reserving_Signals", session.compiled_experiment)
Ramsey Sequence¶
Now we'll implement a full Ramsey sequence using one drive line, a measurement line, and a acquisition line.
We'll first define some new pulses along with sweeping and averaging parameters.
Parameters and pulse definition¶
# pulse definitions
drive_pulse = pulse_library.gaussian(uid="gaussian_drive", length=700e-9, amplitude=1)
readout_pulse = pulse_library.const(uid="Readout", length=300e-9, amplitude=0.8)
# averages
n_average = 2
# sweep parameters
n_steps = 7
start_delay = 0
stop_delay = 13e-6
time_sweep = LinearSweepParameter(
uid="time_sweep_param", start=start_delay, stop=stop_delay, count=n_steps
)
Pulse Sequence¶
Now we get to our Ramsey experiment! We make use of all of the concepts we've introduced above, along with adding a sweep
section, where we can choose a parameter that changes, in this case, the delay time between pulses.
# Ramsey experiment and pulse sequence
exp_ramsey = Experiment(
uid="Ramsey",
signals=[
ExperimentSignal("drive"),
ExperimentSignal("measure"),
ExperimentSignal("acquire"),
],
)
# outer loop - real-time, cyclic averaging
with exp_ramsey.acquire_loop_rt(
uid="RT_Shots",
count=n_average,
averaging_mode=AveragingMode.CYCLIC,
acquisition_type=AcquisitionType.INTEGRATION,
repetition_mode=RepetitionMode.AUTO,
):
# inner loop - real time sweep of Ramsey time delays
with exp_ramsey.sweep(
uid="Ramsey_sweep", parameter=time_sweep, alignment=SectionAlignment.RIGHT
):
# play qubit excitation pulse - delay between pulses is swept
with exp_ramsey.section(uid="qubit_excitation"):
exp_ramsey.play(signal="drive", pulse=drive_pulse)
exp_ramsey.delay(signal="drive", time=time_sweep)
exp_ramsey.play(signal="drive", pulse=drive_pulse)
# readout pulse and data acquisition
with exp_ramsey.section(uid="readout_section", play_after="qubit_excitation"):
# play readout pulse on measure line
exp_ramsey.play(signal="measure", pulse=readout_pulse)
# trigger signal data acquisition
exp_ramsey.acquire(
signal="acquire",
handle="ramsey",
kernel=readout_pulse,
)
with exp_ramsey.section(uid="delay", length=1e-6):
# relax time after readout - for qubit relaxation to groundstate and signal processing
exp_ramsey.reserve(signal="measure")
Signal Map¶
As we have introduced new signal lines, we have to define a new map to our logical signals.
# define signal map
map_Ramsey = {
"drive": device_setup.logical_signal_groups["q0"].logical_signals["drive_line"],
"measure": device_setup.logical_signal_groups["q0"].logical_signals["measure_line"],
"acquire": device_setup.logical_signal_groups["q0"].logical_signals["acquire_line"],
}
Calibration¶
We also add more items to our calibration:
# Basic calibration of q0 IF and LO readout frequencies
readout_qo_if = Oscillator(
uid="readout" + "_q0" + "_if",
frequency=50e6,
modulation_type=ModulationType.SOFTWARE,
)
readout_q0_lo = Oscillator(
uid="readout" + "_q0" + "_lo",
frequency=6.0e9,
)
def calibrate_devices_readout(device_setup):
# measure drive line q0
device_setup.logical_signal_groups["q0"].logical_signals[
"measure_line"
].calibration = SignalCalibration(
oscillator=readout_qo_if, port_delay=0, local_oscillator=readout_q0_lo, range=10
)
# acquisition line q0
device_setup.logical_signal_groups["q0"].logical_signals[
"acquire_line"
].calibration = SignalCalibration(
oscillator=readout_qo_if,
# for an experiment on hardware add an offset between the readout pulse
# and the start of the data acquisition
# to compensate for round-trip time of readout pulse
port_delay=0,
local_oscillator=readout_q0_lo,
range=10,
)
calibrate_devices_readout(device_setup)
device_setup.list_calibratables()
# set experiment calibration and signal map
exp_ramsey.set_signal_map(map_Ramsey)
# compile
compiled_exp = session.compile(exp_ramsey)
# run the experiment
my_results = session.run()
# show pulse sheet
show_pulse_sheet("12_Ramsey", session.compiled_experiment)