Download

Download this file as Jupyter notebook: res_spec.ipynb.

Resonator Spectroscopy

Quantum computers use qudits for storing and processing information and rely on readout resonators for measurement. These resonators are coupled to qudits and help determine their state by detecting changes in the electromagnetic field.

Resonator spectroscopy is used to characterize the resonators involved in qudit state readout. It focuses on finding the resonance frequency of the readout resonator, which is designed to be a specific value but often deviates due to manufacturing variations. Knowing this frequency is crucial and is typically the first step in characterizing a qubit chip.

A resonator spectroscopy experiment typically follows these steps:

  1. Initialize the qudit: Prepare the qudit in a known state, usually the ground state.

  2. Sweep a probe signal: Apply a probe signal at varying frequencies through the resonator coupled to the qudit.

  3. Measure the response: Observe how the resonator responds by checking the transmission or reflection of the probe signal.

  4. Identify the resonance frequency: Find the frequency that gives the strongest response, which is the resonance frequency of the readout resonator.

The strongest response is identified by a characteristic peak or dip (depending on the measurement setup) in the transmission or reflection curve as the frequency changes, indicating the resonant frequency of the resonator.

Let’s walk through the steps to perform this experiment on Keysight hardware using the QCS package.

Note

The assets are set up to map a maximum of 4 qudits to a single physical AWG and digitizer channel with an LO frequency of 5.5 GHz. For the purposes of this demonstration, we connect the output of the AWG to the digitizer so we can capture both the control and readout pulses.

First, we’ll load all the necessary packages and a channel mapper, which links up to the hardware channels. If a channel mapper doesn’t exist, we can create a new one and add the hardware references to it. Then we generate a calibration set for the qubits using make_calibration_set(). This file that stores all the important variables and parameters we need to run experiments on our quantum system.

[2]:
import numpy as np
import keysight.qcs as qcs
from keysight.qcs.experiments import make_calibration_set
from keysight.qcs.experiments import ResonatorSpectroscopy, ResonatorSpectroscopy2D
from simulated_experiments.simulated_experiments import (
    SimulatedResSpectroscopyExperiment,
)
[3]:
# Set this to True if channel mapper exists
channel_mapper_exists = False

if channel_mapper_exists:
    mapper = qcs.load("<path/to/channel_mapper.qcs>")
else:
    # generate an empty channel mapper with the correct address
    mapper = qcs.ChannelMapper("<ip address>")

# Generate the calibration_set template
calibration_set = make_calibration_set(qubits=1)

# Set this to True if connected to hardware
run_on_hw = False

Next, we’ll set up our ResonatorSpectroscopy experiment to find the readout resonator’s frequency. The ResonatorSpectroscopy class is actually a subclass of the more general CalibrationExperiment. The CalibrationExperiment class is specifically designed to calibrate quantum operations for individual qubits and store all the parameters in the calibration_set.

In this example, we’re setting up the experiment to calibrate the readout resonator for 1 qubit.

[4]:
# Define the number of qubits
n_qubits = 1
qubits = qcs.Qudits(range(n_qubits))

# Create a resonator spectroscopy experiment
res_spectroscopy = ResonatorSpectroscopy(
    backend=mapper,
    calibration_set=calibration_set,
    qubits=qubits,
    operation="measurement",
)
[5]:
# Draw the program to view the hardware operations
res_spectroscopy.draw()
keysight-logo-svg
Program
Program
Duration 100 ns
Layers 1
Targets 2
Repetitions
Layer #0
Layer #0
Duration 100 ns
readout_pulse 0
RFWaveform on ('readout_pulse', 0)

Parameters
Duration ScalarRef(name=readout_pulse_duration, value=100 ns, dtype=float, unit=s)
Amplitude ScalarRef(name=readout_pulse_amplitudes, value=0.1, dtype=float, unit=none)
Frequency ScalarRef(name=readout_frequencies, value=5.15 GHz, dtype=float, unit=Hz)
Envelope SineEnvelope()
Instantaneous Phase ScalarRef(name=measurement_phase, value=0 rad, dtype=float, unit=rad)
Post-phase ScalarRef(name=measurement_post_phase, value=0 rad, dtype=float, unit=rad)
Delay on ('readout_pulse', 0)

Parameters
Duration Scalar(name=_implicit, value=0 s, dtype=float, unit=s)
readout_acquisition 0
Acquisition on ('readout_acquisition', 0)

Parameters
Duration ScalarRef(name=acquisition_duration, value=100 ns, dtype=float, unit=s)
Integration Filter
RFWaveform

