Note
Run interactively in jupyter notebook.
Calibrating single-qubit gates on ibmq_armonk
¶
In this tutorial we demonstrate how to calibrate single-qubit gates on ibmq_armonk
using the calibration framework in qiskit-experiments. We will run experiments to find the qubit frequency, calibrate the amplitude of DRAG pulses and chose the value of the DRAG parameter that minimizes leakage. The calibration framework requires the user to
setup an instance of
Calibrations
,run calibration experiments which can be found in
qiskit_experiments.library.calibration
.
Note that the values of the parameters stored in the instance of the Calibrations
class will automatically be updated by the calibration experiments. This automatic updating can also be disabled using the auto_update
flag.
[1]:
import pandas as pd
import numpy as np
import qiskit.pulse as pulse
from qiskit.circuit import Parameter
from qiskit_experiments.calibration_management.calibrations import Calibrations
from qiskit import IBMQ, schedule
[2]:
IBMQ.load_account()
provider = IBMQ.get_provider(hub='ibm-q', group='open', project='main')
backend = provider.get_backend('ibmq_armonk')
[3]:
qubit = 0 # The qubit we will work with
The two functions below show how to setup an instance of Calibrations
. To do this the user defines the template schedules to calibrate. These template schedules are fully parameterized, even the channel indices on which the pulses are played. Furthermore, the name of the parameter in the channel index must follow the convention laid out in the documentation of the calibration module. Note that the parameters in the channel indices are automatically mapped to the channel index when
get_schedule
is called.
[4]:
def setup_cals(backend) -> Calibrations:
"""A function to instantiate calibrations and add a couple of template schedules."""
cals = Calibrations.from_backend(backend)
dur = Parameter("dur")
amp = Parameter("amp")
sigma = Parameter("σ")
beta = Parameter("β")
drive = pulse.DriveChannel(Parameter("ch0"))
# Define and add template schedules.
with pulse.build(name="xp") as xp:
pulse.play(pulse.Drag(dur, amp, sigma, beta), drive)
with pulse.build(name="xm") as xm:
pulse.play(pulse.Drag(dur, -amp, sigma, beta), drive)
with pulse.build(name="x90p") as x90p:
pulse.play(pulse.Drag(dur, Parameter("amp"), sigma, Parameter("β")), drive)
cals.add_schedule(xp, num_qubits=1)
cals.add_schedule(xm, num_qubits=1)
cals.add_schedule(x90p, num_qubits=1)
return cals
def add_parameter_guesses(cals: Calibrations):
"""Add guesses for the parameter values to the calibrations."""
for sched in ["xp", "x90p"]:
cals.add_parameter_value(80, "σ", schedule=sched)
cals.add_parameter_value(0.5, "β", schedule=sched)
cals.add_parameter_value(320, "dur", schedule=sched)
cals.add_parameter_value(0.5, "amp", schedule=sched)
When setting up the calibrations we add three pulses: a \(\pi\)-rotation, with a schedule named xp
, a schedule xm
identical to xp
but with a nagative amplitude, and a \(\pi/2\)-rotation, with a schedule named x90p
. Here, we have linked the amplitude of the xp
and xm
pulses. Therefore, calibrating the parameters of xp
will also calibrate the parameters of xm
.
[5]:
cals = setup_cals(backend)
add_parameter_guesses(cals)
A samilar setup is achieved by using a pre-built library of gates. The library of gates provides a standard set of gates and some initial guesses for the value of the parameters in the template schedules. This is shown below using the FixedFrequencyTransmon
which provides the x
, y
, sx
, and sy
pulses. Note that in the example below we change the default value of the pulse duration to 320 samples.
[6]:
from qiskit_experiments.calibration_management.basis_gate_library import FixedFrequencyTransmon
[7]:
library = FixedFrequencyTransmon(default_values={"duration": 320})
cals = Calibrations.from_backend(backend, libraries=[library])
1. Finding qubits with spectroscopy¶
Here, we are using a backend for which we already know the qubit frequency. We will therefore use the spectroscopy experiment to confirm that there is a resonance at the qubit frequency reported by the backend.
[8]:
from qiskit_experiments.library.calibration.rough_frequency import RoughFrequencyCal
We first show the contents of the calibrations for qubit 0. Note that the guess values that we added before apply to all qubits on the chip. We see this in the table below as an empty tuple ()
in the qubits column. Observe that the parameter values of xm
do not appear in this table as they are given by the values of xp
.
[9]:
pd.DataFrame(**cals.parameters_table(qubit_list=[qubit, ()]))
[9]:
parameter | qubits | schedule | value | group | valid | date_time | exp_id | |
---|---|---|---|---|---|---|---|---|
0 | β | () | x | 0.000000e+00 | default | True | 2021-12-12 16:45:32.534775+0100 | None |
1 | duration | () | sx | 3.200000e+02 | default | True | 2021-12-12 16:45:32.534783+0100 | None |
2 | σ | () | x | 8.000000e+01 | default | True | 2021-12-12 16:45:32.534757+0100 | None |
3 | duration | () | x | 3.200000e+02 | default | True | 2021-12-12 16:45:32.534723+0100 | None |
4 | σ | () | sx | 8.000000e+01 | default | True | 2021-12-12 16:45:32.534790+0100 | None |
5 | amp | () | sx | 2.500000e-01 | default | True | 2021-12-12 16:45:32.534805+0100 | None |
6 | drive_freq | (0,) | None | 4.971613e+09 | default | True | 2021-12-12 16:45:32.769415+0100 | None |
7 | amp | () | x | 5.000000e-01 | default | True | 2021-12-12 16:45:32.534767+0100 | None |
8 | meas_freq | (0,) | None | 6.993371e+09 | default | True | 2021-12-12 16:45:32.769441+0100 | None |
9 | β | () | sx | 0.000000e+00 | default | True | 2021-12-12 16:45:32.534798+0100 | None |
[10]:
freq01_estimate = backend.defaults().qubit_freq_est[qubit]
frequencies = np.linspace(freq01_estimate -15e6, freq01_estimate + 15e6, 51)
spec = RoughFrequencyCal(qubit, cals, frequencies, backend=backend)
spec.set_experiment_options(amp=0.1)
[11]:
circuit = spec.circuits()[0]
circuit.draw(output="mpl")
[11]:

