Changing the experiment structure with a sweep - match-case statements with sweep parameters¶
Typical sweep parameters in standard experiments change some aspect of the experimental pulse sequence, without changing its underlying structure. However, in experiments like randomized benchmarking or dynamical decoupling, each instance of a sweep parameter will affect the structure of the pulse sequence itself. This notebook will show how to achieve this behavior in LabOne Q, by constructing a match-case statement conditioned on a sweep parameter.
Advanced examples are also available, for example the randomized benchmarking demonstration notebook uses this behavior.
0. Imports and setup¶
from __future__ import annotations
from laboneq.contrib.example_helpers.plotting.plot_helpers import plot_simulation
from laboneq.dsl.experiment.builtins import *
from laboneq.simple import *
# construct a simple device setup
device_setup = DeviceSetup(uid="my_QCCS")
device_setup.add_dataserver(host="localhost", port="8004")
device_setup.add_instruments(
SHFQC(uid="device_shfqc", address="dev12345", device_options="SHFQC/QC6CH")
)
device_setup.add_connections(
"device_shfqc",
create_connection(to_signal="q0/drive_line", ports="SGCHANNELS/0/OUTPUT"),
create_connection(to_signal="q0/measure_line", ports="QACHANNELS/0/OUTPUT"),
create_connection(to_signal="q0/acquire_line", ports="QACHANNELS/0/INPUT"),
)
# set a minimal calibration to the device setup
drive_lo = Oscillator(frequency=1e9)
measure_lo = Oscillator(frequency=4e9)
cal = Calibration()
cal["/logical_signal_groups/q0/drive_line"] = SignalCalibration(
local_oscillator=drive_lo
)
cal["/logical_signal_groups/q0/measure_line"] = SignalCalibration(
local_oscillator=measure_lo
)
cal["/logical_signal_groups/q0/acquire_line"] = SignalCalibration(
local_oscillator=measure_lo
)
device_setup.set_calibration(cal)
# connect to the session
emulate = True
session = Session(device_setup)
session.connect(do_emulation=emulate)
1. Choose different sections or pulses based on sweep parameter¶
In short, a sweep parameters in LabOne Q can appear as the target of a match block:
with sweep(...) as p:
...
with match(sweep_parameter=p):
with case(0):
...
with case(1):
...
...
The individual match arms are selected based on the current value of the sweep parameter when stepping through the sweep.
The following examples sweep the parameter in real-time. This makes for easy visualization using the output simulator. However, the sweep may equally happen in near-time.
1.1 Simple example¶
# define a set of pulses
pulse_const = pulse_library.const(length=100e-9)
pulse_saw = pulse_library.sawtooth(length=100e-9)
# define an experiment
@experiment(signals=["drive"])
def match_sweep_simple():
map_signal(
"drive", device_setup.logical_signal_groups["q0"].logical_signals["drive_line"]
)
with acquire_loop_rt(1):
with sweep_range(start=0, stop=1, count=2) as pulse_type_sweep:
with section():
delay("drive", 100e-9)
# play either constant or sawtooth pulse depending on the value of pulse_type_sweep
with match(sweep_parameter=pulse_type_sweep):
with case(0):
play("drive", pulse_const)
with case(1):
play("drive", pulse_saw)
with section():
delay("drive", 100e-9)
# compile experiment and plot the simulated output
compiled_match_sweep = session.compile(match_sweep_simple())
plot_simulation(
compiled_match_sweep,
start_time=0,
length=6e-7,
signals=["drive"],
plot_height=4,
plot_width=12,
)
1.2 Advanced example - Nesting of match
blocks for different sweep parameters¶
# define a set of pulses
pulse_const = pulse_library.const(length=100e-9)
pulse_saw = pulse_library.sawtooth(length=100e-9)
pulse_gauss = pulse_library.gaussian(length=100e-9)
pulse_triangle = pulse_library.triangle(length=100e-9)
# define an experiment
@experiment(signals=["drive"])
def match_sweep_nested():
map_signal(
"drive", device_setup.logical_signal_groups["q0"].logical_signals["drive_line"]
)
with acquire_loop_rt(1):
with sweep_range(0, 1, 2) as pulse_type_sweep_1:
with sweep_range(0, 1, 2) as pulse_type__sweep_2:
with section():
delay("drive", 100e-9)
with match(sweep_parameter=pulse_type_sweep_1):
with case(0):
with match(sweep_parameter=pulse_type__sweep_2):
with case(0):
play("drive", pulse_const)
with case(1):
play("drive", pulse_saw)
with case(1):
with match(sweep_parameter=pulse_type__sweep_2):
with case(0):
play("drive", pulse_gauss)
with case(1):
play("drive", pulse_triangle)
with section():
delay("drive", 100e-9)
# compile experiment and plot the simulated output
compiled_match_sweep_nested = session.compile(match_sweep_nested())
plot_simulation(
compiled_match_sweep_nested,
start_time=0,
length=1.25e-6,
signals=["drive"],
plot_height=4,
plot_width=12,
)
2. Sweeping pulse count for e.g. dynamical decoupling or RB¶
While LabOne Q does not yet have 1st class support for sweeping pulse count, matchable sweep parameters allow us to get there with only minor workarounds. We can create a dedicated case
section for every pulse count, such that case(N)
contains N
pulses.
For example, the following plays 1, then 2, and finally 3 pulses.
2.1 Simple example¶
# define a pulse
pulse = pulse_library.const(length=30e-9)
# define an experiment
@experiment(signals=["drive"])
def match_pulse_count_simple():
map_signal(
"drive", device_setup.logical_signal_groups["q0"].logical_signals["drive_line"]
)
with acquire_loop_rt(1):
with sweep_range(start=0, stop=2, count=3) as pulse_number_sweep:
with section():
delay("drive", 100e-9)
# vary the number of pulse played based on the value of pulse_number_sweep
with match(sweep_parameter=pulse_number_sweep):
with case(0):
play("drive", pulse)
with case(1):
play("drive", pulse)
delay("drive", 30e-9)
play("drive", pulse)
with case(2):
play("drive", pulse)
delay("drive", 30e-9)
play("drive", pulse)
delay("drive", 30e-9)
play("drive", pulse)
with section():
delay("drive", 100e-9)
# compile experiment and plot the simulated output
compiled_match_pulse_count = session.compile(match_pulse_count_simple())
plot_simulation(
compiled_match_pulse_count,
start_time=0,
length=8e-7,
signals=["drive"],
plot_height=4,
plot_width=12,
)
2.2 Advanced example - using a helper function to implicitly construct the match-case statement¶
This is a helper function that allows us to conveniently express a number of pulses that is parametrized.
def repeat(count: int | SweepParameter | LinearSweepParameter):
def decorator(f):
if isinstance(count, (LinearSweepParameter, SweepParameter)):
with match(sweep_parameter=count):
for v in count.values:
with case(v):
for _ in range(int(v)):
f()
else:
for _ in range(count):
f()
return decorator
Now a similar experiment is easily expressed.
# define 90 and 190 degree rotations
pulse_pi = pulse_library.gaussian(length=30e-9)
pulse_pi_half = pulse_library.gaussian(length=30e-9, amplitude=0.5)
# define a dynamical decoupling experiment
@experiment(signals=["drive"])
def dynamical_decoupling():
map_signal(
"drive", device_setup.logical_signal_groups["q0"].logical_signals["drive_line"]
)
with acquire_loop_rt(1):
with sweep_range(start=2, stop=50, count=10) as pulse_count:
with section(length=2.5e-6):
with section():
play("drive", pulse_pi_half)
delay("drive", 15e-9)
@repeat(pulse_count)
def _():
with section():
play("drive", pulse_pi)
delay("drive", 15e-9)
with section():
play("drive", pulse_pi_half)
# compile experiment and plot the simulated output
compiled_dynamical_decouplimg = session.compile(dynamical_decoupling())
plot_simulation(
compiled_dynamical_decouplimg,
start_time=0,
length=0.6e-5,
signals=["drive"],
plot_height=5,
plot_width=15,
)