Superconducting Qubit Tune-up with LabOne Q for SHF Instruments and many qubits in parallel¶
In this notebook we demonstrate qubit tuneup with LabOne Q for many qubits in parallel, implemented as a sequence of experiments.
Before starting the experiments, we define a set of initial qubit parameters, as might be known from fabrication.
These parameters can then be used to update the baseline calibration used in the experiments and defined as part of the device setup.
# LabOne Q:
from laboneq.simple import *
# plotting functionality
from laboneq.contrib.example_helpers.plotting.plot_helpers import (
plot_simulation,
plot_results,
)
# base qubit parameters and function to translate into Transmon class
from laboneq.contrib.example_helpers.example_notebook_helper import (
create_dummy_transmon,
generate_dummy_transmon_parameters,
)
from laboneq.contrib.example_helpers.generate_descriptor import generate_descriptor
from laboneq.contrib.example_helpers.generate_example_datastore import (
generate_example_datastore,
get_first_named_entry,
)
# for saving results and pulse sheets
import datetime
import matplotlib.pyplot as plt
import numpy as np
# Build an in-memory data store with device setup and qubit parameters for the
# example notebooks
dummy_db = generate_example_datastore(in_memory=True)
# set up datastores for intermediate calibration results and experimental raw data
my_setup_db = DataStore("laboneq_data/setup_db")
my_results_db = DataStore("laboneq_data/results_db")
1. Load the calibrated DeviceSetup and the qubits to use in the experiment¶
number_of_qubits = 12 # up to 24
device_setup = get_first_named_entry(
db=dummy_db, name="24_tuneable_qubit_setup_shfqc_hdawg_pqsc_calibrated"
)
all_transmons = dummy_db.find(
condition=lambda metadata: "tuneable_transmon_" in metadata["name"]
)
my_qubits = [dummy_db.get(transmon) for transmon in all_transmons][:number_of_qubits]
# save device setup and its calibration to datastore
my_setup_db.store(
data=device_setup,
# key="device_setup",
metadata={
"name": "initial_device_setup",
"type": "device_setup_calibrated",
"creation_date": datetime.datetime.now(),
},
)
my_setup_db.store(
data=device_setup.get_calibration(),
# key="device_setup_calibration",
metadata={
"name": "initial_device_setup_calibration",
"type": "device_setup_calibration",
"creation_date": datetime.datetime.now(),
},
)
2.2 Create and Connect to a LabOne Q Session¶
Establishes the connection to the instruments and readies them for experiments
# perform experiments in emulation mode only? - if True, also generate dummy data for fitting
emulate = True
# create and connect to a session
session = Session(device_setup=device_setup)
session.connect(do_emulation=emulate)
3. Qubit Tuneup - Experimental Sequence¶
Sequence of experiments for tuneup from scratch of a superconducting qubit in circuit QED architecture
3.1 Resonator Spectroscopy: CW in parallel over full range of 0.5 - 8.5 GHz¶
Find the resonance frequency of the qubit readout resonator by looking at the transmission or reflection of a probe signal applied through the readout line
3.1.1 Experiment Definition¶
Define the experimental pulse and readout sequence - here without any explicit qubit reference
Explicit qubit reference is then given through different experimental calibration and signal maps
# define sweep parameter
def create_freq_sweep(id, start_freq, stop_freq, num_points):
return LinearSweepParameter(
uid=f"frequency_sweep_{id}",
start=start_freq,
stop=stop_freq,
count=num_points,
)
def resonator_spectroscopy_parallel_CW(
qubits, outer_sweeps, inner_sweeps, integration_time=10e-3, num_averages=1
):
# Create resonator spectroscopy experiment - uses only readout drive and signal acquisition
exp_spec = Experiment(
uid="Resonator Spectroscopy",
signals=[
signal
for signal_list in [
[
ExperimentSignal(
f"measure_{qubit.uid}", map_to=qubit.signals["measure"]
),
ExperimentSignal(
f"acquire_{qubit.uid}", map_to=qubit.signals["acquire"]
),
]
for qubit in qubits
]
for signal in signal_list
],
)
## define experimental sequence
# loop - average multiple measurements for each frequency - measurement in spectroscopy mode
with exp_spec.sweep(uid="resonator_frequency_outer", parameter=outer_sweeps):
with exp_spec.acquire_loop_rt(
uid="shots",
count=num_averages,
acquisition_type=AcquisitionType.SPECTROSCOPY,
):
with exp_spec.sweep(
uid="resonator_frequency_inner", parameter=inner_sweeps
):
for qubit in qubits:
# readout pulse and data acquisition
with exp_spec.section(uid=f"resonator_spectroscopy_{qubit.uid}"):
# resonator signal readout
exp_spec.acquire(
signal=f"acquire_{qubit.uid}",
handle=f"resonator_spectroscopy_{qubit.uid}",
length=integration_time,
)
with exp_spec.section(uid=f"delay_{qubit.uid}", length=1e-6):
# holdoff time after signal acquisition
exp_spec.reserve(signal=f"measure_{qubit.uid}")
cal = Calibration()
for it, qubit in enumerate(qubits):
cal[f"measure_{qubit.uid}"] = SignalCalibration(
oscillator=Oscillator(
frequency=inner_sweeps[it], modulation_type=ModulationType.HARDWARE
),
local_oscillator=Oscillator(frequency=outer_sweeps[it]),
range=-10,
)
cal[f"acquire_{qubit.uid}"] = SignalCalibration(
local_oscillator=Oscillator(frequency=outer_sweeps[it]),
range=-5,
port_delay=250e-9,
)
exp_spec.set_calibration(cal)
return exp_spec
3.1.2 Run and Evaluate Experiment¶
Runs the experiment and evaluates the data returned by the measurement
# create experiment with outer, near-time sweep from 1-8 GHz in 1 GHz steps and a sweep over 1001 points within each 1GHz band - in parallel for all 4 QA channels
cw_spectroscopy_exp = resonator_spectroscopy_parallel_CW(
my_qubits[::6],
[create_freq_sweep(f"outer_{qubit.uid}", 1e9, 8e9, 8) for qubit in my_qubits[::6]],
[
create_freq_sweep(f"inner_{qubit.uid}", -500e6, 500e6, 1001)
for qubit in my_qubits[::6]
],
)
compiled_cw_spectroscopy_exp = session.compile(cw_spectroscopy_exp)
cw_spectroscopy_results = session.run(compiled_cw_spectroscopy_exp)
# save results to database
my_results_db.store(
data=cw_spectroscopy_results,
key=f"cw_spectroscopy_results_{datetime.datetime.now()}",
metadata={"creation_date": datetime.datetime.now()},
)
# access and plot results of one 8GHz sweep
full_data = cw_spectroscopy_results.get_data("resonator_spectroscopy_q0")
outer = cw_spectroscopy_results.get_axis("resonator_spectroscopy_q0")[0][0]
inner = cw_spectroscopy_results.get_axis("resonator_spectroscopy_q0")[1][0]
full_sweep = np.array(
[item for item_list in [out + inner for out in outer] for item in item_list]
)
plt.plot(
full_sweep,
np.array(
[item for item_list in [data for data in full_data] for item in item_list]
),
)
# Do analysis of data here
3.1.3 Update Calibration and save to database¶
# update qubit parameters from analysis
# for qubit in my_qubits:
# qubit.parameters.readout_resonator_frequency = my_new_frequency
# device_setup.set_calibration(qubit.calibration())
# store new device setup including calibration in database
my_setup_db.store(
data=device_setup,
# use same key to overwrite previous device setup or different to store individual instances
key="device_setup_CW",
metadata={"creation_date": datetime.datetime.now()},
)
3.2 Pulsed Qubit Spectroscopy: in parallel over 100MHz range for each qubit¶
Find the resonance frequency of the qubit by looking at the change in resonator transmission when sweeping the frequency of a qubit excitation pulse
3.2.1 Experiment Definition¶
The frequency sweep of the drive line can now be done in real time (was: near time in older software releases)
def qubit_spectroscopy_pulse(qubit):
return pulse_library.const(
uid=f"spectroscopy_pulse_{qubit.uid}",
length=qubit.parameters.user_defined["readout_length"],
amplitude=0.8,
# can_compress=True,
)
def readout_pulse(qubit):
return pulse_library.const(
uid=f"readout_pulse_{qubit.uid}",
length=qubit.parameters.user_defined["readout_length"],
amplitude=qubit.parameters.user_defined["readout_amplitude"],
)
def integration_kernel(qubit):
return pulse_library.const(
uid=f"integration_kernel_{qubit.uid}",
length=qubit.parameters.user_defined["readout_length"],
amplitude=1,
)
# function that returns a qubit spectroscopy experiment- accepts frequency sweep range as parameter
def qubit_spectroscopy_parallel(
qubits, qspec_range=100e6, qspec_num=1001, num_averages=2**10
):
# Create qubit spectroscopy Experiment - uses qubit drive, readout drive and data acquisition lines
exp_qspec = Experiment(
uid="Qubit Spectroscopy",
signals=[
signal
for signal_list in [
[
ExperimentSignal(
f"drive_{qubit.uid}", map_to=qubit.signals["drive"]
),
ExperimentSignal(
f"measure_{qubit.uid}", map_to=qubit.signals["measure"]
),
ExperimentSignal(
f"acquire_{qubit.uid}", map_to=qubit.signals["acquire"]
),
]
for qubit in qubits
]
for signal in signal_list
],
)
# List of frequency sweeps for all qubits
qubit_frequency_sweeps = [
LinearSweepParameter(
uid=f"{qubit.uid}_spectroscopy_sweep",
start=qubit.parameters.drive_frequency_ge - qspec_range / 2,
stop=qubit.parameters.drive_frequency_ge + qspec_range / 2,
count=qspec_num,
)
for qubit in my_qubits
]
# inner loop - real-time averaging - QA in integration mode
with exp_qspec.acquire_loop_rt(
uid="freq_shots",
count=num_averages,
acquisition_type=AcquisitionType.INTEGRATION,
):
with exp_qspec.sweep(
uid="qubit_frequency_sweep", parameter=qubit_frequency_sweeps
):
for qubit in qubits:
# qubit drive
with exp_qspec.section(uid=f"{qubit.uid}_excitation"):
exp_qspec.play(
signal=f"drive_{qubit.uid}",
pulse=qubit_spectroscopy_pulse(qubit),
)
# measurement
with exp_qspec.section(
uid=f"readout_{qubit.uid}", play_after=f"{qubit.uid}_excitation"
):
exp_qspec.measure(
measure_signal=f"measure_{qubit.uid}",
measure_pulse=readout_pulse(qubit),
handle=f"{qubit.uid}_spectroscopy",
acquire_signal=f"acquire_{qubit.uid}",
integration_kernel=integration_kernel(qubit),
reset_delay=qubit.parameters.user_defined["reset_delay_length"],
)
cal = Calibration()
for it, qubit in enumerate(qubits):
cal[f"drive_{qubit.uid}"] = SignalCalibration(
oscillator=Oscillator(
frequency=qubit_frequency_sweeps[it],
modulation_type=ModulationType.HARDWARE,
)
)
exp_qspec.set_calibration(cal)
return exp_qspec
3.2.2 Run and Evaluate Experiment for all Qubits in parallel¶
Runs the experiment and evaluates the data returned by the measurement
qubit_spectroscopy_exp = qubit_spectroscopy_parallel(my_qubits)
compiled_qubit_spectroscopy_exp = session.compile(qubit_spectroscopy_exp)
qubit_spectroscopy_results = session.run(compiled_qubit_spectroscopy_exp)
# save results to database
my_results_db.store(
data=qubit_spectroscopy_results,
key=f"qubit_spectroscopy_results_{datetime.datetime.now()}",
metadata={"creation_date": datetime.datetime.now()},
)
# access and plot results of one drive frequency sweep
index = 0
data_qubit = my_qubits[index]
qubit_data = qubit_spectroscopy_results.get_data(f"{data_qubit.uid}_spectroscopy")
qubit_freq = (
qubit_spectroscopy_results.get_axis(f"{data_qubit.uid}_spectroscopy")[0][0]
+ data_qubit.parameters.drive_lo_frequency
)
plt.plot(qubit_freq, qubit_data)
# Do analysis of data here
3.2.3 Update Calibration and save to database¶
# update qubit parameters from analysis - here: qubit resonance frquency
# for qubit in my_qubits:
# qubit.parameters.resonance_frequency_ge = my_new_frequency
# device_setup.set_calibration(qubit.calibration())
# store new device setup including calibration in database
my_setup_db.store(
data=device_setup,
# use same key to overwrite previous device setup or different to store individual instances
key="device_setup_Qubit_Spectroscopy",
metadata={"creation_date": datetime.datetime.now()},
)
3.3 Amplitude Rabi Experiment - in parallel¶
Sweep the pulse amplitude of a qubit drive pulse to determine the ideal amplitudes for specific qubit rotation angles
3.3.1 Experiment Definition¶
def drive_ge_rabi(qubit):
return pulse_library.drag(
uid=f"drag_pulse_{qubit.uid}",
length=qubit.parameters.user_defined["pulse_length"],
sigma=0.4,
beta=0.2,
amplitude=1,
)
# function that returns an amplitude Rabi experiment
def amplitude_rabi_parallel(qubits, amplitude_sweep, num_averages=2**10):
exp_rabi = Experiment(
uid="Qubit Spectroscopy",
signals=[
signal
for signal_list in [
[
ExperimentSignal(
f"drive_{qubit.uid}", map_to=qubit.signals["drive"]
),
ExperimentSignal(
f"measure_{qubit.uid}", map_to=qubit.signals["measure"]
),
ExperimentSignal(
f"acquire_{qubit.uid}", map_to=qubit.signals["acquire"]
),
]
for qubit in qubits
]
for signal in signal_list
],
)
## define Rabi experiment pulse sequence
# outer loop - real-time, cyclic averaging
with exp_rabi.acquire_loop_rt(
uid="rabi_shots",
count=num_averages,
averaging_mode=AveragingMode.CYCLIC,
acquisition_type=AcquisitionType.INTEGRATION,
):
# inner loop - real time sweep of Rabi ampitudes
with exp_rabi.sweep(uid="rabi_sweep", parameter=amplitude_sweep):
for qubit in qubits:
# qubit drive
with exp_rabi.section(
uid=f"{qubit.uid}_excitation", alignment=SectionAlignment.RIGHT
):
exp_rabi.play(
signal=f"drive_{qubit.uid}",
pulse=drive_ge_rabi(qubit),
amplitude=amplitude_sweep,
)
# measurement
with exp_rabi.section(
uid=f"readout_{qubit.uid}", play_after=f"{qubit.uid}_excitation"
):
exp_rabi.measure(
measure_signal=f"measure_{qubit.uid}",
measure_pulse=readout_pulse(qubit),
handle=f"{qubit.uid}_rabi",
acquire_signal=f"acquire_{qubit.uid}",
integration_kernel=integration_kernel(qubit),
reset_delay=qubit.parameters.user_defined["reset_delay_length"],
)
return exp_rabi
3.3.2 Execute experiment and analyze results¶
rabi_exp = amplitude_rabi_parallel(
my_qubits,
LinearSweepParameter(uid="amplitude_sweep", start=0, stop=1, count=201),
)
compiled_rabi_exp = session.compile(rabi_exp)
rabi_results = session.run(compiled_rabi_exp)
# save results to database
my_results_db.store(
data=rabi_results,
key=f"rabi_results_{datetime.datetime.now()}",
metadata={"creation_date": datetime.datetime.now()},
)
# access and plot results of one drive frequency sweep
index = 0
data_qubit = my_qubits[index]
qubit_data = rabi_results.get_data(f"{data_qubit.uid}_rabi")
qubit_amp = rabi_results.get_axis(f"{data_qubit.uid}_rabi")[0]
plt.plot(qubit_amp, qubit_data)
# plot all results
plot_results(rabi_results)
# Do analysis of data here
3.3.3 Update Calibration and save to database¶
# update qubit parameters from analysis - here: qubit pulse amplitude
# for qubit in my_qubits:
# qubit.parameters.user_defined["amplitude_pi"] = my_amplitude
# store qubits including their parameters in database
for qubit in my_qubits:
my_setup_db.store(
data=qubit,
# use same key to overwrite previous device setup or different to store individual instances
key=f"qubit_{qubit.uid}",
metadata={"creation_date": datetime.datetime.now()},
)
3.4 Ramsey Experiment - in parallel¶
The Ramsey experiment is different from the experiments above as the length of the drive section changes. Using a right-aligned sweep section and the automatic repetition time makes sure that the experiment is run as efficiently as possible on the Zurich Instruments hardware.
3.4.1 Experiment Definition¶
# define ramsey drive pulse - use calibration from Rabi experiment
def drive_ge_pi_half(qubit):
return pulse_library.drag(
uid=f"ramsey_drive_{qubit.uid}",
length=qubit.parameters.user_defined["pulse_length"],
sigma=0.4,
beta=0.2,
amplitude=qubit.parameters.user_defined["amplitude_pi"] / 2,
)
# function that returns an amplitude Rabi experiment
def ramsey_parallel(qubits, delay_sweep, num_averages=2**10):
exp_ramsey = Experiment(
uid="Qubit Spectroscopy",
signals=[
signal
for signal_list in [
[
ExperimentSignal(
f"drive_{qubit.uid}", map_to=qubit.signals["drive"]
),
ExperimentSignal(
f"measure_{qubit.uid}", map_to=qubit.signals["measure"]
),
ExperimentSignal(
f"acquire_{qubit.uid}", map_to=qubit.signals["acquire"]
),
]
for qubit in qubits
]
for signal in signal_list
],
)
## define Ramsey experiment pulse sequence
# outer loop - real-time, cyclic averaging
with exp_ramsey.acquire_loop_rt(
uid="ramsey_shots",
count=num_averages,
averaging_mode=AveragingMode.CYCLIC,
acquisition_type=AcquisitionType.INTEGRATION,
repetition_mode=RepetitionMode.AUTO,
):
# inner loop - real time sweep of Ramsey time delays
with exp_ramsey.sweep(
uid="ramsey_sweep", parameter=delay_sweep, alignment=SectionAlignment.RIGHT
):
for qubit in qubits:
# play qubit excitation pulse - pulse amplitude is swept
ramsey_pulse = drive_ge_pi_half(qubit)
with exp_ramsey.section(
uid=f"{qubit.uid}_excitation", alignment=SectionAlignment.RIGHT
):
exp_ramsey.play(signal=f"drive_{qubit.uid}", pulse=ramsey_pulse)
exp_ramsey.delay(signal=f"drive_{qubit.uid}", time=delay_sweep)
exp_ramsey.play(signal=f"drive_{qubit.uid}", pulse=ramsey_pulse)
# readout pulse and data acquisition
# measurement
with exp_ramsey.section(
uid=f"readout_{qubit.uid}", play_after=f"{qubit.uid}_excitation"
):
exp_ramsey.measure(
measure_signal=f"measure_{qubit.uid}",
measure_pulse=readout_pulse(qubit),
handle=f"{qubit.uid}_ramsey",
acquire_signal=f"acquire_{qubit.uid}",
integration_kernel=integration_kernel(qubit),
reset_delay=qubit.parameters.user_defined["reset_delay_length"],
)
return exp_ramsey
3.4.2 Execute experiment¶
ramsey_exp = ramsey_parallel(
my_qubits,
LinearSweepParameter(
uid="ramsey_delay_sweep",
start=0,
stop=15e-6,
count=201,
),
)
compiled_ramsey_exp = session.compile(ramsey_exp)
ramsey_results = session.run(compiled_ramsey_exp)
# save results to database
my_results_db.store(
data=ramsey_results,
key="ramsey_results",
metadata={"creation_date": datetime.datetime.now()},
)
# access and plot results of one drive frequency sweep
index = 0
data_qubit = my_qubits[index]
qubit_data = ramsey_results.get_data(f"{data_qubit.uid}_ramsey")
qubit_delay = ramsey_results.get_axis(f"{data_qubit.uid}_ramsey")[0]
plt.plot(qubit_delay, qubit_data)
# plot all results
plot_results(ramsey_results)
# Do analysis of data here
3.4.3 Update Qubit parameters and save to database¶
# update qubit parameters from analysis - here: qubit dephasing time
# for qubit in my_qubits:
# qubit.parameters.user_defined["t2_time"] = my_t2
# store qubits including their parameters in database
for qubit in my_qubits:
my_setup_db.store(
data=qubit,
# use same key to overwrite previous device setup or different to store individual instances
key=f"qubit_{qubit.uid}",
metadata={"creation_date": datetime.datetime.now()},
)
3.5 T1 Experiment - in parallel¶
3.5.1 Experiment Definition¶
# define drive pulse - use calibration from Rabi experiment
def drive_ge_pi(qubit):
return pulse_library.drag(
uid=f"drive_{qubit.uid}",
length=qubit.parameters.user_defined["pulse_length"],
sigma=0.4,
beta=0.2,
amplitude=qubit.parameters.user_defined["amplitude_pi"],
)
# function that returns an amplitude Rabi experiment
def t1_parallel(qubits, delay_sweep, num_averages=2**10):
exp_t1 = Experiment(
uid="Qubit Spectroscopy",
signals=[
signal
for signal_list in [
[
ExperimentSignal(
f"drive_{qubit.uid}", map_to=qubit.signals["drive"]
),
ExperimentSignal(
f"measure_{qubit.uid}", map_to=qubit.signals["measure"]
),
ExperimentSignal(
f"acquire_{qubit.uid}", map_to=qubit.signals["acquire"]
),
]
for qubit in qubits
]
for signal in signal_list
],
)
## define Ramsey experiment pulse sequence
# outer loop - real-time, cyclic averaging
with exp_t1.acquire_loop_rt(
uid="t1_shots",
count=num_averages,
averaging_mode=AveragingMode.CYCLIC,
acquisition_type=AcquisitionType.INTEGRATION,
repetition_mode=RepetitionMode.AUTO,
):
# inner loop - real time sweep of Ramsey time delays
with exp_t1.sweep(
uid="t1_delay_sweep",
parameter=delay_sweep,
alignment=SectionAlignment.RIGHT,
):
for qubit in qubits:
# play qubit excitation pulse - pulse amplitude is swept
with exp_t1.section(
uid=f"{qubit.uid}_excitation", alignment=SectionAlignment.RIGHT
):
exp_t1.play(signal=f"drive_{qubit.uid}", pulse=drive_ge_pi(qubit))
exp_t1.delay(signal=f"drive_{qubit.uid}", time=delay_sweep)
# readout pulse and data acquisition
# measurement
with exp_t1.section(
uid=f"readout_{qubit.uid}", play_after=f"{qubit.uid}_excitation"
):
exp_t1.measure(
measure_signal=f"measure_{qubit.uid}",
measure_pulse=readout_pulse(qubit),
handle=f"{qubit.uid}_t1",
acquire_signal=f"acquire_{qubit.uid}",
integration_kernel=integration_kernel(qubit),
reset_delay=qubit.parameters.user_defined["reset_delay_length"],
)
return exp_t1
3.5.2 Execute experiment¶
t1_exp = t1_parallel(
my_qubits,
LinearSweepParameter(uid="t1_delay_sweep", start=0, stop=50e-6, count=201),
)
compiled_t1_exp = session.compile(t1_exp)
t1_results = session.run(compiled_t1_exp)
# save results to database
my_results_db.store(
data=t1_results,
key=f"t1_results_{datetime.datetime.now()}",
metadata={"creation_date": datetime.datetime.now()},
)
# access and plot results of one drive frequency sweep
index = 0
data_qubit = my_qubits[index]
qubit_data = t1_results.get_data(f"{data_qubit.uid}_t1")
qubit_delay = t1_results.get_axis(f"{data_qubit.uid}_t1")[0]
plt.plot(qubit_delay, qubit_data)
# plot all results
plot_results(t1_results)
# Do analysis of data here
3.5.3 Update Qubit parameters and save to database¶
# update qubit parameters from analysis - here: qubit relaxation time
# for qubit in my_qubits:
# qubit.parameters.user_defined["t1_time"] = my_t1
# store qubits including their parameters in database
for qubit in my_qubits:
my_setup_db.store(
data=qubit,
# use same key to overwrite previous device setup or different to store individual instances
key=f"qubit_{qubit.uid}",
metadata={"creation_date": datetime.datetime.now()},
)