Download
Download this file as Jupyter notebook: qubit_spec.ipynb.
Qubit spectroscopy
Quantum computers store information in the discrete energy levels of qudits. To process this information, they perform operations that alter the energetic state of these qudits. For example, by exciting the electromagnetic field in the vicinity of a qudit at the frequency corresponding to the difference between the energy levels, quantum computers can drive a transition between two energy levels of the qudit and perform operations such as Pauli-X gates.
When assembling a quantum computer, it is important to learn the energy gap between different levels of the available qudits to be able to perform accurate quantum operations. This is typically done by running a spectroscopy experiment.
In order to learn the energy gap between the ground state and the first excited state of a qudit, a spectroscopy experiment undertakes the following steps:
Initialize the qudit to the ground state.
Apply a control pulse at a frequency \(\omega\) on the target qudit.
Measure the population of the qudit in the excited state.
Repeat the above steps for different values of \(\omega\).
Select the frequency which maximizes the population in the excited state.
The population of the excited state gives a resonance curve, where the energy gap can be deduced from the frequency with the highest population in the excited state.

Note
The above steps can be applied directly to any two levels of a qudit, for example to learn the energy gap between the first and the second excited states in the figure above.
[2]:
import keysight.qcs as qcs
import numpy as np
We start by initializing a qubit and defining an empty channel mapper to create a new
instance of the QubitSpectroscopy
class. We load
a pre-defined CalibrationSet
that contains a
configuration for up to 10 qubits and linkers for the RX
and measurement
gates.
Because the purpose of this experiment is to calibrate a parameter, namely the control
frequency of the qubit, the QubitSpectroscopy
is
a subclass of the more generic
CalibrationExperiment
which requires an
operation name to be passed on instantiation. This name must match the name with which
the associated ParameterizedLinker
is stored in
the calibration set.
[3]:
from keysight.qcs.experiments import QubitSpectroscopy, make_calibration_set
from simulated_experiments.simulated_experiments import SimulatedSpectroscopyExperiment
# set the following to True when connected to hardware
run_on_hw = False
n_qubits = 1
calibration_set = make_calibration_set(n_qubits)
qubits = qcs.Qudits(range(n_qubits))
# generate an empty channel mapper
mapper = qcs.ChannelMapper("127.0.0.1")
# the operation that we are calibrating here is for the RX gate
operation = "x"
# create spectroscopy experiment
spectroscopy_exp = QubitSpectroscopy(
mapper,
calibration_set=calibration_set,
qubits=qubits,
operation=operation,
)
spectroscopy_exp.draw()
Program
Program
|
|||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
Layer #0
Layer #0
|
Layer #1
Layer #1
|
||||||||||||||
|
|
RFWaveform on ('xy_pulse', 0)
Parameters
|
|||||||||||||
|
|
Measure on ('qudits', 0)
Parameters
|
The program consists of the waveform representing our RX
gate on the control
channel that is mapped to the qubit. Note that in this representation, the virtual
qubit
and control
channels appear separate, but after final compilation
through the whole calibration set, they will be merged into the same channel.
To configure the repetitions of this experiment, we sweep the frequency of the RX
waveform. Note that the name of the control frequencies stored
in the calibration set is xy_pulse_frequencies
.
[4]:
# configure the repetitions for this experiment
current_freq = spectroscopy_exp.calibration_set.variables.xy_pulse_frequencies[0].value
start_frequency = current_freq - 200e6
end_frequency = current_freq + 200e6
steps = 9
scan_values = np.linspace(start_frequency, end_frequency, steps)
spectroscopy_exp.configure_repetitions(
frequencies=scan_values, n_shots=1, frequency_name="xy_pulse_frequencies"
)
Compiling this program to the waveform level using the
ParameterizedLinker
s in the calibration set
results in the following program:
[5]:
spectroscopy_exp.draw()
Program
Program
|
|||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
Layer #0
Layer #0
|
Layer #1
Layer #1
|
||||||||||||||
|
|
RFWaveform on ('xy_pulse', 0)
Parameters
|
|||||||||||||
|
|
Measure on ('qudits', 0)
Parameters
|
We again use the render method to visualize this with the
ChannelMapper
.
[6]:
spectroscopy_exp.compiled_program.render(
channel_subplots=False,
lo_frequency=5e9,
sweep_index=5,
sample_rate=5e9,
)
To execute this experiment, we can simply run
[7]:
if run_on_hw:
spectroscopy_exp.execute()
else:
# load in a previously executed version of this experiment
spectroscopy_exp = qcs.load("QubitSpectroscopy.qcs")
For the purposes of this demonstration, we added a second “ancilla” qubit to the Ramsey program and connected the physical output channels for our qubit to the digizer associated with the ancilla to allow us to capture the control pulse.
[8]:
spectroscopy_exp.draw()
Program
Program
|
|||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
Layer #0
Layer #0
|
Layer #1
Layer #1
|
||||||||||||||
|
|
RFWaveform on ('xy_pulse', 0)
Parameters
|
|||||||||||||
|
|
Measure on ('ancilla', 1)
Parameters
|
|||||||||||||
|
|
Measure on ('qudits', 0)
Parameters
|
The ancilla qubit is mapped to the digitizer channel 1
and has a single
acquisition that spans the duration of the control pulse.
[9]:
spectroscopy_exp.plot_trace(channels=qcs.Qudits(1, "ancilla"))
Here we can see the frequencies of the control pulse being updated at each point in the sweep. Note that our local oscillator (LO) frequency was set to 5 GHz for this example.
Fitting and calibration workflow
Let’s walk through how to extract the qubit resonance frequency from the Qubit Spectroscopy experiment and how to update the corresponding variables in our calibration set. For this example, we load in an experiment with simulated data that was imported at the beginning of the file:
[10]:
spectroscopy_exp = SimulatedSpectroscopyExperiment(calibration_set, qubits)
[11]:
spectroscopy_exp.plot_iq(plot_type="linear")
[12]:
spectroscopy_exp.get_iq()
[12]:
(((Qudits(labels=[0], name=qudits, dim=2)))) | |||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
(xy_pulse_frequencies, 4.9 GHz) | (xy_pulse_frequencies, 4.904040404 GHz) | (xy_pulse_frequencies, 4.9080808081 GHz) | (xy_pulse_frequencies, 4.9121212121 GHz) | (xy_pulse_frequencies, 4.9161616162 GHz) | (xy_pulse_frequencies, 4.9202020202 GHz) | (xy_pulse_frequencies, 4.9242424242 GHz) | (xy_pulse_frequencies, 4.9282828283 GHz) | (xy_pulse_frequencies, 4.9323232323 GHz) | (xy_pulse_frequencies, 4.9363636364 GHz) | ... | (xy_pulse_frequencies, 5.2636363636 GHz) | (xy_pulse_frequencies, 5.2676767677 GHz) | (xy_pulse_frequencies, 5.2717171717 GHz) | (xy_pulse_frequencies, 5.2757575758 GHz) | (xy_pulse_frequencies, 5.2797979798 GHz) | (xy_pulse_frequencies, 5.2838383838 GHz) | (xy_pulse_frequencies, 5.2878787879 GHz) | (xy_pulse_frequencies, 5.2919191919 GHz) | (xy_pulse_frequencies, 5.295959596 GHz) | (xy_pulse_frequencies, 5.3 GHz) | |
0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | ... | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | |
0 | -0.026974+0.026972j | -0.026983+0.026972j | -0.027016+0.026971j | -0.026991+0.026970j | -0.027009+0.026970j | -0.026979+0.026969j | -0.026977+0.026968j | -0.026994+0.026967j | -0.026997+0.026966j | -0.027017+0.026965j | ... | -0.026991+0.027027j | -0.026991+0.027027j | -0.027030+0.027026j | -0.027004+0.027025j | -0.026976+0.027025j | -0.027020+0.027024j | -0.026981+0.027024j | -0.026968+0.027024j | -0.027003+0.027023j | -0.026996+0.027023j |
1 rows × 100 columns
The fit()
method takes this 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 Estimate
s for each
qubit that was fitted.
[13]:
ec = spectroscopy_exp.fit()
print(ec)
print(ec.estimates[0])
EstimateCollection(1)
Estimate(amplitude=0.0004928379821974705, resonance=5079874827.71701, kappa=20192703.44965815, offset_real=-0.026998524628909344, ...)
The fitted and the pre-processed data can be visualized by calling the
plot()
method:
[14]:
spectroscopy_exp.plot()
We can then get the calibration value associated to this Spectroscopy Experiment by
calling
get_updated_calibration_values()
.
This method will compute the new values for the calibration variable(s) of this
experiment using the fit results.
[15]:
spectroscopy_exp.get_updated_calibration_values()
[15]:
{'xy_pulse_frequencies': 5079874827.71701}
To check the current state of that variable in the calibration_set to confirm its update, one can do:
[16]:
spectroscopy_exp.calibration_set.variables.xy_pulse_frequencies.value
[16]:
array([5.1e+09])
The update is then done as follows:
[17]:
spectroscopy_exp.set_updated_calibration_values()
spectroscopy_exp.calibration_set.variables.xy_pulse_frequencies.value
[17]:
array([5.07987483e+09])
The last line printing the new updated value, thus confirming that the update has been successful.
Download
Download this file as Jupyter notebook: qubit_spec.ipynb.