[12]:
schedule(circuit, backend).draw()
[12]:

[13]:
spec_data = spec.run().block_for_results()
[14]:
spec_data.figure(0)
[14]:

[15]:
print(spec_data.analysis_results("f01"))
DbAnalysisResultV1
- name: f01
- value: 4971670289.422816 ± 23140.38461583078 Hz
- χ²: 1.3182518484011887
- quality: good
- device_components: ['Q0']
- verified: False
We now update the instance of Calibrations
with the value of the frequency that we measured using the Frequency.update
function. Note that for the remainder of this notebook we use the value of the qubit frequency in the backend as it is not yet possible to updated qubit frequencies with the circuit path.
[16]:
pd.DataFrame(**cals.parameters_table(qubit_list=[qubit]))
[16]:
parameter | qubits | schedule | value | group | valid | date_time | exp_id | |
---|---|---|---|---|---|---|---|---|
0 | drive_freq | (0,) | None | 4.971670e+09 | default | True | 2021-12-12 16:49:50.675000+0100 | 70119ceb-0b48-4a24-b39a-af583e50e219 |
1 | meas_freq | (0,) | None | 6.993371e+09 | default | True | 2021-12-12 16:45:32.769441+0100 | None |
As seen from the table above the measured frequency has been added to the calibrations.
2. Calibrating the pulse amplitudes with a Rabi experiment¶
In the Rabi experiment we apply a pulse at the frequency of the qubit and scan its amplitude to find the amplitude that creates a rotation of a desired angle. We do this with the calibration experiment RoughXSXAmplitudeCal
. This is a specialization of the Rabi
experiment that will update the calibrations for both the X
pulse and the SX
pulse using a single experiment.
[17]:
from qiskit_experiments.library.calibration import RoughXSXAmplitudeCal
[18]:
rabi = RoughXSXAmplitudeCal(qubit, cals, backend=backend)
The rough amplitude calibration is therefore a Rabi experiment in which each circuit contains a pulse with a gate. Different circuits correspond to pulses with different amplitudes.
[19]:
rabi.circuits()[0].draw("mpl")
[19]:

After the experiment completes the value of the amplitudes in the calibrations will automatically be updated. This behaviour can be controlled using the auto_update
argument given to the calibration experiment at initialization.
[20]:
rabi_data = rabi.run().block_for_results()
[21]:
rabi_data.figure(0)
[21]:

