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.
0. Python Imports¶
# LabOne Q:
# additional imports
from math import pi
# qiskit
from qiskit import qasm3, transpile
from qiskit_experiments.library import randomized_benchmarking
# device setup and descriptor
from laboneq._utils import id_generator
from laboneq.contrib.example_helpers.generate_example_datastore import (
generate_example_datastore,
get_first_named_entry,
)
# plotting functionality
from laboneq.contrib.example_helpers.plotting.plot_helpers import plot_simulation
# core LabOne Q functionality
from laboneq.simple import *
# Build an in-memory data store with device setup and qubit parameters for the
# example notebooks
setup_db = generate_example_datastore(path="", filename=":memory:")
1. LabOne Q startup¶
1.1 Import pre-calibrated setup - Qubits and setup configuration & set up LabOne Q session¶
# load a calibrated device setup from the dummy database
device_setup = get_first_named_entry(
db=setup_db,
name="12_tuneable_qubit_setup_shfsg_shfqa_shfqc_hdawg_pqsc_calibrated",
)
[q0, q1] = device_setup.qubits[:2]
# create and connect to Session
# use emulation mode - no connection to instruments
use_emulation = True
my_session = Session(device_setup=device_setup)
my_session.connect(do_emulation=use_emulation, reset_devices=True)
2. Defining a LabOne Q Backend¶
Here, we add Gate and Pulse Definitions for Transpilation Support from QASM into LabOne Q
def drive_pulse(qubit: Qubit, label: str, amplitude_scale=1.0):
"""Return a drive pulse for the given qubit.
Pulse parameters are taken from the qubit parameters.
"""
return pulse_library.drag(
uid=f"{qubit.uid}_{label}",
length=qubit.parameters.user_defined["pulse_length"],
amplitude=amplitude_scale * qubit.parameters.user_defined["amplitude_pi"],
)
def rz(qubit: Qubit):
"""Return a parameterized rotation (virtual z) gate for the specified qubit.
The gate is a function that takes the angle to rotate and
returns a LabOne Q section that performs the rotation.
"""
def rz_gate(angle: float):
"""Rz(theta).
Theta is in radians - implements a virtual z-gate
"""
gate = Section(uid=id_generator(f"p_{qubit.uid}_rz_{int(180 * angle / pi)}"))
gate.play(
signal=qubit.signals["drive"],
pulse=None,
increment_oscillator_phase=angle,
)
return gate
return rz_gate
def measurement(qubit: Qubit):
"""Return a measurement operation of the specified qubit.
The operation is a function that takes the measurement handle (a string)
and returns a LabOne Q section that performs the measurement.
"""
def measurement_gate(handle: str):
"""Perform a measurement.
Handle is the name of where to store the measurement result. E.g. "meas[0]".
"""
measure_pulse = pulse_library.gaussian_square(
uid=f"{qubit.uid}_readout_pulse",
length=qubit.parameters.user_defined["readout_length"],
amplitude=qubit.parameters.user_defined["readout_amplitude"],
zero_boundaries=True,
)
integration_kernel = pulse_library.const(
uid=f"{qubit.uid}_integration_kernel",
length=qubit.parameters.user_defined["readout_length"],
)
gate = Section(uid=id_generator(f"meas_{qubit.uid}_{handle}"))
gate.reserve(signal=qubit.signals["drive"])
gate.play(signal=qubit.signals["measure"], pulse=measure_pulse)
gate.acquire(
signal=qubit.signals["acquire"],
handle=handle,
kernel=integration_kernel,
)
return gate
return measurement_gate
def reset(qubit: Qubit, reset_pulse):
"""Reset the specified qubit to the ground state with the supplied reset pulse.
The reset gate function takes no arguments and returns a LabOne Q section that performs
the reset.
"""
def reset_gate():
sig = qubit.signals
# Reset Section
reset = Section(uid=f"{qubit.uid}_reset")
# qubit state readout
readout = measurement(qubit)(f"{qubit.uid}_qubit_state")
# delay after measurement
readout.delay(
signal=sig["acquire"],
time=qubit.parameters.user_defined["reset_delay_length"],
)
# real-time feedback, fetching the measurement data identified by handle locally from the QA unit of the SHFQC
match_case = Match(
uid=f"{qubit.uid}_feedback",
handle=f"{qubit.uid}_qubit_state",
play_after=readout,
)
# measurement result 0 - ground state
case_0 = Case(uid=f"{qubit.uid}_0_Case", state=0)
case_0.play(signal=sig["drive"], pulse=reset_pulse, amplitude=0.01)
# measurement result 1 - excited state
case_1 = Case(uid=f"{qubit.uid}_1_Case", state=1)
# play x180 pulse
case_1.play(signal=sig["drive"], pulse=reset_pulse)
match_case.add(case_0)
match_case.add(case_1)
reset.add(readout)
reset.add(match_case)
return reset
return reset_gate
def cx(control: Qubit, target: Qubit):
"""Return a controlled X gate for the specified control and target qubits.
The CX gate function takes the control and target qubit and returns a LabOne Q section that performs
a controlled X gate between these two qubits using a cross-resonance scheme.
"""
def cx_gate():
cx_id = f"cx_{control.uid}_{target.uid}"
gate = Section(uid=id_generator(cx_id))
# define X pulses for target and control
x180_pulse_control = drive_pulse(control, label="x180")
x180_pulse_target = drive_pulse(target, label="x180")
# define cancellation pulses for target and control
cancellation_control_n = pulse_library.gaussian_square(uid="CR-")
cancellation_control_p = pulse_library.gaussian_square(uid="CR+")
cancellation_target_p = pulse_library.gaussian_square(uid="q1+")
cancellation_target_n = pulse_library.gaussian_square(uid="q1-")
# play X pulses on both target and control
x180_both = Section(uid=id_generator(f"{cx_id}_x_both"))
x180_both.play(signal=control.signals["drive"], pulse=x180_pulse_control)
x180_both.play(signal=target.signals["drive"], pulse=x180_pulse_target)
gate.add(x180_both)
# First cross-resonance component
cancellation_p = Section(
uid=id_generator(f"{cx_id}_canc_p"), play_after=x180_both.uid
)
cancellation_p.play(signal=target.signals["drive"], pulse=cancellation_target_p)
cancellation_p.play(
signal=control.signals["flux"], pulse=cancellation_control_n
)
gate.add(cancellation_p)
# play X pulse on control
x180_control = Section(
uid=id_generator(f"{cx_id}_x_q0"), play_after=cancellation_p.uid
)
x180_control.play(signal=control.signals["drive"], pulse=x180_pulse_control)
gate.add(x180_control)
# Second cross-resonance component
cancellation_n = Section(
uid=id_generator(f"cx_{cx_id}_canc_n"), play_after=x180_control.uid
)
cancellation_n.play(signal=target.signals["drive"], pulse=cancellation_target_n)
cancellation_n.play(
signal=control.signals["flux"], pulse=cancellation_control_p
)
gate.add(cancellation_n)
return gate
return cx_gate
3. Randomised benchmarking circuits from Qiskit¶
You'll start by creating Standard RB experiments from the Qiskit Experiment Library here. We do this for one and two qubits for a few different 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!
# Use Qiskit Experiment Library to Generate RB
rb1_qiskit_circuits = randomized_benchmarking.StandardRB(
physical_qubits=[0],
lengths=[4, 8, 16],
num_samples=2,
).circuits()
rb2_qiskit_circuits = randomized_benchmarking.StandardRB(
physical_qubits=[0, 1],
lengths=[4, 8, 16],
num_samples=2,
).circuits()
When efficiently importing and executing a list of quantum circuits, there currently are strong limitations as to how the measurements are scheduled in these experiment. We strip them here from the Qiskit circuit. We will re-add them to the LabOne Q experiment separately when doing the import.
for circuit in rb1_qiskit_circuits:
circuit.remove_final_measurements()
for circuit in rb2_qiskit_circuits:
circuit.remove_final_measurements()
rb1_qiskit_circuits[2].draw()
rb2_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.
# Choose basis gates
rb1_transpiled_circuits = transpile(
rb1_qiskit_circuits, basis_gates=["id", "sx", "x", "rz", "cx"]
)
rb2_transpiled_circuits = transpile(
rb2_qiskit_circuits, basis_gates=["id", "sx", "x", "rz", "cx"]
)
rb1_transpiled_circuits[2].draw()
rb1_program_list = []
for circuit in rb1_transpiled_circuits:
rb1_program_list.append(qasm3.dumps(circuit))
rb2_program_list = []
for circuit in rb2_transpiled_circuits:
rb2_program_list.append(qasm3.dumps(circuit))
print(rb1_program_list[2])
4. Execute one Qubit RB¶
Define Gates, Load QASM 3 Program, and Go!¶
Now, you'll map your OpenQASM gates to signals produced on the instruments using register_gate
and register_gate_section
functions.
Once you've done that, you can compile your experiment and plot the output using the LabOne Q simulator.
rb1_gate_store = GateStore()
# Note: the below may need to be updated to match the
# names of your qubits from your QASM circuit!
rb1_qubit_map = {"q[0]": q0}
# Single qubit gates:
for oq3_qubit, l1q_qubit in rb1_qubit_map.items():
rb1_gate_store.register_gate(
"sx",
oq3_qubit,
drive_pulse(l1q_qubit, label="sx", amplitude_scale=0.5),
signal=l1q_qubit.signals["drive"],
)
rb1_gate_store.register_gate(
"x",
oq3_qubit,
drive_pulse(l1q_qubit, label="x"),
signal=l1q_qubit.signals["drive"],
)
rb1_gate_store.register_gate_section("rz", (oq3_qubit,), rz(l1q_qubit))
rb1_gate_store.register_gate_section(
"measure", (oq3_qubit,), measurement(l1q_qubit)
)
4.1 Compile and execute a single QASM program¶
rb1_exp = exp_from_qasm(
rb1_program_list[2], qubits=rb1_qubit_map, gate_store=rb1_gate_store
)
rb1_compiled_exp = my_session.compile(rb1_exp)
# _ = my_session.run(rb1_compiled_exp)
plot_simulation(
rb1_compiled_exp,
length=1.6e-6,
plot_width=12,
plot_height=3,
signals=[
"/logical_signal_groups/q0/drive_line",
],
)
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)
4.2 Compile and execute a list of QASM programs¶
exp = exp_from_qasm_list(
rb1_program_list,
qubits=rb1_qubit_map,
gate_store=rb1_gate_store,
repetition_time=20e-5,
# batch_execution_mode="rt",
batch_execution_mode="pipeline",
do_reset=False,
count=1,
pipeline_chunk_count=2,
)
compiled_exp = my_session.compile(exp)
_ = my_session.run(compiled_exp)
## KNOWN ISSUE - pulse sheet viewer and output simulation are not available
4.3 Compile and execute a list of QASM programs - including active qubit reset¶
# add reset operation to the gate store
for oq3_qubit, l1q_qubit in rb1_qubit_map.items():
rb1_gate_store.register_gate_section(
"reset", (oq3_qubit,), reset(l1q_qubit, drive_pulse(l1q_qubit, "reset"))
)
exp = exp_from_qasm_list(
rb1_program_list,
qubits=rb1_qubit_map,
gate_store=rb1_gate_store,
repetition_time=20e-5,
# batch_execution_mode="rt",
batch_execution_mode="pipeline",
do_reset=True,
count=1,
pipeline_chunk_count=3,
)
compiled_exp = my_session.compile(exp)
_ = my_session.run(compiled_exp)
5. Execute two Qubit RB¶
Define Gates, Load QASM 3 Program, and Go!¶
Now, you'll map your OpenQASM gates to signals produced on the instruments using register_gate
and register_gate_section
functions.
Once you've done that, you can compile your experiment and plot the output using the LabOne Q simulator.
rb2_gate_store = GateStore()
# Note: the below may need to be updated to match the
# names of your qubits from your QASM circuit!
rb2_qubit_map = {"q[0]": q0, "q[1]": q1}
# Single qubit gates:
for oq3_qubit, l1q_qubit in rb2_qubit_map.items():
rb2_gate_store.register_gate(
"sx",
oq3_qubit,
drive_pulse(l1q_qubit, label="sx", amplitude_scale=0.5),
signal=l1q_qubit.signals["drive"],
)
rb2_gate_store.register_gate(
"x",
oq3_qubit,
drive_pulse(l1q_qubit, label="x"),
signal=l1q_qubit.signals["drive"],
)
rb2_gate_store.register_gate_section("rz", (oq3_qubit,), rz(l1q_qubit))
rb2_gate_store.register_gate_section(
"measure", (oq3_qubit,), measurement(l1q_qubit)
)
# Two qubit gates:
rb2_gate_store.register_gate_section("cx", ("q[0]", "q[1]"), cx(q0, q1))
rb2_gate_store.register_gate_section("cx", ("q[1]", "q[0]"), cx(q1, q0))
5.1 Compile and execute a single QASM program¶
rb2_exp = exp_from_qasm(
rb2_program_list[2], qubits=rb2_qubit_map, gate_store=rb2_gate_store
)
rb2_compiled_exp = my_session.compile(rb2_exp)
_ = my_session.run(rb2_compiled_exp)
plot_simulation(
rb2_compiled_exp,
length=15e-6,
plot_width=12,
plot_height=3,
signals=[
"/logical_signal_groups/q0/flux_line",
"/logical_signal_groups/q1/flux_line",
"/logical_signal_groups/q0/drive_line",
"/logical_signal_groups/q1/drive_line",
],
)
Draw the circuit from above¶
rb2_transpiled_circuits[2].draw()
Look at the pulse sheet¶
show_pulse_sheet(
name="2-qubit RB", compiled_experiment=rb2_compiled_exp, max_events_to_publish=10e4
)
5.2 Compile and execute a list of QASM programs¶
exp = exp_from_qasm_list(
rb2_program_list,
qubits=rb2_qubit_map,
gate_store=rb2_gate_store,
repetition_time=100e-5,
# batch_execution_mode="rt",
batch_execution_mode="pipeline",
do_reset=False,
count=1,
pipeline_chunk_count=3,
)
compiled_exp = my_session.compile(exp)
_ = my_session.run(compiled_exp)
## KNOWN ISSUE - pulse sheet viewer and output simulation are not available
5.3 Compile and execute a list of QASM programs - including active qubit reset¶
# add reset operation to the gate store
for oq3_qubit, l1q_qubit in rb2_qubit_map.items():
rb2_gate_store.register_gate_section(
"reset", (oq3_qubit,), reset(l1q_qubit, drive_pulse(l1q_qubit, "reset"))
)
exp = exp_from_qasm_list(
rb2_program_list,
qubits=rb2_qubit_map,
gate_store=rb2_gate_store,
repetition_time=100e-5,
# batch_execution_mode="rt",
batch_execution_mode="pipeline",
do_reset=True,
count=1,
pipeline_chunk_count=3,
)
compiled_exp = my_session.compile(exp)
_ = my_session.run(compiled_exp)