Sweeping in combination with match-case statements¶
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.
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 *
import numpy as np
# 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="q1/drive_line", ports="SGCHANNELS/1/OUTPUT"),
create_connection(to_signal="q2/drive_line", ports="SGCHANNELS/2/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/q1/drive_line"] = SignalCalibration(
local_oscillator=drive_lo
)
cal["/logical_signal_groups/q2/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)
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.
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,
)
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,
)
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.
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,
)
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,
)
Complex nested sweeping¶
Sweep parameter values even in nested sweeps may be used to calculate arbitrary pulse parameters within those sweeps by using nested match-case constructs. The following example demonstrates one such usage.
We use two nested sweep parameters, each of which drives the amplitude of a pulse played on individual lines. The amplitude of a pulse played on a third signal line is then calculated as the product of the two sweep parameter values.
# define a generic drive pulse
pulse_drive = pulse_library.gaussian(length=30e-9)
# define two sweep parameters
amplitudes_1 = LinearSweepParameter(uid="amplitudes_1", start=-0.9, stop=0.9, count=5)
amplitudes_2 = LinearSweepParameter(uid="amplitudes_2", start=-0.5, stop=0.5, count=5)
# define the experiment with three distinct drive signals
@experiment(signals=["drive_q0", "drive_q1", "drive_q2"])
def complex_amplitude_sweeping():
map_signal(
"drive_q0",
device_setup.logical_signal_groups["q0"].logical_signals["drive_line"],
)
map_signal(
"drive_q1",
device_setup.logical_signal_groups["q1"].logical_signals["drive_line"],
)
map_signal(
"drive_q2",
device_setup.logical_signal_groups["q2"].logical_signals["drive_line"],
)
with acquire_loop_rt(1):
# sweep first signal amplitude
with sweep(uid="amplitude_sweep_1", parameter=amplitudes_1):
# sweep second signal amplitude
with sweep(uid="amplitude_sweep_2", parameter=amplitudes_2):
# construct a nested match-case structure for all parameter values of the enclosing sweeps
with match(sweep_parameter=amplitudes_1, uid="nested_match_case"):
for amp_1 in amplitudes_1.values:
with case(amp_1):
with match(sweep_parameter=amplitudes_2):
for amp_2 in amplitudes_2.values:
with case(amp_2):
# play pulse with first signal amplitude
play(
signal="drive_q0",
pulse=pulse_drive,
amplitude=amplitudes_1,
)
# play pulse with second signal amplitude
play(
signal="drive_q1",
pulse=pulse_drive,
amplitude=amplitudes_2,
phase=np.pi / 2,
)
# play pulse with amplitude and phase calculated depending on both outer sweep parameters
play(
signal="drive_q2",
pulse=pulse_drive,
amplitude=amp_1 * amp_2 + 0.1,
phase=amp_2 * np.pi / 4,
)
delay(signal="drive_q0", time=25e-9)
# add a delay for better visibility
with section(uid="sweep_delay", play_after="amplitude_sweep_2"):
delay(signal="drive_q0", time=100e-9)
# compile experiment and plot the simulated output
compiled_complex_amplitude_sweeping = session.compile(complex_amplitude_sweeping())
plot_simulation(
compiled_complex_amplitude_sweeping,
start_time=0,
length=2e-6,
signals=["drive_q0", "drive_q1", "drive_q2"],
plot_height=5,
plot_width=15,
)