Download
Download this file as Jupyter notebook: qubit_rabi.ipynb.
Rabi experiment
This guide shows how to perform a Rabi experiment to calibrate a \(\pi\)-pulse
using a program. This experiment can be easily generated using the
RabiExperiment
class.
The Rabi experiment is the second step in calibrating our first quantum gates. Following the Qubit spectroscopy experiment, we learned the energy gap between the ground state and first excited state of our two-level system. In other words, we have determined the resonance frequency of our qubit, and can now apply pulses at this frequency to alter the qubit’s state.
The state of the qubit can be visualized using the Bloch sphere. The ground state \(|0\rangle\) is represented by the north pole, and the excited state \(|1\rangle\) is represented by the south pole as shown. By applying pulses of varying strengths to a qubit in the ground state, we can drive a rotation around the Bloch sphere by some as-yet-unknown angles.

A rotation of \(\pi\) takes us from \(|0\rangle\) to \(|1\rangle\), which is the operation implemented by a Pauli-X gate. The pulse that drives this rotation is called a \(\pi\)-pulse. Here we assume that the pulse duration is fixed, and we will vary strength by varying pulse amplitude. Our goal is to learn the required \(\pi\)-pulse amplitude, in order to calibrate our gate.
To learn the pulse amplitude, we perform a Rabi experiment as follows:
Initialize the qubit to the ground state.
Apply a control pulse with the resonance frequency \(\omega_r\) and amplitude \(A\), for a fixed duration, on the target qubit.
Measure the population of the qubit in the excited state.
Repeat the above steps with varying amplitude \(A\).
The observed population as a function of amplitude will be a sinusoid, corresponding to rotations around the Bloch sphere. We can read off the \(\pi\)-pulse amplitude as half the period of the observed oscillation.
[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 RabiExperiment
class. We load
a make_calibration_set
function that creates
a calibration set for the given amount of qubit containing the linkers for the RX
, Z
and measurement gates. Lastly, we import an experiment with simulated data
to demonstrate the fitting and calibration workflow at the end of this file.
[3]:
from keysight.qcs.experiments import RabiExperiment, make_calibration_set
from simulated_experiments.simulated_experiments import SimulatedRabiExperiment
[4]:
# 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_addr")
# create Rabi experiment
rabi_experiment = RabiExperiment(mapper, calibration_set=calibration_set, qubits=qubits)
rabi_experiment.program.draw()
# The program consists of a simple `RX` gate with variable amplitude followed by a
# measurement. During execution, we set the amplitude of this gate to a range of values
# from zero to one.
Program
Program
|
|||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
Layer #0
Layer #0
|
Layer #1
Layer #1
|
||||||||||||||
|
|
RFWaveform on ('xy_pulse', 0)
Parameters
|
|||||||||||||
|
|
Measure on ('qudits', 0)
Parameters
|
[5]:
# configure the repetitions for this experiment
start_amplitude = 0
end_amplitude = 1
steps = 10
scan_values = np.linspace(start_amplitude, end_amplitude, steps)
rabi_experiment.configure_repetitions(amplitudes=scan_values, n_shots=1)
Compiling this program to the waveform level using the
ParameterizedLinker
s in the calibration set
results in the following program:
[6]:
rabi_experiment.compiled_program.draw()
Program
Program
|
|||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
Layer #0
Layer #0
|
Layer #1
Layer #1
|
||||||||||||||||||||||||||
|
|
RFWaveform on ('xy_pulse', 0)
Parameters
|
|||||||||||||||||||||||||
|
|
Delay on ('readout_pulse', 0)
Parameters
|
RFWaveform on ('readout_pulse', 0)
Parameters
|
Delay on ('readout_pulse', 0)
Parameters
|
|||||||||||||||||||||||
|
|
Delay on ('readout_acquisition', 0)
Parameters
|
Acquisition on ('readout_acquisition', 0)
Parameters
|
Delay on ('readout_acquisition', 0)
Parameters
|
We again use the render method to visualize this with the
ChannelMapper
.
[7]:
rabi_experiment.compiled_program.render(
channel_subplots=False,
lo_frequency=5e9,
sweep_index=5,
sample_rate=5e9,
)
To execute this experiment, we can simply run
[8]:
if run_on_hw:
rabi_experiment.execute()
else:
# load in a previously executed version of this experiment
rabi_experiment = qcs.load("RabiExperiment.qcs")
For the purposes of this demonstration, we added a second “ancilla” qubit to the Rabi program and connected the physical output channels for our qubit control to the digizer associated with the ancilla to allow us to capture both the control and the readout pulse.
[9]:
rabi_experiment.draw()
Program
Program
|
|||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
Layer #0
Layer #0
|
Layer #1
Layer #1
|
||||||||||||||
|
|
RFWaveform on ('xy_pulse', 0)
Parameters
|
|||||||||||||
|
|
Measure on ('qudits', 0)
Parameters
|
|||||||||||||
|
|
Measure on ('ancilla', 1)
Parameters
|
[10]:
rabi_experiment.compiled_program.draw()
Program
Program
|
|||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
Layer #0
Layer #0
|
Layer #1
Layer #1
|
||||||||||||||||||||||||||
|
|
RFWaveform on ('xy_pulse', 0)
Parameters
|
|||||||||||||||||||||||||
|
|
Delay on ('readout_pulse', 0)
Parameters
|
RFWaveform on ('readout_pulse', 0)
Parameters
|
Delay on ('readout_pulse', 0)
Parameters
|
|||||||||||||||||||||||
|
|
Delay on ('readout_acquisition', 0)
Parameters
|
Acquisition on ('readout_acquisition', 0)
Parameters
|
Delay on ('readout_acquisition', 0)
Parameters
|
|||||||||||||||||||||||
|
Acquisition on ('readout_acquisition', 1)
Parameters
|
The ancilla qubit is mapped to the digitizer channel 1
and has a single
acquisition that spans the duration of the control pulse.
[11]:
rabi_experiment.plot_trace()
Here we can see the control pulse with our varying amplitudes. 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 desired drive amplitude from the Rabi experiment and how to update the corresponding variables in our calibration set. For this example, we load in an experiment with simulated data that we imported at the beginning of the file:
[12]:
rabi_experiment = SimulatedRabiExperiment(calibration_set, qubits)
[13]:
rabi_experiment.plot_iq(plot_type="linear")
The fit()
method takes this I/Q data
and fits it to a decaying sinusoidal model, as specified by the built-in
DecayingSinusoid
. In order to specify how to
prepare the I/Q data for fitting (in this case, we want to fit the magnitude), the
RabiExperiment
uses the
IQuadrature
pre-processor, which extracts the
I quandrature out of the complex I/Q data.
The result of the fit is an EstimateCollection
,
which contains individual Estimate
s for each
qubit that was fitted.
[14]:
ec = rabi_experiment.fit()
print(ec)
print(ec.estimates[0])
EstimateCollection(1)
Estimate(amplitude=1.3380733822618327, decay_rate=2.1250154939591077, frequency=39.84246404567255, phase=-1.5244173114068151, ...)
We can represent estimate parameters and its values and estimate collection
in a tabular form by calling the
draw()
method:
[15]:
ec.draw()
Estimate Collection
| |||||||||
---|---|---|---|---|---|---|---|---|---|
0
|
|||||||||
amplitude
|
1.33807
Estimate:
|
||||||||
decay_rate
|
2.12502
Estimate:
|
||||||||
frequency
|
39.84246
Estimate:
|
||||||||
offset
|
1.22284
Estimate:
|
||||||||
phase
|
-1.52442
Estimate:
|
The fitted and the pre-processed data can be visualized by calling the
plot()
method:
[16]:
rabi_experiment.plot()
We can then get the calibration value associated to this Rabi 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.
[17]:
rabi_experiment.get_updated_calibration_values()
[17]:
{'x180_pulse_amplitudes': 0.05019769855608499}
To check the current state of that variable in the calibration_set to confirm its update, one can do:
[18]:
rabi_experiment.calibration_set.variables.x180_pulse_amplitudes.value
[18]:
array([0.5])
The update is then done as follows:
[19]:
rabi_experiment.set_updated_calibration_values()
rabi_experiment.calibration_set.variables.x180_pulse_amplitudes.value
[19]:
array([0.0501977])
The last line printing the new updated value, thus confirming that the update has been successful.
Download
Download this file as Jupyter notebook: qubit_rabi.ipynb.