DC-Biased 2D Resonator and Qubit Spectroscopy with SHF Instruments¶
In this notebook we demonstrate resonator and qubit spectroscopy experiments showing how an external DC source can be incoportated using the call
function with the LabOne Q software.
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.
0. General Imports and Definitions¶
0.1 Python Imports¶
from pathlib import Path
# LabOne Q:
from laboneq.simple import *
from laboneq.contrib.example_helpers.plotting.plot_helpers import (
plot_simulation,
plot_results,
)
1. Define the Instrument Setup and Required Experimental Parameters¶
1.1 Create device setup¶
Create the device setup from the descriptor, and apply some convenient mapping to instruments and logical signals.
descriptor = """\
instruments:
SHFQC:
- address: dev12XX0
uid: device_shfqc
connections:
device_shfqc:
- iq_signal: q0/drive_line
ports: [SGCHANNELS/0/OUTPUT]
- iq_signal: q1/drive_line
ports: [SGCHANNELS/1/OUTPUT]
- iq_signal: q2/drive_line
ports: [SGCHANNELS/2/OUTPUT]
- iq_signal: q0/measure_line
ports: [QACHANNELS/0/OUTPUT]
- acquire_signal: q0/acquire_line
ports: [QACHANNELS/0/INPUT]
- iq_signal: q1/measure_line
ports: [QACHANNELS/0/OUTPUT]
- acquire_signal: q1/acquire_line
ports: [QACHANNELS/0/INPUT]
- iq_signal: q2/measure_line
ports: [QACHANNELS/0/OUTPUT]
- acquire_signal: q2/acquire_line
ports: [QACHANNELS/0/INPUT]
"""
# Emulation mode does not create connection to the devices
do_emulation = True
# create device setup
device_setup = DeviceSetup.from_descriptor(
descriptor,
server_host="localhost", # ip address of the LabOne dataserver used to communicate with the instruments
server_port="8004", # port number of the dataserver - default is 8004
setup_name="ZI_QCCS", # setup name
)
shfqa_address = "dev12XX0"
1.2 Qubit Parameters¶
A python dictionary containing all parameters needed to control and readout the qubits - frequencies, pulse lengths, timings
May initially contain only the design parameters and will be updated with measurement results during the tuneup procedure
# a function to define a collection of single qubit control and readout parameters as a python dictionary
def single_qubit_parameters():
return {
"freq": 100e6, # qubit 0 drive frequency in [Hz] - relative to local oscillator for qubit drive upconversion
"ro_freq": 5e6, # 50e6,
"ro_delay": 0, # 15e-9,#100e-9,
"ro_int_delay": 0, # 40-9,
"qb_len_spec": 1e-6,
"qb_len": 700e-9,
"qb_amp_spec": 1.0,
"pi_amp": 1,
"freq_ef": -500e6,
}
# for sake of simplicity, give all qubits the same initial parameters
qubit_parameters = {
k: single_qubit_parameters() for k in device_setup.logical_signal_groups.keys()
}
# up / downconversion settings - to convert between IF and RF frequencies
def single_lo_settings():
return {
# SHFQA LO Frequency
"shfqa_lo": 6.0e9,
# SHFSG LO Frequencies, one center frequency per two channels on SHFQC
"shfsg_lo": 5.0e9,
}
lo_settings = {
k: single_lo_settings() for k in device_setup.logical_signal_groups.keys()
}
1.3 Setup Calibration¶
Generate a calibration object from the qubit control and readout parameters
# function that defines the device settings for qubit and readout parameters
def define_calibration(device_setup, parameters, lo_settings):
# Define LOs
def single_oscillator(id, qubit, lo_type):
oscillator = Oscillator()
oscillator.uid = f"{id}" + f"{qubit}" + "_osc"
oscillator.frequency = lo_settings[qubit][lo_type]
return oscillator
readout_lo_dict = {
k: single_oscillator("readout_lo_", k, "shfqa_lo")
for k in device_setup.logical_signal_groups.keys()
}
drive_lo_dict = {
k: single_oscillator("drive_lo_", k, "shfsg_lo")
for k in device_setup.logical_signal_groups.keys()
}
# the calibration object will later be applied to the device setup
calibration = Calibration()
# qubits q0-q3 are multiplexed on one acquisition line
calibration[
device_setup.logical_signal_groups["q0"].logical_signals["acquire_line"]
] = SignalCalibration(
oscillator=Oscillator(
frequency=parameters["q0"]["ro_freq"],
modulation_type=ModulationType.SOFTWARE,
),
# add an offset between the readout pulse and the start of the data acquisition - to compensate for round-trip time of readout pulse
port_delay=parameters["q0"]["ro_delay"] + parameters["q0"]["ro_int_delay"],
local_oscillator=readout_lo_dict["q0"],
range=-20,
)
for logical_signal_group in device_setup.logical_signal_groups.keys():
# measure line
calibration[
device_setup.logical_signal_groups[logical_signal_group].logical_signals[
"measure_line"
]
] = SignalCalibration(
oscillator=Oscillator(
frequency=parameters[logical_signal_group]["ro_freq"],
modulation_type=ModulationType.SOFTWARE,
),
port_delay=parameters["q0"]["ro_delay"],
local_oscillator=readout_lo_dict["q0"],
range=-20,
)
calibration[
device_setup.logical_signal_groups[logical_signal_group].logical_signals[
"drive_line"
]
] = SignalCalibration(
# each logical signal can have an oscillator associated with it
oscillator=Oscillator(
frequency=parameters[logical_signal_group]["freq"],
modulation_type=ModulationType.HARDWARE,
),
# DANGER! Verify which qubits share the same LOs!
local_oscillator=drive_lo_dict[logical_signal_group],
range=-20,
)
return calibration
1.4 Define user functions¶
import logging
mylogger = logging.getLogger("user_func")
# A user function can be used to sweep the DC bias
# The function may return values, which will be accessible after execution
# The first argument must be the LabOne Q SW session
def set_dc_bias(session, voltage):
mylogger.info(f"Called 'user_func' with params: voltage={voltage:.1f}")
# Insert your code here that sets the voltage
return f"voltage={voltage:.1f}"
2. Apply Calibration Data, Connect to the Instruments¶
2.1 Set Calibration¶
Create qubit control and readout calibration, and apply it to the device setup.
# define Calibration object based on qubit control and readout parameters
calibration = define_calibration(device_setup, qubit_parameters, lo_settings)
# apply calibration to device setup
device_setup.set_calibration(calibration)
2.2 Create and Connect to a QCCS Session¶
Establishes a session and connection to the instruments.
Register the user function to the session.
# 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)
# register user function
session.register_user_function(set_dc_bias)
session.connect(do_emulation=emulate)
3. Experimental Sequences¶
Sequence of experiments for tune-up of a superconducting qubit with DC bias in circuit QED architecture
3.1 Resonator Spectroscopy: CW with DC Bias¶
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 Additional Experimental Parameters¶
Define the frequency scan and the excitation pulse
# frequency range of spectroscopy scan - around expected centre frequency as defined in qubit parameters
start_freq = -500.0e6
stop_freq = 500.0e6
num_points = 5
integration_time = 1e-3
num_averages = 2
# define sweep parameter
def create_readout_freq_sweep(qubit, start_freq, stop_freq, num_points):
return LinearSweepParameter(
uid=f"res_freq_{qubit}",
start=start_freq + qubit_parameters[qubit]["ro_freq"],
stop=stop_freq + qubit_parameters[qubit]["ro_freq"],
count=num_points,
)
# define sweep parameter for DC bias
def create_dc_bias_sweep(start, stop, count):
return LinearSweepParameter(
uid="dc_volt_sweep_param",
start=start,
stop=stop,
count=count,
)
3.1.2 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
# function that defines a resonator spectroscopy experiment, and takes the frequency sweep as a parameter
def res_spectroscopy_CW_bias_sweep(freq_sweep, dc_volt_sweep, exp_settings):
# Create resonator spectroscopy experiment - uses only readout drive and signal acquisition
exp_spec = Experiment(
uid="Resonator Spectroscopy",
signals=[
ExperimentSignal("measure"),
ExperimentSignal("acquire"),
],
)
## define experimental sequence
# near-time loop - vary dc bias
with exp_spec.sweep(uid="dc_volt_sweep", parameter=dc_volt_sweep):
exp_spec.call(set_dc_bias, voltage=dc_volt_sweep)
# real-time loop - average multiple measurements for each frequency - measurement in spectroscopy mode
with exp_spec.acquire_loop_rt(
uid="shots",
count=exp_settings["num_averages"],
acquisition_type=AcquisitionType.SPECTROSCOPY,
):
with exp_spec.sweep(uid="res_freq", parameter=freq_sweep):
# readout pulse and data acquisition
with exp_spec.section(uid="spectroscopy"):
# resonator signal readout
exp_spec.acquire(
signal="acquire",
handle="res_spec",
length=exp_settings["integration_time"],
)
with exp_spec.section(uid="delay", length=1e-6):
# holdoff time after signal acquisition
exp_spec.reserve(signal="measure")
return exp_spec
# function that returns the calibration of the readout line oscillator for the experimental signals
def res_spec_calib(freq_sweep):
exp_calibration = Calibration()
# sets the oscillator of the experimental measure signal
exp_calibration["measure"] = SignalCalibration(
# for spectroscopy, use the hardware oscillator of the QA, and set the sweep parameter as frequency
oscillator=Oscillator(
"readout_osc",
frequency=freq_sweep,
modulation_type=ModulationType.HARDWARE,
)
)
return exp_calibration
# signal maps for the two different qubits - maps the logical signal of the device setup to the experimental signals of the experiment
def res_spec_map(qubit):
signal_map = {
"measure": device_setup.logical_signal_groups[f"{qubit}"].logical_signals[
"measure_line"
],
"acquire": device_setup.logical_signal_groups[f"{qubit}"].logical_signals[
"acquire_line"
],
}
return signal_map
3.1.3 Run and Evaluate Experiment¶
Runs the experiment and evaluates the data returned by the measurement
# define the experiment with the frequency sweep relevant for qubit 0
freq_sweep = create_readout_freq_sweep("q0", start_freq, stop_freq, num_points)
dc_volt_sweep = create_dc_bias_sweep(start=0, stop=0.1, count=10)
exp_settings = {"integration_time": integration_time, "num_averages": num_averages}
exp_spec = res_spectroscopy_CW_bias_sweep(freq_sweep, dc_volt_sweep, exp_settings)
# set signal calibration and signal map for experiment to qubit 0
exp_spec.set_calibration(res_spec_calib(freq_sweep))
exp_spec.set_signal_map(res_spec_map("q0"))
# run the experiment on the open instrument session
compiled_res_spec = session.compile(exp_spec)
res_spec_results = session.run()
# get the measurement data returned by the instruments from the QCCS session
spec_res = res_spec_results.get_data("res_spec")
spec_freq = lo_settings["q0"]["shfqa_lo"] + res_spec_results.get_axis("res_spec")[0]
plot_results(res_spec_results)
3.1.4 Update Calibration¶
# From the fitting, set the qubit operating freq here
opt_freq = None
print(f"Resonant frequency: {opt_freq} GHz")
if not emulate:
# update qubit parameter dictionary with results from data fitting
qubit_parameters["q0"]["ro_freq"] = opt_freq - lo_settings["q0"]["shfqa_lo"]
# update calibration
# apply calibration to device setup
device_setup.set_calibration(
define_calibration(device_setup, qubit_parameters, lo_settings)
)
3.2 Resonator Spectroscopy: Pulsed with DC Bias¶
# frequency range of spectroscopy scan -
# around expected centre frequency as defined in qubit parameters
start_freq = -500e6
stop_freq = 500e6
num_points = 1001
# define number of averages
# used for 2^num_averages, maximum: num_averages = 17
num_averages = 4
# pulse parameters and definitions
envelope_duration = 2.0e-6
sigma = 0.2
flat_duration = 1.0e-6
def create_readout_pulse(
qubit, length=envelope_duration, amplitude=0.9, width=flat_duration, sigma=sigma
):
readout_pulse = pulse_library.gaussian_square(
uid=f"readout_pulse_{qubit}",
length=length,
amplitude=amplitude,
width=width,
sigma=sigma,
)
return readout_pulse
readout_pulse = create_readout_pulse("q0")
# function that defines a resonator spectroscopy experiment, and takes the frequency sweep as a parameter
def res_spectroscopy_pulsed_bias_sweep(
freq_sweep, dc_volt_sweep, num_averages, readout_pulse
):
# Create resonator spectroscopy experiment - uses only readout drive and signal acquisition
exp_spec_pulsed = Experiment(
uid="Puslsed Resonator Spectroscopy Bias Sweep",
signals=[
ExperimentSignal("measure"),
ExperimentSignal("acquire"),
],
)
## define experimental sequence
# near-time loop - vary dc bias
with exp_spec_pulsed.sweep(uid="dc_volt_sweep", parameter=dc_volt_sweep):
exp_spec_pulsed.call(set_dc_bias, voltage=dc_volt_sweep)
# real-time loop - average multiple measurements for each frequency - measurement in spectroscopy mode
with exp_spec_pulsed.acquire_loop_rt(
uid="shots",
count=2**num_averages,
acquisition_type=AcquisitionType.SPECTROSCOPY,
):
with exp_spec_pulsed.sweep(uid="res_freq", parameter=freq_sweep):
# readout pulse and data acquisition
with exp_spec_pulsed.section(uid="spectroscopy"):
# play resonator excitation pulse
exp_spec_pulsed.play(signal="measure", pulse=readout_pulse)
# resonator signal readout
exp_spec_pulsed.acquire(
signal="acquire",
handle="res_spec_pulsed",
length=envelope_duration,
)
with exp_spec_pulsed.section(uid="delay", length=1e-6):
# holdoff time after signal acquisition - minimum 1us required for data processing on UHFQA
exp_spec_pulsed.reserve(signal="measure")
return exp_spec_pulsed
# create freq sweep
freq_sweep = create_readout_freq_sweep("q0", start_freq, stop_freq, num_points)
dc_volt_sweep = create_dc_bias_sweep(start=0, stop=0.1, count=10)
# define the experiment with the frequency sweep relevant for qubit 0
# exp_spec_pulsed = res_spectroscopy_pulsed(freq_sweep, num_averages, readout_pulse)
exp_spec_pulsed = res_spectroscopy_pulsed_bias_sweep(
freq_sweep, dc_volt_sweep, num_averages, readout_pulse
)
# set signal calibration and signal map for experiment to qubit 0
exp_spec_pulsed.set_calibration(res_spec_calib(freq_sweep))
exp_spec_pulsed.set_signal_map(res_spec_map("q0"))
# compile the experiment on the open instrument session
compiled_spec_pulsed = session.compile(exp_spec_pulsed)
Path("Pulse_Sheets").mkdir(parents=True, exist_ok=True)
# generate a pulse sheet to inspect experiment before runtime
show_pulse_sheet("Pulse_Sheets/Pulsed_Spectroscopy", compiled_spec_pulsed)
# run the experiment on the open instrument session
spec_pulsed_results = session.run()
plot_results(spec_pulsed_results)
# get the measurement data returned by the instruments from the QCCS session
spec_res = spec_pulsed_results.get_data("res_spec_pulsed")
# define the frequency axis from the qubit parameters
spec_freq = (
lo_settings["q0"]["shfqa_lo"] + spec_pulsed_results.get_axis("res_spec_pulsed")[0]
)
3.2.1 Update calibration¶
Extract the resonance frequency and update the calibration
if not emulate:
opt_freq = None
qubit_parameters["q0"]["ro_freq"] = opt_freq - lo_settings["q0"]["shfqa_lo"]
device_setup.set_calibration(
define_calibration(device_setup, qubit_parameters, lo_settings)
)
3.3 Pulsed Qubit Spectroscopy with DC Bias¶
Find the resonance frequency of the qubit by looking at the change in resonator transmission when sweeping the frequency of a qubit excitation pulse vs DC bias
3.3.1 Additional Experimental Parameters¶
Define the frequency scan and the pulses used in the experiment
def create_drive_freq_sweep(qubit, start_freq, stop_freq, num_points):
return LinearSweepParameter(
uid=f"drive_freq_{qubit}",
start=start_freq + qubit_parameters[qubit]["freq"],
stop=stop_freq + qubit_parameters[qubit]["freq"],
count=num_points,
)
def create_drive_spec_pulse(qubit):
pulse = pulse_library.const(
uid=f"drive_spec_pulse_{qubit}",
length=qubit_parameters[qubit]["qb_len_spec"],
amplitude=qubit_parameters[qubit]["qb_amp_spec"],
)
return pulse
# frequency range of spectroscopy scan - defined around expected qubit frequency as defined in qubit parameters
qspec_range = 100e6
# how many frequency points to measure
qspec_num = 1001
# define number of averages
# used for 2^num_averages, maximum: num_averages = 17
num_averages = 10
3.3.2 Experiment Definition¶
The frequency sweep of the drive line can now be done in real time (was: near time in older software releases)
# function that returns a qubit spectroscopy experiment- accepts frequency sweep range as parameter
def qubit_spectroscopy_bias_sweep(
freq_sweep, dc_volt_sweep, drive_pulse, readout_pulse
):
# Create qubit spectroscopy Experiment - uses qubit drive, readout drive and data acquisition lines
exp_qspec = Experiment(
uid="Qubit Spectroscopy",
signals=[
ExperimentSignal("drive"),
ExperimentSignal("measure"),
ExperimentSignal("acquire"),
],
)
# near-time loop: sweep dc voltage
with exp_qspec.sweep(uid="dc_volt_sweep", parameter=dc_volt_sweep):
exp_qspec.call(set_dc_bias, voltage=dc_volt_sweep)
# real-time loop with frequency sweep
with exp_qspec.acquire_loop_rt(
uid="freq_shots",
count=2**num_averages,
acquisition_type=AcquisitionType.INTEGRATION,
):
with exp_qspec.sweep(uid="qfreq_sweep", parameter=freq_sweep):
# qubit drive
with exp_qspec.section(uid="qubit_excitation"):
# exp_qspec.play(signal="drive", pulse=drive_pulse)
exp_qspec.play(signal="drive", pulse=drive_pulse)
with exp_qspec.section(
uid="readout_section", play_after="qubit_excitation"
):
# play readout pulse on measure line
exp_qspec.play(signal="measure", pulse=readout_pulse)
# trigger signal data acquisition
exp_qspec.acquire(
signal="acquire",
handle="qb_spec",
kernel=readout_pulse,
)
with exp_qspec.section(uid="delay"):
# relax time after readout - for qubit relaxation to groundstate and signal processing
exp_qspec.delay(signal="measure", time=1e-6)
return exp_qspec
freq_sweep_q0 = create_drive_freq_sweep("q0", -qspec_range, qspec_range, qspec_num)
dc_volt_sweep = create_dc_bias_sweep(start=0, stop=0.1, count=10)
# experiment signal calibration for qubit 0
exp_calibration_q0 = Calibration()
exp_calibration_q0["drive"] = SignalCalibration(
oscillator=Oscillator(
frequency=freq_sweep_q0,
modulation_type=ModulationType.HARDWARE,
),
)
# signal map for qubit 0
def signal_map_default(qubit):
signal_map = {
"drive": device_setup.logical_signal_groups[f"{qubit}"].logical_signals[
"drive_line"
],
"measure": device_setup.logical_signal_groups[f"{qubit}"].logical_signals[
"measure_line"
],
"acquire": device_setup.logical_signal_groups[f"{qubit}"].logical_signals[
"acquire_line"
],
}
return signal_map
3.3.3 Run and Evaluate Experiment for Both Qubits¶
Runs the experiment and evaluates the data returned by the measurement
# define experiment with frequency sweep for qubit 0
drive_pulse = create_drive_spec_pulse("q0")
readout_pulse = create_readout_pulse("q0", length=1000e-9, width=900e-9, sigma=0.2)
device_setup.set_calibration(
define_calibration(device_setup, qubit_parameters, lo_settings)
)
# exp_qspec = qubit_spectroscopy(freq_sweep_q0, drive_pulse, readout_pulse)
exp_qspec = qubit_spectroscopy_bias_sweep(
freq_sweep_q0, dc_volt_sweep, drive_pulse, readout_pulse
)
# apply calibration and signal map for qubit 0
exp_qspec.set_calibration(exp_calibration_q0)
exp_qspec.set_signal_map(signal_map_default("q0"))
# compile the experiment on the open instrument session
compiled_qspec = session.compile(exp_qspec)
Path("Pulse_Sheets").mkdir(parents=True, exist_ok=True)
# generate a pulse sheet to inspect experiment before runtime
show_pulse_sheet("Pulse_Sheets/Qubit_Spectroscopy", compiled_qspec)
plot_simulation(compiled_qspec, 0, 100e-6)
# run the experiment on qubit 0
qspec_results = session.run()
plot_results(qspec_results)
Do fitting and frequency update now if not in emulation mode:
# update qubit parameters
if not emulate:
qubit_operating_freq = None
qubit_parameters["q0"]["freq"] = (
qubit_operating_freq - lo_settings["q0"]["shfsg_lo"]
)
device_setup.set_calibration(
define_calibration(device_setup, qubit_parameters, lo_settings)
)