Active Qudit Multistate Reset with SHF instruments¶
In this notebook, you will learn how to execute active reset of a multistate qudit, i.e. active feedback based on real-time measurement of the qudit 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 readoud acquisition line. We emulate the different qubit states by up to four different readout measurement pulses, differing by a phase. To demonstrate real-time feedback, we first calibrate the state discrimintation unit for the four measurement pulses with which we choose to emulate the qudit response. Then we use this calibration to play an arbitrary simulated pattern of qudit 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
# LabOne Q:
from laboneq.simple import *
from laboneq.dsl.experiment.builtins import *
# utilities for multistate discrimination:
import zhinst.utils.shfqa as shfqa_utils
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
q2 = my_setup.logical_signal_groups["q2"].logical_signals
q3 = my_setup.logical_signal_groups["q3"].logical_signals
1.2 Adapt setup calibration¶
In this notebook we are using pulses played from an additional set of measure lines to emulate the qudit being in the excited state. In this case we want to have the same instrument settings for the four 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/q2/measure_line"
] = my_setup.get_calibration()["/logical_signal_groups/q0/measure_line"]
active_reset_calibration[
"/logical_signal_groups/q3/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 measuring traces of the four qubit states (ground state, e, f, and h) and computing integration kernels using the toolkit routines. We simulate different qubit responses by playing pulses with different phases and amplitudes on the readout line. We have to make sure that the traces are a multiple of 16 samples long.
2.1 Obtain traces¶
def create_calibration_experiment(
measure_pulse_time,
measure_pulse_phase,
measure_pulse_amplitude,
measure_signal,
acquire_time,
acquire_signal,
):
@experiment(signals=["measure", "acquire"])
def exp():
map_signal("measure", measure_signal)
map_signal("acquire", acquire_signal)
with acquire_loop_rt(count=1024, acquisition_type=AcquisitionType.RAW):
play(
signal="measure",
pulse=pulse_library.const(length=measure_pulse_time),
phase=measure_pulse_phase,
amplitude=measure_pulse_amplitude,
)
acquire(signal="acquire", handle="raw", length=acquire_time)
delay(signal="measure", time=2e-6)
return exp()
num_states = 4
phase_delta = np.pi / 2.5
measure_pulse_time = 400e-9
acquire_time = 300e-9
experiments = [
create_calibration_experiment(
measure_pulse_time=measure_pulse_time,
measure_pulse_phase=phase_delta * i,
measure_pulse_amplitude=0.2 + 0.2 * i,
measure_signal=l["measure_line"],
acquire_time=acquire_time,
acquire_signal=q0["acquire_line"],
)
for i, l in enumerate([q0, q1, q2, q3])
]
traces = []
for exp in experiments:
res = my_session.run(exp)
trace = res.get_data("raw")
traces.append(trace[: (len(trace) // 16) * 16])
2.2 Compute kernels¶
We only need the number of states minus 1 kernels, the additional kernel is computed on the device.
qdit_settings = shfqa_utils.multistate.QuditSettings(ref_traces=traces)
weights = qdit_settings.weights[: num_states - 1]
kernels = [pulse_library.sampled_pulse_complex(w.vector) for w in weights]
# Plot traces
fig, ax = plt.subplots(2, 2)
ax[0][0].plot(traces[0].real, alpha=0.5)
ax[0][0].plot(traces[0].imag, alpha=0.5)
ax[0][1].plot(traces[1].real, alpha=0.5)
ax[0][1].plot(traces[1].imag, alpha=0.5)
ax[1][0].plot(traces[2].real, alpha=0.5)
ax[1][0].plot(traces[2].imag, alpha=0.5)
ax[1][1].plot(traces[3].real, alpha=0.5)
ax[1][1].plot(traces[3].imag, alpha=0.5)
# Plot kernels
fig, ax = plt.subplots(2, 2)
ax[0][0].plot(kernels[0].samples.real, alpha=0.5)
ax[0][0].plot(kernels[0].samples.imag, alpha=0.5)
ax[0][1].plot(kernels[1].samples.real, alpha=0.5)
ax[0][1].plot(kernels[1].samples.imag, alpha=0.5)
ax[1][0].plot(kernels[2].samples.real, alpha=0.5)
ax[1][0].plot(kernels[2].samples.imag, alpha=0.5)
for i, k in enumerate(kernels):
ax[1][1].plot(k.samples.real, k.samples.imag, ["ro-", "gx-", "b+-"][i], alpha=0.2)
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 one of four pulses.
3.1 Define Experiment¶
def create_feedback_experiment(feedback_pattern=".-.. *==== --.-", kernels=kernels):
# Pattern example: ".-.. *==== --.-"
# with:
# . ground state
# - first excited state
# * second excited state
# = third excited state
# (empty space) break between symbols
num_average = 4
space_delay = 400e-9
pattern_delay = 1000e-9
acquire_delay = 120e-9
measure_pulse_time = 400e-9
acquire_time = 300e-9
length = my_qubit.parameters.user_defined["pulse_length"]
xg = pulse_library.drag(
uid=f"xg", length=length, amplitude=0.2, sigma=0.3, beta=0.4
)
xe = pulse_library.drag(
uid=f"xe", length=length, amplitude=0.4, sigma=0.3, beta=0.4
)
xf = pulse_library.drag(
uid=f"xf", length=length, amplitude=0.6, sigma=0.3, beta=0.4
)
xh = pulse_library.drag(
uid=f"xh", length=length, amplitude=0.8, sigma=0.3, beta=0.4
)
measure_pulse = pulse_library.const(length=measure_pulse_time)
@experiment(
signals=["drive", "measure", "measure_e", "measure_f", "measure_h", "acquire"]
)
def exp():
map_signal("drive", q0["drive_line"])
map_signal("measure", q0["measure_line"])
map_signal("measure_e", q1["measure_line"])
map_signal("measure_f", q2["measure_line"])
map_signal("measure_h", q3["measure_line"])
map_signal("acquire", q0["acquire_line"])
with acquire_loop_rt(
count=num_average,
acquisition_type=AcquisitionType.DISCRIMINATION,
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
with section(uid=f"measure_{id}", play_after=last):
# emulate qubit state by playing different measurement pulses based on pattern
idx = {".": 0, "-": 1, "*": 2, "=": 3}[letter]
line = ["measure", "measure_e", "measure_f", "measure_h"][idx]
phase = phase_delta * idx
amp = 0.2 + 0.2 * idx
play(signal=line, pulse=measure_pulse, phase=phase, amplitude=amp)
acquire(signal="acquire", handle="qubit_state", kernel=kernels)
# delay after state discrimination and before reset pulse playback
if acquire_delay > 0:
with section(uid=f"acquire_delay_{id}", play_after=f"measure_{id}"):
# delay after state discrimination and before reset pulse playback
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=xg)
with case(state=1):
play(signal="drive", pulse=xe)
if len(kernels) > 1:
with case(state=2):
play(signal="drive", pulse=xf)
if len(kernels) > 2:
with case(state=3):
play(signal="drive", pulse=xh)
last = f"feedback_{id}"
# introduce a delay between repetitions of the pattern, only 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()
# 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)
# verify results (will return False in emulation mode)
my_data = my_results.get_data("qubit_state")
print(my_data)
all(my_data.real.astype(np.int16) == [0, 1, 0, 0, 2, 3, 3, 3, 3, 1, 1, 0, 1])
## 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"])