Parameters
Duration ScalarRef(name=acquisition_duration, value=100 ns, dtype=float, unit=s)
Amplitude ScalarRef(name=measurement_integrator_amplitude, value=1, dtype=float, unit=none)
Frequency ScalarRef(name=readout_frequencies, value=5.15 GHz, dtype=float, unit=Hz)
Envelope ConstantEnvelope()
Instantaneous Phase ScalarRef(name=measurement_integrator_phase, value=0 rad, dtype=float, unit=rad)
Post-phase ScalarRef(name=measurement_integrator_post_phase, value=0 rad, dtype=float, unit=rad)
Classifier Classifier(Array(name=references, shape=(1, 2), dtype=complex, unit=none))
Delay on ('readout_acquisition', 0)

Parameters
Duration Scalar(name=_implicit, value=0 s, dtype=float, unit=s)

Now we specify the frequency range to sweep over for the measurement operation. If you already have a specific frequency range in mind, you can pass it as an argument to the ResonatorSpectroscopy class. Alternatively, you can set an arbitrary range by retrieving the corresponding value from the calibration_set and using it as the mid-point for your frequency sweep. The frequency range can always be configured or modified using the configure_repetitions method.

Note

If the “measurement” operation is stored in the calibration_set under a different name, you’ll need to pass that name as an argument (operation=<name>) to the ResonatorSpectroscopy class so it can reference the correct entry in the calibration_set.

[6]:
# Retrieve the readout frequencies stored in the calibration set
current_freq = res_spectroscopy.calibration_set.variables.readout_frequencies[
    list(qubits.labels)
].value

# Set the frequency range for sweeping
start_frequency = current_freq - 200e6
end_frequency = current_freq + 200e6
steps = 9
freq_scan_values = np.linspace(start_frequency, end_frequency, steps)
[7]:
# Configure the repetitions for this experiment
res_spectroscopy.configure_repetitions(
    frequencies=freq_scan_values, n_shots=1, frequency_name="readout_frequencies"
)

We’ll now compile the program down to the waveform level and use the render method to visualize the waveforms to be executed on the hardware. You can adjust the index parameter to view individual waveforms from the sweep.

Note

res_spectroscopy.render() can also be used to display the waveforms. This requires a properly defined channel mapper with all connections configured and passed into the experiment class. For more information, refer to render().

[8]:
# View the waveforms to be executed on the hardware
res_spectroscopy.compiled_program.render(
    channel_subplots=False, lo_frequency=5e9, sweep_index=4, sample_rate=5e9
)

To execute this experiment, we can simply run

[9]:
if run_on_hw:
    res_spectroscopy.execute()
else:
    # load in a previously executed version of this experiment
    res_spectroscopy = qcs.load("ReadoutSpectroscopy.qcs")

We can now plot the output trace waveforms that have been generated by the AWGs. To plot a specific channel, you may pass the argument to the channels parameter of plot_trace method

Note

The plot_iq method is ideal for visualizing the transmission/reflection response from the resonator

[10]:
# Plot the trace waveforms
res_spectroscopy.plot_trace()

Fitting and calibration workflow

Now that we’ve plotted the response, let’s go over how to extract the resonator’s resonance frequency and update the variables in the calibration set.

For this example, we will load an experiment with simulated data:

[11]:
res_spectroscopy = SimulatedResSpectroscopyExperiment(calibration_set, qubits)
[12]:
res_spectroscopy.plot_iq(plot_type="linear")

The fit() method takes the I/Q data and fits it to a complex lorentzian model, as specified by the built-in ComplexLorentzian.

The result of the fit is an EstimateCollection, which contains individual Estimatefor each qubit’s readout resonator that was fitted.

[13]:
ec = res_spectroscopy.fit()

# View the data format of the estimate
print(ec.estimates[0])
Estimate(amplitude=0.000499314294551207, resonance=5149864062.317487, kappa=19867396.825222116, offset_real=-0.02699828492003239, ...)

The fitted and the pre-processed data can be visualized by calling the plot() method:

[14]:
res_spectroscopy.plot()

To compute the new calibration values for the readout resonator, use the get_updated_calibration_values() method. This will calculate the updated calibration values based on the fit results.

[15]:
res_spectroscopy.get_updated_calibration_values()
[15]:
{'readout_frequencies': 5149864062.317487}

Let’s check the current value of the variable in the calibration_set

[16]:
res_spectroscopy.calibration_set.variables.readout_frequencies.value
[16]:
array([5.15e+09])