[22]:
print(rabi_data.analysis_results("rabi_rate"))
DbAnalysisResultV1
- name: rabi_rate
- value: 0.5842264468922855 ± 0.001401840343655317
- χ²: 3.933170680803399
- quality: bad
- device_components: ['Q0']
- verified: False
[23]:
pd.DataFrame(**cals.parameters_table(qubit_list=[qubit, ()], parameters="amp"))
[23]:
parameter | qubits | schedule | value | group | valid | date_time | exp_id | |
---|---|---|---|---|---|---|---|---|
0 | amp | (0,) | x | 0.855833+0.000000j | default | True | 2021-12-12 16:58:31.216000+0100 | 2ebdbde5-f504-41d6-8bb3-4fd7286a15fa |
1 | amp | () | sx | 0.250000+0.000000j | default | True | 2021-12-12 16:45:32.534805+0100 | None |
2 | amp | (0,) | sx | 0.427916+0.000000j | default | True | 2021-12-12 16:58:31.216000+0100 | 2ebdbde5-f504-41d6-8bb3-4fd7286a15fa |
3 | amp | () | x | 0.500000+0.000000j | default | True | 2021-12-12 16:45:32.534767+0100 | None |
The table above shows that we have now updated the amplitude of our \(\pi\)-pulse from 0.5 to the value obtained in the most recent Rabi experiment. Importantly, since we linked the amplitudes of the x
and y
schedules we will see that the amplitude of the y
schedule has also been updated as seen when requesting schedules form the Calibrations
instance. Furthermore, we used the result from the Rabi
experiment to also update the value of the sx
pulse. This was achieved
by specifying (np.pi/2, "amp", "sx")
when calling update
.
[24]:
cals.get_schedule("sx", qubit)
[24]:
ScheduleBlock(Play(Drag(duration=320, amp=(0.42791627+0j), sigma=80, beta=0), DriveChannel(0)), name="sx", transform=AlignLeft())
[25]:
cals.get_schedule("x", qubit)
[25]:
ScheduleBlock(Play(Drag(duration=320, amp=(0.85583253+0j), sigma=80, beta=0), DriveChannel(0)), name="x", transform=AlignLeft())
[26]:
cals.get_schedule("y", qubit)
[26]:
ScheduleBlock(Play(Drag(duration=320, amp=0.85583253j, sigma=80, beta=0), DriveChannel(0)), name="y", transform=AlignLeft())
3. Saving and loading calibrations¶
The values of the calibrated parameters can be saved to a .csv
file and reloaded at a later point in time.
[27]:
cals.save(file_type="csv", overwrite=True, file_prefix="Armonk")
After saving the values of the parameters you may restart your kernel. If you do so, you will only need to run the following cell to recover the state of your calibrations. Since the schedules are currently not stored we need to call our setup_cals
function to populate an instance of Calibrations
with the template schedules. By contrast, the value of the parameters will be recovered from the file.
[28]:
cals = Calibrations.from_backend(backend, library)
cals.load_parameter_values(file_name="Armonkparameter_values.csv")
[29]:
pd.DataFrame(**cals.parameters_table(qubit_list=[qubit, ()], parameters="amp"))
[29]:
parameter | qubits | schedule | value | group | valid | date_time | exp_id | |
---|---|---|---|---|---|---|---|---|
0 | amp | (0,) | x | 0.855833+0.000000j | default | True | 2021-12-12 16:58:31.216000+0100 | 2ebdbde5-f504-41d6-8bb3-4fd7286a15fa |
1 | amp | () | sx | 0.250000+0.000000j | default | True | 2021-12-12 16:58:48.227382+0100 | None |
2 | amp | (0,) | sx | 0.427916+0.000000j | default | True | 2021-12-12 16:58:31.216000+0100 | 2ebdbde5-f504-41d6-8bb3-4fd7286a15fa |
3 | amp | () | x | 0.500000+0.000000j | default | True | 2021-12-12 16:58:48.227345+0100 | None |
4. Calibrating the value of the DRAG coefficient¶
A Derivative Removal by Adiabatic Gate (DRAG) pulse is designed to minimize leakage to a neighbouring transition. It is a standard pulse with an additional derivative component. It is designed to reduce the frequency spectrum of a normal pulse near the \(|1\rangle\) - \(|2\rangle\) transition, reducing the chance of leakage to the \(|2\rangle\) state. The optimal value of the DRAG parameter is chosen to minimize both leakage and phase errors resulting from the AC Stark shift. The pulse envelope is \(f(t) = \Omega_x(t) + j \beta \frac{\rm d}{{\rm d }t} \Omega_x(t)\). Here, \(\Omega_x\) is the envelop of the in-phase component of the pulse and \(\beta\) is the strength of the quadrature which we refer to as the DRAG parameter and seek to calibrate in this experiment. The DRAG calibration will run several series of circuits. In a given circuit a Rp(β) - Rm(β) block is repeated \(N\) times. Here, Rp is a rotation with a positive angle and Rm is the same rotation with a negative amplitude.
[30]:
from qiskit_experiments.library import RoughDragCal
[31]:
cal_drag = RoughDragCal(qubit, cals, backend=backend, betas=np.linspace(-20, 20, 25))
[32]:
cal_drag.set_experiment_options(reps=[3, 5, 7])
cal_drag.circuits()[5].draw(output='mpl')
[32]:

