One- and Two-Qubit Randomized Benchmarking in LabOne Q with Qiskit¶
In this notebook, we'll use the Qiskit Experiment Library to generate one and two qubit randomized benchmarking experiments. We'll then export the generated experiment to OpenQASM, import these OpenQASM experiments into LabOne Q, compile, and simulate the output signals.
When generating randomized benchmarking experiments in Qiskit, it will return a list of quantum circuits with the specified parameters. We show here how to efficiently import, compile and execute such a list into LabOne Q, resulting in a single, large experiment.
Imports¶
from __future__ import annotations
# LabOne Q:
# additional imports
# device setup and descriptor
from laboneq import openqasm3
# plotting functionality
from laboneq.contrib.example_helpers.plotting.plot_helpers import plot_simulation
# core LabOne Q functionality
from laboneq.simple import *
# qiskit
from qiskit import qasm3, transpile
from qiskit_experiments.library import randomized_benchmarking
Define the Experimental Setup¶
Let's define our experimental setup. We will need:
a set of TunableTransmonOperations
a QPU
Here, we will be brief. We will mainly provide the code to obtain these objects. To learn more, check out these other tutorials:
We will use 3 TunableTransmonQubits
in this guide. Change this number to the one describing your setup.
number_of_qubits = 3
DeviceSetup¶
This guide requires a setup that can drive and readout tunable transmon qubits. Your setup could contain an SHFQC+ instrument, or an SHFSG together with an SHFQA instrument. Here, we will use an SHFQC+ with 6 signal generation channels and a PQSC.
If you have used LabOne Q before and already have a DeviceSetup
for your setup, you can reuse that.
If you do not have a DeviceSetup
, you can create one using the code below. Just change the device numbers to the ones in your rack and adjust any other input parameters as needed.
# generate the device setup using a helper function
from laboneq.contrib.example_helpers.generate_device_setup import (
generate_device_setup,
)
setup = generate_device_setup(
number_qubits=number_of_qubits,
pqsc=[{"serial": "DEV10001"}],
hdawg=[
{
"serial": "DEV8001",
"zsync": 0,
"number_of_channels": 8,
"options": "HDAWG8/MF/ME/SKW/PC",
}
],
shfqc=[
{
"serial": "DEV12001",
"zsync": 1,
"number_of_channels": 6,
"readout_multiplex": number_of_qubits,
"options": "SHFQC/QC6CH/PLUS/16W",
}
],
include_flux_lines=True,
multiplex_drive_lines=True, # adds drive_ef signal line
server_host="localhost",
setup_name=f"my_{number_of_qubits}_tunable_qubit_setup",
)
Qubits¶
We will generate 3 TunableTransmonQubits
from the logical signal groups in our DeviceSetup
. The names of the logical signal groups, q0
, q1
, q2
, will be the UIDs of the qubits. Moreover, the qubits will have the same logical signal lines as the ones of the logical signal groups in the DeviceSetup
.
from laboneq_applications.qpu_types.tunable_transmon import TunableTransmonQubit
qubits = TunableTransmonQubit.from_device_setup(setup)
q0, q1, q2 = qubits
for q in qubits:
print("-------------")
print("Qubit UID:", q.uid)
print("Qubit logical signals:")
for sig, lsg in q.signals.items():
print(f" {sig:<10} ('{lsg:>10}')")
Configure the qubit parameters to reflect the properties of the qubits on your QPU using the following code:
for q in qubits:
q.parameters.ge_drive_pulse["sigma"] = 0.25
q.parameters.readout_amplitude = 0.5
q.parameters.reset_delay_length = 1e-6
q.parameters.readout_range_out = -25
q.parameters.readout_lo_frequency = 7.4e9
qubits[0].parameters.drive_lo_frequency = 6.4e9
qubits[0].parameters.resonance_frequency_ge = 6.3e9
qubits[0].parameters.resonance_frequency_ef = 6.0e9
qubits[0].parameters.readout_resonator_frequency = 7.0e9
qubits[1].parameters.drive_lo_frequency = 6.4e9
qubits[1].parameters.resonance_frequency_ge = 6.5e9
qubits[1].parameters.resonance_frequency_ef = 6.3e9
qubits[1].parameters.readout_resonator_frequency = 7.3e9
qubits[2].parameters.drive_lo_frequency = 6.0e9
qubits[2].parameters.resonance_frequency_ge = 5.8e9
qubits[2].parameters.resonance_frequency_ef = 5.6e9
qubits[2].parameters.readout_resonator_frequency = 7.2e9
Quantum Operations¶
Create the set of TunableTransmonOperations
:
from laboneq_applications.qpu_types.tunable_transmon import TunableTransmonOperations
qops = TunableTransmonOperations()
QPU¶
Create the QPU
object from the qubits and the quantum operations
from laboneq.dsl.quantum import QPU
qpu = QPU(qubits, quantum_operations=qops)
Alternatively, load from a file¶
If you already have a DeviceSetup
and a QPU
stored in .json
files, you can simply load them back using the code below:
from laboneq import serializers
setup = serializers.load(full_path_to_device_setup_file)
qpu = serializers.load(full_path_to_qpu_file)
qubits = qpu.qubits
qops = qpu.quantum_operations
Connect to Session¶
session = Session(setup)
session.connect(do_emulation=True) # do_emulation=False when at a real setup
QASM operations¶
We need to make sure that the gate names that appear in the OpenQASM program are defined in our set of quantum operations. For the randomized benchmarking experiments in this notebook, we will need the gates x
, sx
, rz
, cx
, and a reset
operation to implement active reset.
The rz
operation already exists in our set of quantum operations:
qpu.quantum_operations.rz.src
The sx
, x
, and reset
operations exist in our set under the names x90
, x180
, and active_reset
. Below, we create aliases for these existing operations to map them to the names of the equivalent operations in the OpenQASM program. To learn more about creating aliases for quantum operations, check out our tutorial.
qops = qpu.quantum_operations
qops["sx"] = qops.x90
qops["x"] = qops.x180
qops["reset"] = qops.active_reset
Finally, we define the cx
operation and register it to our existing set. Check out our tutorial to learn more about registering a new operation to an existing set of quantum operations.
@qops.register
def cx(self, q_control: Transmon, q_target: Transmon) -> None:
"""An operation implementing a cx gate on two qubits.
The controlled X gate is implemented using a cross-resonance gate.
"""
cx_id = f"cx_{q_control.uid}_{q_target.uid}"
# define cancellation pulses for target and control
cancellation_control_n = dsl.create_pulse(
{"function": "gaussian_square"}, name="CR-"
)
cancellation_control_p = dsl.create_pulse(
{"function": "gaussian_square"}, name="CR+"
)
cancellation_target_p = dsl.create_pulse(
{"function": "gaussian_square"}, name="q1+"
)
cancellation_target_n = dsl.create_pulse(
{"function": "gaussian_square"}, name="q1-"
)
# play X pulses on both target and control
with dsl.section(name=f"{cx_id}_x_both") as x180_both:
self.x180(q_control)
self.x180(q_target)
# First cross-resonance component
with dsl.section(
name=f"{cx_id}_canc_p", play_after=x180_both.uid
) as cancellation_p:
dsl.play(signal=q_target.signals["drive"], pulse=cancellation_target_p)
dsl.play(signal=q_control.signals["flux"], pulse=cancellation_control_n)
# play X pulse on control
x180_control = self.x180(q_control)
x180_control.play_after = cancellation_p.uid
# Second cross-resonance component
with dsl.section(name=f"cx_{cx_id}_canc_n", play_after=x180_control.uid):
dsl.play(signal=q_target.signals["drive"], pulse=cancellation_target_n)
dsl.play(signal=q_control.signals["flux"], pulse=cancellation_control_p)
Great! Now we can move on to defining and running the randomized benchmarking experiments.
Single-Qubit Randomized Benchmarking using Qiskit¶
You'll start by creating Standard RB experiments from the Qiskit Experiment Library here. Here, we do this for one qubit for a few different RB sequence lengths.
Note that most circuits that can be generated in Qiskit and converted to OpenQASM could be adapted to be run in a similar way in LabOne Q!
Define Circuits with Qiskit¶
# Use Qiskit Experiment Library to Generate RB
rb1_qiskit_circuits = randomized_benchmarking.StandardRB(
physical_qubits=[0],
lengths=[4, 8, 16],
num_samples=2,
).circuits()
Handling the measurement operations in an OpenQASM program is currently not supported (see the Known-Issues section in our release notes).
Here, we strip the measurement operation from the Qiskit circuit. We will add them to the LabOne Q experiment in OpenQASMTranspiler.batch_experiment
.
for circuit in rb1_qiskit_circuits:
circuit.remove_final_measurements()
rb1_qiskit_circuits[2].draw()
You can then use the Qiskit transpile
function to obtain a representation of the circuits in your favorite set of basis gates.
Below, we choose the basis ["id", "sx", "x", "rz", "cx"]
. Note that all these gates (except the identity "id") must exist in your set of quantum operations!
# Choose basis gates
rb1_transpiled_circuits = transpile(
rb1_qiskit_circuits, basis_gates=["id", "sx", "x", "rz", "cx"]
)
rb1_program_list = [qasm3.dumps(circuit) for circuit in rb1_transpiled_circuits]
print(rb1_program_list[2])
Execute a single QASM program¶
Now, you'll transpile a single OpenQASM program into a LabOne Q Experiment
pulse sequence using the class OpenQASMTranspiler
and the options class SingleProgramOptions
. Check out the LabOne Q QASM transpiler tutorial to learn more about this interface.
Once you've done that, you can compile your Experiment
and plot the output using the LabOne Q simulator.
Note: the parameter qubit_map
below may need to be updated to match the names of the qubit register from your QASM circuit!
Below, we choose the QASM program defined in the third entry of rb1_program_list
.
# Instantiate OpenQASMTranspiler from the QPU
transpiler = openqasm3.OpenQASMTranspiler(qpu)
# Define options
options = openqasm3.SingleProgramOptions()
# We will not change any of the default options
# Create the Experiment
rb1_exp_single_program = transpiler.experiment(
program=rb1_program_list[2],
qubit_map={"q": [q0]},
options=options,
)
# Compile the Experiment
rb1_compiled_exp_single_program = session.compile(rb1_exp_single_program)
# Run the Experiment
rb1_results_single_program = session.run(rb1_compiled_exp_single_program)
Look at the simulated output¶
plot_simulation(
rb1_compiled_exp_single_program,
length=0.8e-6,
plot_width=12,
plot_height=3,
signal_names_to_show=["drive"],
)
Draw the circuit from above¶
rb1_transpiled_circuits[2].draw()
Look at the pulse sheet¶
show_pulse_sheet(name="1-qubit RB", compiled_experiment=rb1_compiled_exp_single_program)
Execute the full RB Experiment¶
Below, we will use the .batch_experiment()
method to create and Experiment
from our list of QASM programs rb1_program_list
, containing the full single-qubit RB experiment.
The entries in rb1_program_list
are individual RB sequences of a given number of gates, m
. In total, there will be $m\times K$ sequences in the list, where K
is the number of randomizations of each length (the num_samples
parameter in the Qiskit interface above). In our choice above, we have $K=2$ and $m\in \{4, 8, 16\}$.
In the MultiProgramOptions
, you can use the field batch_execution_mode
to specify how all these RB sequences in the list should be executed:
- all in real-time ("rt");
- every sequence of
m
gates in real-time and the iteration over the sequences in near-time ("nt"); - split the entries into the number of near-time steps (called "chunks") using the pipeliner ("pipeline"). Specify the number of chunks to use in the options field
pipeline_chunk_count
.
Below, we use the "pipeline" option, and split our 10 sequences into 2 chunks of 3 RB sequences each. This means that we will have two near-time steps, and each real-time loop will run over 3 RB sequences (all the lengths, in our case).
Note that here we use a different options class, MultiProgramOptions
.
# Instantiate OpenQASMTranspiler from the QPU
transpiler = openqasm3.OpenQASMTranspiler(qpu)
# Define options
options = openqasm3.MultiProgramOptions()
options.repetition_time = 20e-5
options.batch_execution_mode = "pipeline"
options.pipeline_chunk_count = 2
options.add_measurement = (
True # adds the measurement operations which were removed above
)
# Set the format of the result handles to align with the handles
# defined in our quantum operations:
options.add_measurement_handle = "{qubit.uid}/result"
# Create the Experiment
rb1_exp = transpiler.batch_experiment(
programs=rb1_program_list,
qubit_map={"q": [q0]},
options=options,
)
# Compile the Experiment
rb1_compiled_exp = session.compile(rb1_exp)
# Run the Experiment
rb1_results = session.run(rb1_compiled_exp)
## KNOWN ISSUE - pulse sheet viewer and output simulation are not available
Execute the full RB Experiment - including active qubit reset¶
Let's re-run the single-qubit RB experiment with active reset. Just set the options field .add_reset
to True
.
Two-Qubit Randomized Benchmarking using Qiskit¶
# Instantiate OpenQASMTranspiler from the QPU
transpiler = openqasm3.OpenQASMTranspiler(qpu)
# Define options
options = openqasm3.MultiProgramOptions()
options.repetition_time = 20e-5
options.batch_execution_mode = "pipeline"
options.pipeline_chunk_count = 2
options.add_measurement = (
True # adds the measurement operations which were removed above
)
# Set the format of the result handles to align with the handles
# defined in our quantum operations:
options.add_measurement_handle = "{qubit.uid}/result"
options.add_reset = True
# Create the Experiment
rb1_exp_with_reset = transpiler.batch_experiment(
programs=rb1_program_list,
qubit_map={"q": [q0]},
options=options,
)
# Compile the Experiment
rb1_compiled_exp_with_reset = session.compile(rb1_exp_with_reset)
# Run the Experiment
rb1_results_with_reset = session.run(rb1_compiled_exp_with_reset)
Define Circuits with Qiskit¶
# Use Qiskit Experiment Library to Generate RB
rb2_qiskit_circuits = randomized_benchmarking.StandardRB(
physical_qubits=[0, 1],
lengths=[4, 8, 16],
num_samples=2,
).circuits()
Handling the measurement operations in an OpenQASM program is currently not supported (see the Known-Issues section in our release notes).
Here, we strip the measurement operation from the Qiskit circuit. We will add them to the LabOne Q experiment in OpenQASMTranspiler.batch_experiment
.
for circuit in rb2_qiskit_circuits:
circuit.remove_final_measurements()
rb2_qiskit_circuits[0].draw()
You can then use the Qiskit transpile
function to obtain a representation of the circuits in your favorite set of basis gates.
Below, we choose the basis ["id", "sx", "x", "rz", "cx"]
. Note that all these gates (except the identity "id") must exist in your set of quantum operations!
# Choose basis gates
rb2_transpiled_circuits = transpile(
rb2_qiskit_circuits, basis_gates=["id", "sx", "x", "rz", "cx"]
)
rb2_program_list = [qasm3.dumps(circuit) for circuit in rb2_transpiled_circuits]
print(rb2_program_list[0])
Execute a single QASM program¶
Now, you'll transpile a two-qubit OpenQASM program into a LabOne Q Experiment
pulse sequence using the class OpenQASMTranspiler
and the options class SingleProgramOptions
. Check out the LabOne Q QASM transplier tutorial to learn more about this interface.
Once you've done that, you can compile your Experiment
and plot the output using the LabOne Q simulator.
Note: the parameter qubit_map
below may need to be updated to match the names of the qubit register from your QASM circuit!
Below, we choose the QASM program defined in the first entry of rb2_program_list
.
# Instantiate OpenQASMTranspiler from the QPU
transpiler = openqasm3.OpenQASMTranspiler(qpu)
# Define options
options = openqasm3.SingleProgramOptions()
# We will not change any of the default options
# Create the Experiment
rb2_exp_single_program = transpiler.experiment(
program=rb2_program_list[0],
qubit_map={"q": [q0, q1]},
options=options,
)
# Compile the Experiment
rb2_compiled_exp_single_program = session.compile(rb2_exp_single_program)
# Run the Experiment
rb2_results_single_program = session.run(rb2_compiled_exp_single_program)
Look at the simulated output¶
plot_simulation(
rb2_compiled_exp_single_program,
length=3e-6,
plot_width=12,
plot_height=3,
signal_names_to_show=[
"q0/drive",
"q0/flux",
"q1/drive",
"q1/flux",
],
)
Draw the circuit from above¶
rb2_transpiled_circuits[0].draw()
Look at the pulse sheet¶
show_pulse_sheet(
name="2-qubit RB",
compiled_experiment=rb2_compiled_exp_single_program,
max_events_to_publish=10e4,
)
Execute the full RB Experiment¶
Below, we will use the .batch_experiment()
method to create and Experiment
from our list of QASM programs, rb2_program_list
, containing the full two-qubit RB experiment.
The entries in rb2_program_list
are individual RB sequences of a given number of gates, m
. In total, there will be $m\times K$ sequences in the list, where K
is the number of randomizations of each length (the num_samples
parameter in the Qiskit interface above). In our choice above, we have $K=2$ and $m\in \{4, 8, 16\}$.
In the MultiProgramOptions
, you can use the field batch_execution_mode
to specify how all these RB sequences in the list should be executed:
- all in real-time ("rt");
- every sequence of
m
gates in real-time and the iteration over the sequences in near-time ("nt"); - split the entries into the number of near-time steps (called "chunks") using the pipeliner ("pipeline"). Specify the number of chunks to use in the options field
pipeline_chunk_count
.
Below, we use the "pipeline" option, and split our 10 sequences into 2 chunks of 3 RB sequences each. This means that we will have two near-time steps, and each real-time loop will run over 3 RB sequences (all the lengths, in our case).
Note that here we use a different options class, MultiProgramOptions
.
# Instantiate OpenQASMTranspiler from the QPU
transpiler = openqasm3.OpenQASMTranspiler(qpu)
# Define options
options = openqasm3.MultiProgramOptions()
options.repetition_time = 100e-5
options.batch_execution_mode = "pipeline"
options.pipeline_chunk_count = 2
options.add_measurement = (
True # adds the measurement operations which were removed above
)
# Set the format of the result handles to align with the handles
# defined in our quantum operations:
options.add_measurement_handle = "{qubit.uid}/result"
# Create the Experiment
rb2_exp = transpiler.batch_experiment(
programs=rb2_program_list,
qubit_map={"q": [q0, q1]},
options=options,
)
# Compile the Experiment
rb2_compiled_exp = session.compile(rb2_exp)
# Run the Experiment
rb2_results = session.run(rb2_compiled_exp)
## KNOWN ISSUE - pulse sheet viewer and output simulation are not available
Execute the full RB Experiment - including active qubit reset¶
Let's re-run the two-qubit RB experiment with active reset. Just set the options field .add_reset
to True
.
# Instantiate OpenQASMTranspiler from the QPU
transpiler = openqasm3.OpenQASMTranspiler(qpu)
# Define options
options = openqasm3.MultiProgramOptions()
options.repetition_time = 100e-5
options.batch_execution_mode = "pipeline"
options.pipeline_chunk_count = 2
options.add_measurement = (
True # adds the measurement operations which were removed above
)
# Set the format of the result handles to align with the handles
# defined in our quantum operations:
options.add_measurement_handle = "{qubit.uid}/result"
options.add_reset = True
# Create the Experiment
rb2_exp_with_reset = transpiler.batch_experiment(
programs=rb2_program_list,
qubit_map={"q": [q0, q1]},
options=options,
)
# Compile the Experiment
rb2_compiled_exp_with_reset = session.compile(rb2_exp_with_reset)
# Run the Experiment
rb2_results_with_reset = session.run(rb2_compiled_exp_with_reset)