We update this variable as follows:

[17]:
res_spectroscopy.set_updated_calibration_values()

# Print the value of the variable to confirm the update
res_spectroscopy.calibration_set.variables.readout_frequencies.value
[17]:
array([5.14986406e+09])

Resonator Spectroscopy vs Power

To fully understand and characterize the coupling between a readout resonator and a qubit, it’s important to perform a 2D sweep, where both the input power and the frequency of the readout pulse are varied. The 2D sweep is particularly useful because it reveals how the resonance frequency shifts with different power levels, allowing us to identify any nonlinearities in the system. This helps determine the optimal setting for qubit readout.

Running the ResonatorSpectroscopy2D experiment is similar to the previous example. First, we define the qubits we want to target, then we initialize the ResonatorSpectroscopy2D experiment and draw the program.

Note

The qubits object is defined differently here to demonstrate an alternative approach. Notice how it affects the definition of other parameters in the experiment.

[18]:
# Define the number of qubits and the calibration set
n_qubits = [0, 1]
qubits = qcs.Qudits(n_qubits)
calibration_set = make_calibration_set(qubits=len(qubits))
[19]:
# Create a resonator spectroscopy experiment
res_spectroscopy2D = ResonatorSpectroscopy2D(
    backend=mapper,
    calibration_set=calibration_set,
    qubits=qubits,
    operation="measurement",
)
[20]:
# Draw the program to view the operations to be performed on hardware
res_spectroscopy2D.draw()
keysight-logo-svg
Program
Program
Duration 100 ns
Layers 1
Targets 4
Repetitions
Layer #0
Layer #0
Duration 100 ns
readout_pulse 0
RFWaveform on ('readout_pulse', 0)

Parameters
Duration ArraySlice(name=readout_pulse_duration, shape=(2,), dtype=float, unit=s, value=[100 ns, 100 ns])
Amplitude ArraySlice(name=readout_pulse_amplitudes, shape=(2,), dtype=float, unit=none, value=[0.1, 0.1])
Frequency ArraySlice(name=readout_frequencies, shape=(2,), dtype=float, unit=Hz, value=[5.15 GHz, 5.15 GHz])
Envelope SineEnvelope()
Instantaneous Phase ArraySlice(name=measurement_phase, shape=(2,), dtype=float, unit=rad, value=[0 rad, 0 rad])
Post-phase ArraySlice(name=measurement_post_phase, shape=(2,), dtype=float, unit=rad, value=[0 rad, 0 rad])
Delay on ('readout_pulse', 0)

Parameters
Duration Scalar(name=_implicit, value=0 s, dtype=float, unit=s)
1
RFWaveform on ('readout_pulse', 1)

Parameters
Duration ArraySlice(name=readout_pulse_duration, shape=(2,), dtype=float, unit=s, value=[100 ns, 100 ns])
Amplitude ArraySlice(name=readout_pulse_amplitudes, shape=(2,), dtype=float, unit=none, value=[0.1, 0.1])
Frequency ArraySlice(name=readout_frequencies, shape=(2,), dtype=float, unit=Hz, value=[5.15 GHz, 5.15 GHz])
Envelope SineEnvelope()
Instantaneous Phase ArraySlice(name=measurement_phase, shape=(2,), dtype=float, unit=rad, value=[0 rad, 0 rad])
Post-phase ArraySlice(name=measurement_post_phase, shape=(2,), dtype=float, unit=rad, value=[0 rad, 0 rad])
Delay on ('readout_pulse', 1)

Parameters
Duration Scalar(name=_implicit, value=0 s, dtype=float, unit=s)
readout_acquisition 0
Acquisition on ('readout_acquisition', 0)

Parameters
Duration Array(name=_implicit, shape=(2,), dtype=float, unit=s, value=[100 ns, 100 ns])
Integration Filter
RFWaveform

Parameters
Duration ArraySlice(name=acquisition_duration, shape=(2,), dtype=float, unit=s, value=[100 ns, 100 ns])
Amplitude ArraySlice(name=measurement_integrator_amplitude, shape=(2,), dtype=float, unit=none, value=[1, 1])
Frequency ArraySlice(name=readout_frequencies, shape=(2,), dtype=float, unit=Hz, value=[5.15 GHz, 5.15 GHz])
Envelope ConstantEnvelope()
Instantaneous Phase ArraySlice(name=measurement_integrator_phase, shape=(2,), dtype=float, unit=rad, value=[0 rad, 0 rad])
Post-phase ArraySlice(name=measurement_integrator_post_phase, shape=(2,), dtype=float, unit=rad, value=[0 rad, 0 rad])
Classifier Classifier(ArraySlice(name=references, shape=(2, 2), dtype=complex, unit=none))
Delay on ('readout_acquisition', 0)