[33]:
drag_data = cal_drag.run().block_for_results()
[34]:
drag_data.figure(0)
[34]:

[35]:
print(drag_data.analysis_results("beta"))
DbAnalysisResultV1
- name: beta
- value: -1.1559985430408373 ± 0.009022771608798948
- χ²: 1.0864875414479318
- quality: good
- device_components: ['Q0']
- verified: False
[36]:
pd.DataFrame(**cals.parameters_table(qubit_list=[qubit, ()], parameters="β"))
[36]:
parameter | qubits | schedule | value | group | valid | date_time | exp_id | |
---|---|---|---|---|---|---|---|---|
0 | β | () | x | 0.000000 | default | True | 2021-12-12 16:58:48.227353+0100 | None |
1 | β | (0,) | x | -1.155999 | default | True | 2021-12-12 17:08:01.247000+0100 | 3dda7894-1c3a-445a-8e99-fadccdc76a3c |
2 | β | () | sx | 0.000000 | default | True | 2021-12-12 16:58:48.227375+0100 | None |
5. Fine amplitude calibration¶
The FineAmplitude
calibration experiment repeats \(N\) times a gate with a pulse to amplify the under or over-rotations in the gate to determine the optimal amplitude. The circuits that are run have a custom gate with the pulse schedule attached to it through the calibrations.
[37]:
from qiskit_experiments.library.calibration.fine_amplitude import FineXAmplitudeCal
[38]:
amp_x_cal = FineXAmplitudeCal(qubit, cals, backend=backend, schedule_name="x")
[39]:
amp_x_cal.circuits()[5].draw(output="mpl")
[39]:

[40]:
data_fine = amp_x_cal.run().block_for_results()
[41]:
data_fine.figure(0)
[41]:

[42]:
print(data_fine.analysis_results("d_theta"))
DbAnalysisResultV1
- name: d_theta
- value: -0.1259812027299088 ± 0.0015459904802351424
- χ²: 1.1671390569895568
- quality: good
- device_components: ['Q0']
- verified: False
The cell below shows how the amplitude is updated based on the error in the rotation angle measured by the FineXAmplitude
experiment. Note that this calculation is automatically done by the Amplitude.update
function.
[43]:
dtheta = data_fine.analysis_results("d_theta").value.value
target_angle = np.pi
scale = target_angle / (target_angle + dtheta)
pulse_amp = cals.get_parameter_value("amp", qubit, "x")
print(f"The ideal angle is {target_angle:.2f} rad. We measured a deviation of {dtheta:.3f} rad.")
print(f"Thus, scale the {pulse_amp:.4f} pulse amplitude by {scale:.3f} to obtain {pulse_amp*scale:.5f}.")
The ideal angle is 3.14 rad. We measured a deviation of -0.126 rad.
Thus, scale the 0.8916+0.0000j pulse amplitude by 1.042 to obtain 0.92883+0.00000j.
Observe, once again, that the calibrations have automatically been updated.
[44]:
pd.DataFrame(**cals.parameters_table(qubit_list=[qubit, ()], parameters="amp"))
[44]:
parameter | qubits | schedule | value | group | valid | date_time | exp_id | |
---|---|---|---|---|---|---|---|---|
0 | amp | (0,) | x | 0.891586+0.000000j | default | True | 2021-12-12 17:13:11.518000+0100 | 11278d3c-b5e7-4e51-b00e-b2fcaabbb3e9 |
1 | amp | () | sx | 0.250000+0.000000j | default | True | 2021-12-12 16:58:48.227382+0100 | None |
2 | amp | (0,) | sx | 0.427916+0.000000j | default | True | 2021-12-12 16:58:31.216000+0100 | 2ebdbde5-f504-41d6-8bb3-4fd7286a15fa |
3 | amp | () | x | 0.500000+0.000000j | default | True | 2021-12-12 16:58:48.227345+0100 | None |
To check that we have managed to reduce the error in the rotation angle we will run the fine amplitude calibration experiment once again.
[45]:
data_fine2 = amp_x_cal.run().block_for_results()
[46]:
data_fine2.figure(0)
[46]:

