Active Qubit Reset with SHF instruments¶
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 either a SHFQC instrument for this notebook or a combination of SHFSG and SHFQA 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 instrument.
0. General Imports and Definitions¶
0.1 Python Imports¶
import matplotlib.pyplot as plt
import numpy as np
# all LabOne Q functionality
from laboneq.simple import *
# Helpers:
from laboneq.contrib.example_helpers.feedback_helper import (
complex_freq_phase,
exp_raw,
exp_integration,
exp_discrimination,
)
from laboneq.contrib.example_helpers.generate_example_datastore import (
generate_example_datastore,
get_first_named_entry,
)
# 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¶
# Choose your setup - local feedback on a standalone SHFQC or ...
# feedback_type="local"
# my_setup = get_first_named_entry(db=dummy_db, name="6_qubit_setup_shfqc_calibrated")
# ... global feedback on a combination of SHFSG and SHFQA, connected through a PQSC
feedback_type = "global"
my_setup = get_first_named_entry(
db=dummy_db, name="12_qubit_setup_shfsg_shfqa_shfqc_hdawg_pqsc_calibrated"
)
my_qubit = get_first_named_entry(db=dummy_db, name="fixed_transmon_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/acquire_line"
] = my_setup.get_calibration()["/logical_signal_groups/q0/acquire_line"]
active_reset_calibration["/logical_signal_groups/q0/acquire_line"].oscillator = None
# 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 additionall 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 Define measurement pulse waveforms to simulate measurement of |0> and |1> qubit states¶
# measure pulse parameters
pulse_len = my_qubit.parameters.user_defined["readout_length"]
pulse_phase = np.pi / 4
# sampling rate of SHFQC
sampling_rate = 2.0e9
pulse_freq = 0.0
measure0_gen2 = pulse_library.sampled_pulse_complex(
complex_freq_phase(
sampling_rate,
pulse_len,
pulse_freq,
my_qubit.parameters.user_defined["readout_amplitude"],
0,
)
)
measure1_gen2 = pulse_library.sampled_pulse_complex(
complex_freq_phase(
sampling_rate,
pulse_len,
pulse_freq,
my_qubit.parameters.user_defined["readout_amplitude"],
pulse_phase,
)
)
2.2 Determine optimal integration weights based on raw readout results of two measurement pulses¶
## Raw |0>
r = my_session.run(exp_raw(measure_pulse=measure0_gen2, q0=q0, pulse_len=pulse_len))
raw0 = r.acquired_results["raw"].data
## Raw |1>
r = my_session.run(exp_raw(measure_pulse=measure1_gen2, q0=q0, pulse_len=pulse_len))
raw1 = r.acquired_results["raw"].data
## optimal integration kernel
samples_kernel = np.conj(raw1 - raw0)
# plt.figure()
# plt.plot(samples_kernel.real, samples_kernel.imag)
plt.figure()
plt.plot(samples_kernel.real)
plt.plot(samples_kernel.imag)
2.3 Determine optimal rotation of integration weights and discrimination threshold¶
do_rotation = True
my_exp = exp_integration(
measure0=measure0_gen2,
measure1=measure1_gen2,
q0=q0,
q1=q1,
samples_kernel=samples_kernel,
)
r = my_session.run(my_exp)
res0 = r.acquired_results["data0"].data
res1 = r.acquired_results["data1"].data
connect_vector = np.median(res1) - np.median(res0)
if do_rotation:
rotation_angle = -np.angle(connect_vector)
else:
rotation_angle = 0
res0_rot = res0 * np.exp(1j * rotation_angle)
res1_rot = res1 * np.exp(1j * rotation_angle)
my_threshold = (np.median(res0_rot.real) + np.median(res1_rot.real)) / 2
if do_rotation:
plt.scatter(res0.real, res0.imag, c="k", alpha=0.1)
plt.scatter(res1.real, res1.imag, c="g", alpha=0.1)
plt.scatter(res0_rot.real, res0_rot.imag, c="b")
plt.scatter(res1_rot.real, res1_rot.imag, c="r")
plt.plot(
[my_threshold, my_threshold],
[
min([*res0_rot.imag, *res1_rot.imag, *res0.imag, *res1.imag]),
max([*res0_rot.imag, *res1_rot.imag, *res0.imag, *res1.imag]),
],
"r",
)
if do_rotation:
print(f"Using threshold = {my_threshold:e} and rotation angle: {rotation_angle:e}")
else:
print(f"Using threshold={my_threshold:e}")
## define properly rotated integration kernel and set state discrimination threshold in device setup calibration
my_integration_weights = pulse_library.sampled_pulse_complex(
samples_kernel * np.exp(1j * rotation_angle)
)
q0["acquire_line"].calibration.threshold = my_threshold
my_other_exp = exp_integration(
measure0=measure0_gen2,
measure1=measure1_gen2,
q0=q0,
q1=q1,
samples_kernel=samples_kernel,
rotation_angle=rotation_angle,
)
r = my_session.run(my_other_exp)
res0 = r.acquired_results["data0"].data
res1 = r.acquired_results["data1"].data
connect_vector = np.median(res1) - np.median(res0)
threshold_rot = (np.median(res0.real) + np.median(res1.real)) / 2
plt.scatter(res0.real, res0.imag, c="b")
plt.scatter(res1.real, res1.imag, c="r")
plt.plot(
[threshold_rot, threshold_rot],
[min([*res0.imag, *res1.imag]), max([*res0.imag, *res1.imag])],
"r",
)
print(f"Using threshold={threshold_rot:e}")
2.4.2 Check correct state discrimination when including rotation of integration weights¶
r = my_session.run(
exp_discrimination(
measure0=measure0_gen2,
measure1=measure1_gen2,
q0=q0,
q1=q1,
samples_kernel=samples_kernel,
threshold=my_threshold,
rotation_angle=rotation_angle,
)
)
s0 = r.acquired_results["data0"].data
s1 = r.acquired_results["data1"].data
plt.plot(s0.real, ".b")
plt.plot(s1.real, ".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.
3.0 Define Pulses¶
x90 = pulse_library.drag(
uid=f"x90_q{id}",
length=my_qubit.parameters.user_defined["pulse_length"],
amplitude=0.33,
sigma=0.3,
beta=0.4,
)
x180 = pulse_library.drag(
uid=f"x180_q{id}",
length=my_qubit.parameters.user_defined["pulse_length"],
amplitude=0.66,
sigma=0.3,
beta=0.4,
)
3.1 Define Experiment¶
def create_feedback_experiment(
feedback_pattern="1010111",
num_average=2,
## delay parameter between state readout and reset playback, needs to be minimal 120ns for local feedback and 400ns for global feedback
acquire_delay=120e-9,
# parameters to simulate the qubit state discrimination
measure_pulse0=measure0_gen2,
measure_pulse1=measure1_gen2,
integration_weights=my_integration_weights,
acquisition_type=AcquisitionType.DISCRIMINATION,
# parameters that determine the type of pulse sequence to be played
x90=x90,
x180=x180,
pattern_delay=1e-6,
):
exp = Experiment(
signals=[
ExperimentSignal("drive", map_to=q0["drive_line"]),
ExperimentSignal("measure0", map_to=q0["measure_line"]),
ExperimentSignal("measure1", map_to=q1["measure_line"]),
ExperimentSignal("acquire", map_to=q0["acquire_line"]),
]
)
with exp.acquire_loop_rt(
count=num_average,
averaging_mode=AveragingMode.CYCLIC,
acquisition_type=acquisition_type,
):
# iterate over the letters of the given pattern
for id, letter in enumerate(feedback_pattern):
# placeholder for experiments / pulse sequences on the qubit
with exp.section(uid=f"drive_{id}"):
exp.delay(signal="drive", time=5 * x90.length)
# qubit state readout
with exp.section(uid=f"measure_{id}", play_after=f"drive_{id}"):
# emulate qubit state by playing different measurement pulses based on pattern
if letter == "0":
exp.play(signal="measure0", pulse=measure_pulse0)
else:
exp.play(signal="measure1", pulse=measure_pulse1)
# acquire result, assign to handle
exp.acquire(
signal="acquire",
handle="qubit_state",
kernel=integration_weights,
)
# delay after state discrimination and before reset pulse playback
exp.delay(signal="acquire", time=acquire_delay)
# 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 exp.match(
uid=f"feedback_{id}",
handle="qubit_state",
play_after=f"measure_{id}",
):
# measurement result 0 - ground state
with exp.case(state=0):
# could be "pass". i.e. doing nothing. Here we instead play a x90 pulse, purely for visual reasons
exp.play(signal="drive", pulse=x90)
# measurement result 0 - excited state
with exp.case(state=1):
# play x180 pulse
exp.play(signal="drive", pulse=x180)
# introduce a delay between repetitions of the pattern, only for visual distinction
with exp.section():
exp.delay(signal="drive", time=pattern_delay)
return exp
3.3 Run experiment¶
my_feedback_exp = create_feedback_experiment(
feedback_pattern="1010111",
acquire_delay=150e-9,
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"])