Randomized Benchmarking¶
An advanced use case example - Randomized benchmarking using the Clifford group
One applies random sequences of Clifford gates for different sequence lengths followed by a recovery gate - the resulting decay of the state fidelity as function of sequence length is a measure of overall gate fidelity
0. General Imports and Definitions¶
0.1 Python Imports¶
from __future__ import annotations
import matplotlib.pyplot as plt
import numpy as np
from laboneq.contrib.example_helpers.plotting.plot_helpers import plot_simulation
# Helpers:
# additional imports needed for Clifford gate calculation
from laboneq.contrib.example_helpers.randomized_benchmarking_helper import (
clifford_parametrized,
generate_play_rb_pulses,
make_pauli_gate_map,
)
# LabOne Q:
from laboneq.simple import *
1. Setting up the LabOne Q Software¶
Define the device setup, experimental parameters and baseline calibration
Establish a session and connect to it
1.1 Create 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"),
)
1.2 Define Qubit / Experiment Parameters¶
# a collection of qubit control and readout parameters as a python dictionary
qubit_parameters = {
"freq": 100e6, # qubit 0 drive frequency in [Hz] - relative to local oscillator for qubit drive upconversion
"ro_freq": 50e6,
"ro_delay": 0,
"ro_int_delay": 0e-9,
"qb_len_spec": 1e-6,
"qb_amp_spec": 1.0,
"pi_amp": 1,
"qb_len": 200e-9,
"freq_ef": -500e6,
"ro_len": 200e-9,
"ro_amp": 0.8,
"relax": 50e-9,
}
# up / downconversion settings - to convert between IF and RF frequencies
lo_settings = {
"shfqa_lo": 6.0e9, # SHFQA LO Frequency
"shfsg_lo": 5.0e9, # SHFSG LO Frequencies, one center frequency per two channels
}
1.3 Baseline Calibration for Device Setup¶
# function that defines a setup calibration containing the qubit / readout parameters
def define_calibration(qubit_parameters, lo_settings):
qubit0_ro_lo = Oscillator(
uid="ro_lo_" + "q0" + "_osc",
frequency=lo_settings["shfqa_lo"],
)
qubit_0_drive_lo = Oscillator(
uid="drive_lo" + "q0" + "_osc",
frequency=lo_settings["shfsg_lo"],
)
# the calibration object will later be applied to the device setup
my_calibration = Calibration()
## Calibration information for qubit 0
# qubit drive line - the calibration object contains SignalCalibration entries for each logical signal
my_calibration["/logical_signal_groups/q0/drive_line"] = SignalCalibration(
# each logical signal can have an oscillator associated with it
oscillator=Oscillator(
"q0_drive_osc",
frequency=qubit_parameters["freq"],
modulation_type=ModulationType.HARDWARE,
),
local_oscillator=qubit_0_drive_lo,
range=10,
)
# readout drive line
my_calibration["/logical_signal_groups/q0/measure_line"] = SignalCalibration(
oscillator=Oscillator(
"q0_measure_osc",
frequency=qubit_parameters["ro_freq"],
modulation_type=ModulationType.SOFTWARE,
),
port_delay=qubit_parameters["ro_delay"],
local_oscillator=qubit0_ro_lo,
range=10,
)
# acquisition line
my_calibration["/logical_signal_groups/q0/acquire_line"] = SignalCalibration(
oscillator=Oscillator(
"q0_acquire_osc",
frequency=qubit_parameters["ro_freq"],
modulation_type=ModulationType.SOFTWARE,
),
# 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=qubit_parameters["ro_delay"] + qubit_parameters["ro_int_delay"],
local_oscillator=qubit0_ro_lo,
range=-10,
# add a threshold for the state discrimination -- this requires optimized readout integrator weights
threshold=0.0,
)
return my_calibration
1.4 Apply Baseline Calibration¶
# define Calibration object based on qubit control and readout parameters
my_calibration = define_calibration(qubit_parameters, lo_settings)
# apply calibration to device setup
device_setup.set_calibration(my_calibration)
## define shortcut to logical signals for convenience
lsg_q0 = device_setup.logical_signal_groups["q0"].logical_signals
drive_Oscillator_q0 = lsg_q0["drive_line"].oscillator
readout_Oscillator_q0 = lsg_q0["measure_line"].oscillator
acquire_Oscillator_q0 = lsg_q0["acquire_line"].oscillator
# map experiment signals to logical signals
map_q0 = {
"drive": "/logical_signal_groups/q0/drive_line",
"measure": "/logical_signal_groups/q0/measure_line",
"acquire": "/logical_signal_groups/q0/acquire_line",
}
1.5 Create a Session and Connect to it¶
emulate = True # perform experiments in emulation mode only?
my_session = Session(device_setup=device_setup)
my_session.connect(do_emulation=emulate)
2. Randomized Benchmarking¶
Perform a randomized benchmarking experiment on a qubit
2.1 Additional Experimental Parameters and Pulses¶
Define the number of averages and the pulses used in the experiment
# qubit readout pulse
readout_pulse = pulse_library.const(
uid="readout_pulse",
length=qubit_parameters["ro_len"],
amplitude=qubit_parameters["ro_amp"],
)
# integration weights for qubit measurement
integration_kernel = pulse_library.const(
uid="readout_weighting_function", length=qubit_parameters["ro_len"], amplitude=1.0
)
2.1.1 Adjust Pulse Parameters for Clifford Gates¶
Define and prepare the basic gate set and the pulse objects corresponding to them
pulse_reference = pulse_library.gaussian
pulse_parameters = {"sigma": 1 / 3}
pulse_length = 64e-9
gate_map = make_pauli_gate_map(
pi_pulse_amp=0.8,
pi_half_pulse_amp=0.42,
excitation_length=pulse_length,
pulse_factory=pulse_reference,
pulse_kwargs=pulse_parameters,
)
2.2 Define and run the RB Experiment¶
The RB experiment will consist of random sequences of different lengths, where each sequence length contains a number of random instances.
Create Randomized Benchmarking Experiment¶
In real time (within acquire_loop_rt
), the sequence lengths are swept, and for each sequence length, n_sequences_per_length
random sequences are created.
Each random sequence consists of three sections:
- A right-aligned drive section, which is populated by the helper function
generate_play_rb_pulses
- A readout section
- A relax section
generate_play_rb_pulses
first creates a random sequence of Clifford gates together with the recovery gate. Then, the Clifford gates in the sequence are decomposed into the basic gate set and played via an Experiment.play
command.
The handle
in the acquire
command follows the sequence length, facilitating straight-forward result processing after the experiment.
# define a convenience function to generate the RB sequences
def sweep_rb_pulses(
sequence_length: SweepParameter | LinearSweepParameter,
exp: Experiment,
signal: str,
cliffords,
gate_map,
rng,
):
with exp.match(sweep_parameter=sequence_length):
for v in sequence_length.values:
with exp.case(v):
generate_play_rb_pulses(
exp=exp,
signal=signal,
seq_length=v,
cliffords=cliffords,
gate_map=gate_map,
rng=rng,
)
# define the RB experiment
def define_rb_experiment(
num_average=2**8,
min_sequence_exponent=1,
max_sequence_exponent=8,
chunk_count=1,
n_sequences_per_length=2,
signal_map=map_q0,
pulse_length=pulse_length,
readout_pulse=readout_pulse,
integration_kernel=integration_kernel,
reset_delay=qubit_parameters["relax"],
prng=None,
):
# construct the sweep over sequence length as powers of 2 of the sequence exponent
sequence_length_sweep = SweepParameter(
values=np.array(
[2**it for it in range(min_sequence_exponent, max_sequence_exponent + 1)]
)
)
# we are using fixed timing, where the maximum duration is determined by the maximum sequence length
max_seq_duration = 2**max_sequence_exponent * 3 * pulse_length
prng = np.random.default_rng(seed=42) if prng is None else prng
exp_rb = Experiment(
uid="RandomizedBenchmark",
signals=[
ExperimentSignal("drive"),
ExperimentSignal("measure"),
ExperimentSignal("acquire"),
],
)
# outer loop - real-time, cyclic averaging in discrimination mode
with exp_rb.acquire_loop_rt(
uid="rb_shots",
count=num_average,
averaging_mode=AveragingMode.CYCLIC,
acquisition_type=AcquisitionType.DISCRIMINATION,
):
# inner loop - sweep over sequence lengths
with exp_rb.sweep(
parameter=sequence_length_sweep,
chunk_count=chunk_count,
) as sequence_length:
# innermost loop - different random sequences for each length
## KNOWN ISSUE: using a sweep instead of the for loop here will lead to unchanged sequences
for num in range(n_sequences_per_length):
# with exp_rb.sweep(parameter=iteration_sweep):
with exp_rb.section(
uid=f"drive_{num}",
length=max_seq_duration,
alignment=SectionAlignment.RIGHT,
):
sweep_rb_pulses(
sequence_length,
exp_rb,
"drive",
clifford_parametrized,
gate_map,
prng,
)
# readout and data acquisition
with exp_rb.section(uid=f"measure_{num}", play_after=f"drive_{num}"):
exp_rb.measure(
measure_pulse=readout_pulse,
measure_signal="measure",
acquire_signal="acquire",
handle="rb_results",
integration_kernel=integration_kernel,
reset_delay=reset_delay,
)
exp_rb.reserve(signal="drive")
exp_rb.set_signal_map(signal_map)
return exp_rb
# Initialise PRNG
my_prng = np.random.default_rng(42)
exp_rb = define_rb_experiment(max_sequence_exponent=3, chunk_count=1)
# compile the experiment
compiled_exp_rb = my_session.compile(exp_rb)
# KNOWN ISSUE - pulse sheet viewer not working for this experiment
# show_pulse_sheet('rb_experiment', compiled_exp_rb)
## KNOWN ISSUE - output simulation is not yet supported with piplined experiments, if chunk_count>1
plot_simulation(
compiled_exp_rb,
start_time=0,
length=10e-6,
plot_width=15,
plot_height=4,
signals=["drive", "measure"],
)
my_results = my_session.run(compiled_exp_rb)
3. Process Results and Plot¶
For each sequence length, the acquired results are averaged and then plotted.
rb_axis = my_results.get_axis("rb_results")
rb_results = my_results.get_data("rb_results")
rb_results
# plt.figure()
plt.plot(rb_axis[0], np.mean(rb_results, axis=1))
plt.xlabel("Sequence Length")
plt.ylabel("Average Fidelity")
plt.show()