Parameters
Duration Scalar(name=_implicit, value=0 s, dtype=float, unit=s)
1
Acquisition on ('readout_acquisition', 1)

Parameters
Duration Array(name=_implicit, shape=(2,), dtype=float, unit=s, value=[100 ns, 100 ns])
Integration Filter
RFWaveform

Parameters
Duration ArraySlice(name=acquisition_duration, shape=(2,), dtype=float, unit=s, value=[100 ns, 100 ns])
Amplitude ArraySlice(name=measurement_integrator_amplitude, shape=(2,), dtype=float, unit=none, value=[1, 1])
Frequency ArraySlice(name=readout_frequencies, shape=(2,), dtype=float, unit=Hz, value=[5.15 GHz, 5.15 GHz])
Envelope ConstantEnvelope()
Instantaneous Phase ArraySlice(name=measurement_integrator_phase, shape=(2,), dtype=float, unit=rad, value=[0 rad, 0 rad])
Post-phase ArraySlice(name=measurement_integrator_post_phase, shape=(2,), dtype=float, unit=rad, value=[0 rad, 0 rad])
Classifier Classifier(ArraySlice(name=references, shape=(2, 2), dtype=complex, unit=none))
Delay on ('readout_acquisition', 1)

Parameters
Duration Scalar(name=_implicit, value=0 s, dtype=float, unit=s)

The experiment remains mostly the same, but now we’re also sweeping the amplitude parameter in the readout pulse. Next, we define the sweep values for both the amplitude and frequency of the readout pulse.

[21]:
# Retrieve the readout frequencies stored in the calibration set for the target qubits
current_freq = res_spectroscopy2D.calibration_set.variables.readout_frequencies[
    [0, 1]
].value

# Set the range for sweeping the readout pulse frequency
start_frequency = current_freq - 200e6
end_frequency = current_freq + 200e6
freq_steps = 9
freq_scan_values = np.linspace(start_frequency, end_frequency, freq_steps)
[22]:
# Set the range for sweeping the readout pulse amplitude (units = V)
start_amplitude = 0.1 if len(qubits) == 1 else [0.1] * len(qubits)
end_amplitude = 1 if len(qubits) == 1 else [1] * len(qubits)
ampl_steps = 10
ampl_scan_values = np.linspace(start_amplitude, end_amplitude, ampl_steps)

Note

The arrays for amplitudes and frequencies can be defined in different ways, as demonstrated above. However, the shape of the arrays must match the number of qubits, or an InconsistentShapes error will occur.

[23]:
# Configure the repetitions for this experiment
res_spectroscopy2D.configure_repetitions(
    frequencies=freq_scan_values,
    frequency_name="readout_frequencies",
    amplitudes=ampl_scan_values,
    amplitude_name="readout_pulse_amplitudes",
    n_shots=1,
)

After configuring the repetitions, we compile the program to the waveform level and use the render method to visualize the waveforms. In this example, the sweep_index is set to the midpoint of the nested sweeps. The right index represents the inner sweep (amplitude), and the left index represents the outer sweep (frequency).

[24]:
# View the waveforms to be executed on the hardware
res_spectroscopy2D.compiled_program.render(
    channel_subplots=False, lo_frequency=5e9, sweep_index=(5, 5), sample_rate=5e9
)

We now proceed to execute the experiment (or load a previous run) and plot the trace waveforms associated with a specific readout resonator. These waveforms show the output that has passed through the resonator.

[25]:
if run_on_hw:
    res_spectroscopy2D.execute()
else:
    # load in a previously executed version of this experiment
    res_spectroscopy2D = qcs.load("ReadoutSpectroscopy2D.qcs")
[26]:
# Plot the trace waveforms
res_spectroscopy2D.plot_trace()

To view the output waveforms as a heatmap, you can use the following command. For more information on other available plot types refer to plot_iq()

[27]:
# Plot the IQ data as a heatmap
res_spectroscopy2D.plot_iq(plot_type="2d")

This concludes the tutorial for running resonator spectroscopy experiments, both 1D and 2D, using the Keysight QCS System. For detailed information on all available arguments and functionalities, please refer to the API documentation for ResonatorSpectroscopy and ResonatorSpectroscopy2D.


Download

Download this file as Jupyter notebook: res_spec.ipynb.

On this page