VQE with LabOne Q and Qiskit¶
A demonstration of first steps towards performing VQE with LabOne Q.
Qiskit is used as a convenient way to prepare the parameterized ansatz circuit, which is then converted to OpenQASM which is imported and executed by LabOne Q.
Imports¶
# Python standard library and numpy:
import numpy as np
# LabOne Q:
from laboneq.simple import *
from laboneq._utils import id_generator
from laboneq.contrib.example_helpers.plotting.plot_helpers import plot_simulation
from laboneq.pulse_sheet_viewer.pulse_sheet_viewer import show_pulse_sheet
from laboneq.openqasm3.gate_store import GateStore
from laboneq.contrib.example_helpers.generate_example_datastore import (
generate_example_datastore,
)
# Qiskit:
from qiskit import qasm3 as q3
from qiskit import QuantumCircuit
from qiskit.circuit.library import TwoLocal
from qiskit.quantum_info import SparsePauliOp
from qiskit.circuit.classicalregister import ClassicalRegister
# Build an in-memory data store with device setup and qubit parameters for the
# example notebooks
setup_db = generate_example_datastore(path="", filename=":memory:")
Device setup & physical qubit parameters (LabOne Q)¶
Here we initialize the QCCS control system and set the calibration properties based on a number of qubits we define
# load a calibrated device setup from the dummy database
all_data = setup_db.find(
metadata={"name": "12_qubit_setup_shfsg_shfqa_shfqc_hdawg_pqsc_calibrated"}
)
device_setup = setup_db.get(next(all_data))
all_transmons = setup_db.find(
condition=lambda metadata: "tuneable_transmon_" in metadata["name"]
)
[q0, q1] = [setup_db.get(next(all_transmons)) for _ in range(2)]
use_emulation = True
Define the physical gate operations (LabOne Q)¶
Define the gate ($R_y$, $R_z$, $CX$) and measurement operations for each qubit.
A gate here is a function that returns an LabOne Q section that performs the specified operation. In the case of rotation gates, the gate accepts a parameter that specifies the angle to rotate through.
The implementations here are just for demonstration purposes. Real implementations would depend on the physical system being controlled and might varying according to the individual qubit being controlled.
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 ry(qubit: Qubit):
"""Return a parameterized Ry 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 ry_gate(angle):
"""Ry(theta).
Theta is in radians - pulse amplitude is adjusted according to the chosen angle
"""
gate = Section(uid=id_generator(f"p_{qubit.uid}_ry_{int(180 * angle / np.pi)}"))
amplitude = qubit.parameters.user_defined["amplitude_pi"] * angle / np.pi
gate.play(
signal=qubit.signals["drive"],
pulse=drive_pulse(
qubit,
"ry",
length=qubit.parameters.user_defined["pulse_length"],
amplitude=amplitude,
),
phase=np.pi / 2,
)
return gate
return ry_gate
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 / np.pi)}"))
gate.play(
signal=qubit.signals["drive"],
pulse=None,
increment_oscillator_phase=angle,
)
return gate
return rz_gate
q0.parameters.user_defined
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"],
)
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 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
)
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
)
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
)
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
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
Define a gate store (LabOne Q)¶
The gate store defines the mapping between logical operations (i.e. those that appear in OpenQASM statements) and the physical operations (i.e. functions that define LabOne Q sections to play) above.
gate_store = GateStore()
# Note: the below may need to be updated to match the
# names of your qubits from your QASM circuit!
qubit_map = {"_qubit0": q0, "_qubit1": q1}
# Single qubit gates:
for oq3_qubit, l1q_qubit in qubit_map.items():
gate_store.register_gate_section("ry", (oq3_qubit,), ry(l1q_qubit))
gate_store.register_gate_section("rz", (oq3_qubit,), rz(l1q_qubit))
gate_store.register_gate_section("measure", (oq3_qubit,), measurement(l1q_qubit))
gate_store.register_gate_section(
"reset", (oq3_qubit,), reset(l1q_qubit, drive_pulse(l1q_qubit, "reset"))
)
# Two qubit gates:
gate_store.register_gate_section("cx", ("_qubit0", "_qubit1"), cx(q0, q1))
Generate an OpenQASM3 program for the VQE ansatz (Qiskit)¶
Define a parameterized circuit to optimize over.¶
Here we use the TwoLocal
circuit creator to define a two qubit circuit with four layers (i.e. three reps
). Each layer performs an Ry
and an Rz
gate on each qubit, four a total of 16 parameters (four layers times four gates). A CX
gate is applied to both qubits in between each layer.
ansatz = TwoLocal(2, ["ry", "rz"], "cx", "full", reps=3, insert_barriers=True)
ansatz.decompose().draw()
Now we can assign values to the parameters to generate a concrete circuit. We also add active qubit reset to initialise all qubits and a measurement of each qubit at the end.
As a demonstration, we assign the values [0.1, 0.15, ..., 0.85]
to the 16 parameters:
parameters = np.linspace(0.1, 0.85, 16)
print("Parameters:", parameters)
circuit = ansatz.assign_parameters(parameters)
# add active reset of both qubits at the start
reset_circuit = QuantumCircuit(circuit.qubits)
reset_circuit.reset(circuit.qubits)
circuit = reset_circuit & circuit
# add measurements for both qubits at the end
circuit.add_bits(ClassicalRegister(size=2))
circuit.measure(0, 0)
circuit.measure(1, 1)
circuit.decompose().draw()
Generate and compile an experiment from the OpenQASM3 program (LabOne Q)¶
Generate a LabOne Q experiment from the OpenQASM program and compile it.
program = q3.dumps(circuit.decompose())
print(program)
# Import qasm into LabOne Q experiment
exp = exp_from_qasm(program, qubits=qubit_map, gate_store=gate_store)
my_session = Session(device_setup=device_setup)
my_session.connect(do_emulation=True)
compiled_exp = my_session.compile(exp)
Plot the experiment¶
plot_simulation(compiled_exp, plot_width=10, plot_height=3)
Examine the pulse sheet¶
show_pulse_sheet("QASM_program", compiled_exp)
# show_pulse_sheet("QASM_program", compiled_exp, interactive=True)
Run the experiment and look at results¶
my_results = my_session.run(compiled_exp)
my_results.acquired_results