Two Qubit Randomized Benchmarking in LabOne Q with pyGSTi¶
Python Imports¶
# LabOne Q:
from laboneq.simple import *
# plotting functionality
from laboneq.contrib.example_helpers.plotting.plot_helpers import *
# device setup and descriptor
from laboneq._utils import id_generator
from laboneq.contrib.example_helpers.generate_descriptor import generate_descriptor
# LabOne Q OpenQASM Tools
from laboneq.openqasm3.gate_store import GateStore
# qiskit
from qiskit import qasm3, transpile
# pyGSTi
import pygsti
from pygsti.processors import QubitProcessorSpec as QPS
from pygsti.processors import CliffordCompilationRules as CCR
# additional imports
from math import pi
pyGSTi Experiment Generation¶
You'll start by creating a Clifford RB experiment in a similar fashion as done in the pyGSTi Tutorial here.
Note that mosst circuits that can be generated in pyGSTi and converted to OpenQASM could be adapted to be run in a similar way!
# Define pyGSTi 2 Qubit RB circuit
n_qubits = 2
qubit_labels = ["Q0", "Q1"]
gate_names = ["Gxpi2", "Gxmpi2", "Gypi2", "Gympi2", "Gcphase"]
availability = {"Gcphase": [("Q0", "Q1")]}
# Uncomment below for more qubits or to use a different set of basis gates
# n_qubits = 4
# qubit_labels = ['Q0','Q1','Q2','Q3']
# gate_names = ['Gxpi2', 'Gxmpi2', 'Gypi2', 'Gympi2', 'Gcnot']
# availability = {'Gcphase':[('Q0','Q1'), ('Q1','Q2'), ('Q2','Q3'), ('Q3','Q0')]}
pspec = QPS(n_qubits, gate_names, availability=availability, qubit_labels=qubit_labels)
compilations = {
"absolute": CCR.create_standard(
pspec, "absolute", ("paulis", "1Qcliffords"), verbosity=0
),
"paulieq": CCR.create_standard(
pspec, "paulieq", ("1Qcliffords", "allcnots"), verbosity=0
),
}
depths = [20, 50]
circuits_per_depth = 2
qubits = ["Q0", "Q1"]
randomizeout = True
citerations = 20
You'll then compile the circuits for your Clifford RB experiment and print them.
design = pygsti.protocols.CliffordRBDesign(
pspec,
compilations,
depths,
circuits_per_depth,
qubit_labels=qubits,
randomizeout=randomizeout,
citerations=citerations,
)
circuits_rb = design.all_circuits_needing_data
for circuit in circuits_rb:
print(circuit)
pyGSTi Output to QASM 3 Sanitization¶
Here, you'll define a function to output the above circuit into OpenQASM.
Note: while pyGSTi can output directly into OpenQASM 2, Labone Q imports OpenQASM 3, so we take care of that in this function as well.
def sanitize_pygsti_output(
circuit=circuits_rb,
pygsti_standard_gates="x-sx-rz",
qasm_basis_gates=("id", "sx", "x", "rz", "cx"),
# qasm_basis_gates=("rx","ry","rz","cz"),
):
qasm2_circuit = []
for circuit in circuits_rb:
# pyGSTi standard gates are "u3" and "x-sx-rz""
qasm2_circuit.append(
circuit.convert_to_openqasm(standard_gates_version=pygsti_standard_gates)
.replace("OPENQASM 2.0;", "OPENQASM 3.0;")
.replace('include "qelib1.inc";', 'include "stdgates.inc";')
# Support PyGSTI >= 0.9.12 by removing opaque zero delay
# gates:
.replace("opaque delay(t) q;", "")
.replace("delay(0) q[0];", "")
.replace("delay(0) q[1];", "")
)
qasm3_circuit = []
for entry in qasm2_circuit:
qasm3_circuit.append(
qasm3.Exporter().dumps(
transpile(
qasm3.loads(entry),
basis_gates=qasm_basis_gates,
)
)
)
return qasm3_circuit
You'll now output your OpenQASM 3 circuits as a list and print the first one.
program_list = sanitize_pygsti_output(
qasm_basis_gates=["id", "sx", "x", "rz", "cx"],
)
# Prtint the first circuit in the list
print(program_list[0])
LabOne Q Experiment¶
Setup, Calibration & Configuration¶
You'll define your device setup and calibration below using Qubits to calibrate your devices.
generate_descriptor(
pqsc=["DEV10056"],
shfqc_6=["DEV12108"],
hdawg_8=["DEV8138"],
number_data_qubits=3,
number_flux_lines=3,
multiplex=True,
number_multiplex=3,
save=True,
filename="SeaCucumber_SHF_HD_PQSC",
include_cr_lines=True,
)
device_setup = DeviceSetup.from_yaml(
filepath="./Descriptors/SeaCucumber_SHF_HD_PQSC.yaml",
server_host="ip_address",
server_port="8004",
setup_name="my_setup_name",
)
q0 = Transmon.from_logical_signal_group(
"q0",
lsg=device_setup.logical_signal_groups["q0"],
parameters=TransmonParameters(
resonance_frequency_ge=6.15e9,
resonance_frequency_ef=5.85e9,
drive_lo_frequency=6.1e9,
readout_resonator_frequency=6.4e9,
readout_lo_frequency=6.3e9,
user_defined={
"cross_resonance_frequency": 200e6,
"amplitude_pi": 0.5,
"pulse_length": 50e-9,
"readout_len": 5e-7,
"readout_amp": 0.2,
"reset_length": 200e-9,
},
),
)
q1 = Transmon.from_logical_signal_group(
"q1",
lsg=device_setup.logical_signal_groups["q1"],
parameters=TransmonParameters(
resonance_frequency_ge=6.25e9,
resonance_frequency_ef=5.95e9,
drive_lo_frequency=6.1e9,
readout_resonator_frequency=6.4e9,
readout_lo_frequency=6.3e9,
user_defined={
"cross_resonance_frequency": -200e6,
"amplitude_pi": 0.6,
"pulse_length": 50e-9,
"readout_len": 5e-7,
"readout_amp": 0.2,
"reset_length": 200e-9,
},
),
)
qubits = [q0, q1]
for qubit in qubits:
device_setup.set_calibration(qubit.calibration())
# set calibration of cross resonance signal lines - not currently included in TransmonQubit calibration method
device_setup.logical_signal_groups[qubit.uid].logical_signals[
"drive_line_cr"
].calibration = SignalCalibration(
oscillator=Oscillator(
frequency=qubit.parameters.user_defined["cross_resonance_frequency"],
modulation_type=ModulationType.HARDWARE,
)
)
Transpilation Support (Gate Definitions)¶
You'll now define functions to generate pulses and gates from the OpenQASM program text.
def drive_pulse(qubit: Qubit, label, length=50e-9, amplitude=0.6):
"""Return a drive pulse for the given qubit.
In practice different drive pulses would be specified for each qubit and operation.
"""
return pulse_library.drag(
uid=f"{qubit.uid}_{label}",
length=qubit.parameters.user_defined["pulse_length"],
amplitude=qubit.parameters.user_defined["amplitude_pi"],
)
def drive_pulse_root(qubit: Qubit, label, length=50e-9, amplitude=0.6):
"""Return a root drive pulse for the given qubit.
In practice different drive pulses would be specified for each qubit and operation.
"""
return pulse_library.drag(
uid=f"{qubit.uid}_{label}",
length=qubit.parameters.user_defined["pulse_length"],
amplitude=(qubit.parameters.user_defined["amplitude_pi"]) / 2,
)
def rz(qubit: Qubit):
"""Return a parameterized Rz 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_len"],
amplitude=qubit.parameters.user_defined["readout_amp"],
)
integration_kernel = pulse_library.const(
uid=f"{qubit.uid}_integration_kernel",
length=qubit.parameters.user_defined["readout_len"],
)
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 cx(control: Qubit, target: Qubit):
"""Return a controlled X gate for the specified control and target qubits.
The CX gate function takes no arguments and returns a LabOne Q section that performs
the controllex X gate.
"""
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
Two Qubit RB¶
You're almost ready to run your experiment!
Connect to Session¶
You'll need to start a LabOne Q session. Here, you'll run the session in emulation mode. If you've modified the descriptor to run on your own devices above, you could connect to them here instead.
my_session = Session(device_setup=device_setup)
my_session.connect(do_emulation=True)
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.
gate_store = GateStore()
# Note: the below may need to be updated to match the
# names of your qubits from your QASM circuit!
qubit_map = {"q[0]": q0, "q[1]": q1}
# Single qubit gates:
for oq3_qubit, l1q_qubit in qubit_map.items():
gate_store.register_gate(
"sx",
oq3_qubit,
drive_pulse_root(l1q_qubit, label="sx"),
signal=l1q_qubit.signals["drive"],
)
gate_store.register_gate(
"x",
oq3_qubit,
drive_pulse(l1q_qubit, label="x"),
signal=l1q_qubit.signals["drive"],
)
gate_store.register_gate_section("rz", (oq3_qubit,), rz(l1q_qubit))
gate_store.register_gate_section("measure", (oq3_qubit,), measurement(l1q_qubit))
# Two qubit gates:
gate_store.register_gate_section("cx", ("q[0]", "q[1]"), cx(q0, q1))
gate_store.register_gate_section("cx", ("q[1]", "q[0]"), cx(q1, q0))
# Compile Experiments for desired circuit in the RB circuits list
# Could also compile all in a for loop over list of circuits, if desired
exp = exp_from_qasm(program_list[0], qubits=qubit_map, gate_store=gate_store)
compiled_exp = my_session.compile(exp)
plot_simulation(compiled_exp, length=100e-6)
my_results = my_session.run(compiled_exp)
Draw the circuit from above¶
You can also draw the circuit corresponding to the simulated signals you just produced!
circuit_to_draw_0 = qasm3.loads(program_list[0])
circuit_to_draw_0.draw()
Compile and draw more circuits in the list¶
You can do this for any circuit you've generated in the list.
# Compile Experiments
exp_1 = exp_from_qasm(program_list[1], qubits=qubit_map, gate_store=gate_store)
compiled_exp_1 = my_session.compile(exp_1)
plot_simulation(compiled_exp_1, length=100e-6)
circuit_to_draw_1 = qasm3.loads(program_list[1])
circuit_to_draw_1.draw()