Active Qubit Reset with UHFQA and HDAWG¶
In this notebook, we demonstrate how to execute active qubit reset, i.e. active feedback based on real-time measurement of the qubit state. We require a combination of UHFQA and HDAWG instruments, connected via a PQSC.
This demonstration runs without real qubits, assuming a loopback on the readout drive line directly into the reaoud acquisition line. We emulate the different qubit states by two different readout measurement pulses, differing by a phase. To demonstrate real-time feedback, we first calibrate the state discrimintation unit for the two measurement pulsese we choose to emulate the qubit response. The we use this calibration to play an arbitrary simulated pattern of qubit states and demonstrate the real-time feedback capabilities of the instruments.
0. General Imports and Definitions¶
0.1 Python Imports¶
import matplotlib.pyplot as plt
import numpy as np
from laboneq.analysis import calculate_integration_kernels_thresholds
# Helpers:
from laboneq.contrib.example_helpers.feedback_helper import (
create_calibration_experiment,
create_discrimination_experiment,
state_emulation_pulse,
)
from laboneq.contrib.example_helpers.generate_example_datastore import (
generate_example_datastore,
get_first_named_entry,
)
from laboneq.dsl.experiment.builtins import *
# all LabOne Q functionality
from laboneq.simple import *
# Build an in-memory data store with device setup and qubit parameters for the
# example notebooks
dummy_db = generate_example_datastore(in_memory=True)
use_emulation = True
1. Device setup and calibration¶
1.1 Load a calibrated Device Setup and qubit object¶
feedback_type = "global"
my_setup = get_first_named_entry(
db=dummy_db, name="2_fixed_qubit_setup_hdawg_uhfqa_pqsc_calibrated"
)
my_qubit = my_setup.qubits[0]
q0 = my_setup.logical_signal_groups["q0"].logical_signals
q1 = my_setup.logical_signal_groups["q1"].logical_signals
1.2 Adapt setup calibration¶
In this notebook we are using a pulse played from a second measure line to emulate the qubit being in the excited state. In this case we want to have the same instrument settings for the two used measurement lines. Additionally, for the method of readout weight calibration demonstrated in this notebook, the acquire line should not be modulated, as the calculated readout weights already contain the software modulation by construction.
active_reset_calibration = Calibration()
active_reset_calibration["/logical_signal_groups/q1/measure_line"] = (
my_setup.get_calibration()["/logical_signal_groups/q0/measure_line"]
)
active_reset_calibration["/logical_signal_groups/q0/measure_line"] = (
my_setup.get_calibration()["/logical_signal_groups/q0/measure_line"]
)
active_reset_calibration["/logical_signal_groups/q0/acquire_line"] = (
my_setup.get_calibration()["/logical_signal_groups/q0/acquire_line"]
)
active_reset_calibration["/logical_signal_groups/q0/acquire_line"].oscillator = None
active_reset_calibration["/logical_signal_groups/q0/measure_line"].range = 1.5
active_reset_calibration["/logical_signal_groups/q1/measure_line"].range = 1.5
active_reset_calibration["/logical_signal_groups/q0/acquire_line"].range = 1.5
# print(active_reset_calibration)
my_setup.set_calibration(active_reset_calibration)
# print(my_setup.get_calibration())
# create and connect to a LabOne Q session
my_session = Session(device_setup=my_setup)
my_session.connect(do_emulation=use_emulation)
2. Calibration of state discrimination¶
We determine the optimal integration weights by subtracting and conjugating the raw response corresponding to the two different qubit states. We then additionally rotate these integration weights to result in maximum separation of the resulting IQ valuebs on the real axis and set the threshold to the setup calibration.
2.1 Obtain traces¶
num_states = 2
experiments = [
create_calibration_experiment(
state_emulation_pulse=state_emulation_pulse(),
qubit_state=i,
measure_signal=l["measure_line"],
acquire_signal=q0["acquire_line"],
)
for i, l in enumerate([q0, q1])
]
traces = []
for exp in experiments:
res = my_session.run(exp)
trace = res.get_data("raw")
traces.append(trace[: (len(trace) // 16) * 16])
# In emulation mode, the 'acquired' traces are all identical. Consequently, the computation of the optimal
# discrimination weights will fail. Instead we 'patch' the traces with an artificial phase.
if use_emulation:
for i in range(num_states):
phase = np.exp(2j * np.pi * i / num_states)
traces[i] *= phase
2.2 Compute kernels¶
Using calculate_integration_kernels_thresholds
, we get number of states minus 1 optimal integration kernels together with the optimal thresholds for state discrimination.
# Calculate and plot kernel
kernels, thresholds = calculate_integration_kernels_thresholds(traces)
plt.plot(kernels[0].samples.real, kernels[0].samples.imag, "ro-", alpha=0.2)
# set the thresholds in the acquire line calibration
threshold_calibration = Calibration()
threshold_calibration["/logical_signal_groups/q0/acquire_line"] = SignalCalibration(
threshold=thresholds
)
my_setup.set_calibration(threshold_calibration)
# print(my_setup.get_calibration())
2.3 Verify state discrimination¶
my_exp = create_discrimination_experiment(
measure_lines=[q0["measure_line"], q1["measure_line"]],
acquire_line=q0["acquire_line"],
kernels=kernels,
state_emulation_pulse=state_emulation_pulse,
thresholds=thresholds,
)
discrimination_results = my_session.run(my_exp)
s0 = discrimination_results.get_data("data_0").real
s1 = discrimination_results.get_data("data_1").real
plt.plot(s0, ".b")
plt.plot(s1, ".r")
3. Feedback experiment¶
Here, we create a real-time feedback demonstration that plays back a user defined sequence of "qubit states", i.e., a sequences of different measurement pulses emulating different qubit states. The measured qubit state after state discrimination is used in a real-time feedback section to play back either of two pulses: x90 for the qubit in its ground state and x180 for the qubit in the excited state.
# define pulses
pulse_length = my_qubit.parameters.user_defined["pulse_length"]
x90_eg = pulse_library.drag(
uid="x90_eg",
length=pulse_length,
amplitude=0.2,
)
x180_eg = pulse_library.drag(
uid="x180_eg",
length=pulse_length,
amplitude=0.6,
)
3.1 Define Experiment¶
def create_feedback_experiment(
feedback_pattern="1010 1111 0011",
kernels=kernels,
num_average=4,
space_delay=400e-9,
pattern_delay=1000e-9,
acquire_delay=150e-9,
state_emulation_pulse=state_emulation_pulse,
reset_pulses=(x90_eg, x180_eg),
acquisition_type=AcquisitionType.DISCRIMINATION,
):
# Pattern example: "1010 1111 0011"
# with:
# 0 - ground state
# 1 - first excited state
# (empty space) break between symbols
@experiment(
signals=[
"drive",
"measure_g",
"measure_e",
"acquire",
]
)
def exp():
map_signal("drive", q0["drive_line"])
map_signal("measure_g", q0["measure_line"])
map_signal("measure_e", q1["measure_line"])
map_signal("acquire", q0["acquire_line"])
measure_emulation_pulse = state_emulation_pulse()
with acquire_loop_rt(
count=num_average,
acquisition_type=acquisition_type,
uid="shots",
):
# iterate over the letters of the given pattern
last = None
id = 0
for id, letter in enumerate(feedback_pattern):
#
if letter == " ":
with section(uid=f"delay_{id}", play_after=last):
delay(signal="drive", time=space_delay)
last = f"delay_{id}"
continue
# emulate qubit state by playing different measurement pulses based on pattern
with section(uid=f"measure_{id}", play_after=last):
idx = {"0": 0, "1": 1}[letter]
line = ["measure_g", "measure_e"][idx]
play(
signal=line,
pulse=measure_emulation_pulse.pulse,
phase=measure_emulation_pulse.pulse_phase(qubit_state=idx),
amplitude=measure_emulation_pulse.pulse_amplitude(
qubit_state=idx
),
)
acquire(signal="acquire", handle="qubit_state", kernel=kernels)
last = f"measure_{id}"
# delay after state discrimination and before reset pulse playback
if acquire_delay > 0:
with section(uid=f"acquire_delay_{id}", play_after=last):
reserve(signal="acquire")
delay(signal="drive", time=acquire_delay)
last = f"acquire_delay_{id}"
# real-time feedback, fetching the measurement data identified by handle from the QA unit specified in the descriptor
# determines automatically if local (SHFQC only) of global (through PQSC) feedback path is to be used
with match(uid=f"feedback_{id}", handle="qubit_state", play_after=last):
with case(state=0):
play(signal="drive", pulse=reset_pulses[0])
with case(state=1):
play(signal="drive", pulse=reset_pulses[1])
last = f"feedback_{id}"
# introduce a delay between repetitions of the pattern, for visual distinction
with section(uid=f"pattern_delay{id}", play_after=last):
delay(signal="drive", time=pattern_delay)
last = f"pattern_delay{id}"
return exp()
3.2 Run experiment¶
my_feedback_exp = create_feedback_experiment(
feedback_pattern="1010 1111 0011",
acquisition_type=AcquisitionType.INTEGRATION,
)
# compile experiment
my_compiled_exp = my_session.compile(my_feedback_exp)
# run experiment and get the results
my_results = my_session.run(my_compiled_exp)
# when executed in integration mode, IQ data of each state readout is still available
my_data = my_results.get_data("qubit_state")
my_data
## Look at th pulse sheet - feedback is characterised by two simultaneous sections
# show_pulse_sheet("feedback_experiment", my_compiled_exp)
## have a look at the sequencer code for the QA unit, making the measurements
print(my_compiled_exp.src[0]["text"])
## have a look at the sequencer code for the SG unit, playing the feedback pulses
print(my_compiled_exp.src[1]["text"])