As can be seen from the data above and the analysis result below we have managed to reduce the error in the rotation angle \({\rm d}\theta\).
[47]:
print(data_fine2.analysis_results("d_theta"))
DbAnalysisResultV1
- name: d_theta
- value: -0.02297341948181875 ± 0.0006927301059360831
- χ²: 1.93685218471896
- quality: good
- device_components: ['Q0']
- verified: False
Fine amplitude calibration of the \(\pi/2\) rotation¶
We now wish to calibrate the amplitude of the \(\pi/2\) rotation.
[48]:
from qiskit_experiments.library.calibration.fine_amplitude import FineSXAmplitudeCal
[49]:
amp_sx_cal = FineSXAmplitudeCal(qubit, cals, backend=backend, schedule_name="sx")
[50]:
amp_sx_cal.circuits()[5].draw(output="mpl")
[50]:

[51]:
data_fine_sx = amp_sx_cal.run().block_for_results()
[52]:
data_fine_sx.figure(0)
[52]:

[53]:
print(data_fine_sx.analysis_results(0))
DbAnalysisResultV1
- name: @Parameters_FineAmplitudeAnalysis
- value: [0.79868138 0.03532474 0.48706798] ± [0.00668965 0.00057385 0.00179089]
- χ²: 1.1894079114882583
- quality: good
- extra: <4 items>
- device_components: ['Q0']
- verified: False
[54]:
print(data_fine_sx.analysis_results("d_theta"))
DbAnalysisResultV1
- name: d_theta
- value: 0.035324738012950226 ± 0.0005738510570579542
- χ²: 1.1894079114882583
- quality: good
- device_components: ['Q0']
- verified: False
The parameter value is reflected in the calibrations.
[55]:
pd.DataFrame(**cals.parameters_table(qubit_list=[qubit, ()], parameters="amp"))
[55]:
parameter | qubits | schedule | value | group | valid | date_time | exp_id | |
---|---|---|---|---|---|---|---|---|
0 | amp | (0,) | x | 0.898154+0.000000j | default | True | 2021-12-12 17:16:31.432000+0100 | 3cfb9f75-1ba8-4de5-9f68-68f9871be450 |
1 | amp | () | sx | 0.250000+0.000000j | default | True | 2021-12-12 16:58:48.227382+0100 | None |
2 | amp | (0,) | sx | 0.418505+0.000000j | default | True | 2021-12-12 17:19:01.453000+0100 | 26b8c108-38dc-4d45-921a-e586d21dc261 |
3 | amp | () | x | 0.500000+0.000000j | default | True | 2021-12-12 16:58:48.227345+0100 | None |
[56]:
cals.get_schedule("sx", qubit)
[56]:
ScheduleBlock(Play(Drag(duration=320, amp=(0.4185047565+0j), sigma=80, beta=0), DriveChannel(0)), name="sx", transform=AlignLeft())
[57]:
cals.get_schedule("x", qubit)
[57]:
ScheduleBlock(Play(Drag(duration=320, amp=(0.8981539795+0j), sigma=80, beta=-1.155998543), DriveChannel(0)), name="x", transform=AlignLeft())
[58]:
cals.get_schedule("y", qubit)
[58]:
ScheduleBlock(Play(Drag(duration=320, amp=0.8981539795j, sigma=80, beta=-1.155998543), DriveChannel(0)), name="y", transform=AlignLeft())
[59]:
import qiskit.tools.jupyter
%qiskit_copyright
This code is a part of Qiskit
© Copyright IBM 2017, 2021.
This code is licensed under the Apache License, Version 2.0. You may
obtain a copy of this license in the LICENSE.txt file in the root directory
of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.
Any modifications or derivative works of this code must retain this
copyright notice, and modified files need to carry a notice indicating
that they have been altered from the originals.
[ ]: