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:

  1. Initialize the qudit to the ground state.

  2. Apply a control pulse at a frequency \(\omega\) on the target qudit.

  3. Measure the population of the qudit in the excited state.

  4. Repeat the above steps for different values of \(\omega\).

  5. 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.

../_images/spectroscopy_experiment.png

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("<ip_address>")

# 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()
keysight-logo-svg
Program
Program
Duration undefined
Layers 2
Targets 2
Repetitions
Layer #0
Layer #0
Duration 30 ns
Layer #1
Layer #1
Duration undefined
xy_pulse 0
RFWaveform on ('xy_pulse', 0)

Parameters
Duration ScalarRef(name=xy_pulse_durations, value=30 ns, dtype=float, unit=s)
Amplitude ScalarRef(name=x180_pulse_amplitudes, value=0.5, dtype=float, unit=none)
Frequency ScalarRef(name=xy_pulse_frequencies, value=5.1 GHz, dtype=float, unit=Hz)
Envelope GaussianEnvelope(2.0)
Instantaneous Phase ScalarRef(name=x_phase, value=0 rad, dtype=float, unit=rad)
Post-phase ScalarRef(name=x_post_phase, value=0 rad, dtype=float, unit=rad)
qudits 0
Measure on ('qudits', 0)

Parameters
Dim 2

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 ParameterizedLinkers in the calibration set results in the following program:

[5]:
spectroscopy_exp.draw()
keysight-logo-svg
Program
Program
Duration undefined
Layers 2
Targets 2
Repetitions Repeat with 1 repetitions
Sweep with 9 repetitions
Associations
xy_pulse_frequencies Array(name=_implicit, shape=(9,), dtype=float, unit=none, value=[4.9 GHz, 4.95 GHz, 5 GHz, 5.05 GHz, 5.1 GHz, 5.15 GHz, 5.2 GHz, 5.25 GHz, 5.3 GHz])
Layer #0
Layer #0
Duration 30 ns
Layer #1
Layer #1
Duration undefined
xy_pulse 0
RFWaveform on ('xy_pulse', 0)

Parameters
Duration ScalarRef(name=xy_pulse_durations, value=30 ns, dtype=float, unit=s)
Amplitude ScalarRef(name=x180_pulse_amplitudes, value=0.5, dtype=float, unit=none)
Frequency ScalarRef(name=xy_pulse_frequencies, value=5.1 GHz, dtype=float, unit=Hz)
Envelope GaussianEnvelope(2.0)
Instantaneous Phase ScalarRef(name=x_phase, value=0 rad, dtype=float, unit=rad)
Post-phase ScalarRef(name=x_post_phase, value=0 rad, dtype=float, unit=rad)
qudits 0
Measure on ('qudits', 0)

Parameters
Dim 2

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()
keysight-logo-svg
Program
Program
Duration undefined
Layers 2
Targets 3
Repetitions Sweep with 9 repetitions
Associations
xy_pulse_frequencies Array(name=_implicit, shape=(9,), dtype=float, unit=none, value=[4.9 GHz, 4.95 GHz, 5 GHz, 5.05 GHz, 5.1 GHz, 5.15 GHz, 5.2 GHz, 5.25 GHz, 5.3 GHz])
Repeat with 1 repetitions
Layer #0
Layer #0
Duration 30 ns
Layer #1
Layer #1
Duration undefined
xy_pulse 0
RFWaveform on ('xy_pulse', 0)

Parameters
Duration ScalarRef(name=xy_pulse_durations, value=30 ns, dtype=float, unit=s)
Amplitude ScalarRef(name=_implicit, value=0.5, dtype=float, unit=none)
Frequency ScalarRef(name=xy_pulse_frequencies, value=5.1 GHz, dtype=float, unit=Hz)
Envelope GaussianEnvelope(2.0)
Instantaneous Phase ScalarRef(name=rx_phase, value=0 rad, dtype=float, unit=rad)
Post-phase ScalarRef(name=rx_post_phase, value=0 rad, dtype=float, unit=rad)
qudits 0
Measure on ('qudits', 0)

Parameters
Dim 2
ancilla 1
Measure on ('ancilla', 1)

Parameters
Dim 2

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 Estimates for each qubit that was fitted.

[13]:
ec = spectroscopy_exp.fit()
print(ec)
print(ec.estimates[0])
EstimateCollection(1)
Estimate(amplitude=0.0004928379788716721, resonance=5079874827.724896, kappa=20192703.172680642, offset_real=-0.02699852462816001, ...)

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.724896}

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.

On this page