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)

return cals

"""Add guesses for the parameter values to the calibrations."""
for sched in ["xp", "x90p"]:


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)


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


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)

[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


### This code is a part of Qiskit

[ ]: