<jupyter_start><jupyter_text>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.<jupyter_code>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
IBMQ.load_account()
provider = IBMQ.get_provider(hub='ibm-q', group='open', project='main')
backend = provider.get_backend('ibmq_armonk')
qubit = 0  # The qubit we will work with<jupyter_output><empty_output><jupyter_text>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.<jupyter_code>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)<jupyter_output><empty_output><jupyter_text>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`.<jupyter_code>cals = setup_cals(backend)
add_parameter_guesses(cals)<jupyter_output><empty_output><jupyter_text>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.<jupyter_code>from qiskit_experiments.calibration_management.basis_gate_library import FixedFrequencyTransmon
library = FixedFrequencyTransmon(default_values={"duration": 320})
cals = Calibrations.from_backend(backend, library)<jupyter_output><empty_output><jupyter_text>1. Finding qubits with spectroscopyHere, 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.<jupyter_code>from qiskit_experiments.library.calibration.rough_frequency import RoughFrequencyCal<jupyter_output><empty_output><jupyter_text>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`.<jupyter_code>pd.DataFrame(**cals.parameters_table(qubit_list=[qubit, ()]))
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)
circuit = spec.circuits()[0]
circuit.draw(output="mpl")
schedule(circuit, backend).draw()
spec_data = spec.run().block_for_results()
spec_data.figure(0)
print(spec_data.analysis_results("f01"))<jupyter_output>DbAnalysisResultV1
- name: f01
- value: 4971670289.422816 ± 23140.38461583078 Hz
- χ²: 1.3182518484011887
- quality: good
- device_components: ['Q0']
- verified: False<jupyter_text>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.<jupyter_code>pd.DataFrame(**cals.parameters_table(qubit_list=[qubit]))<jupyter_output><empty_output><jupyter_text>As seen from the table above the measured frequency has been added to the calibrations.  2. Calibrating the pulse amplitudes with a Rabi experimentIn 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.<jupyter_code>from qiskit_experiments.library.calibration import RoughXSXAmplitudeCal
rabi = RoughXSXAmplitudeCal(qubit, cals, backend=backend)<jupyter_output><empty_output><jupyter_text>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.<jupyter_code>rabi.circuits()[0].draw("mpl")<jupyter_output><empty_output><jupyter_text>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.<jupyter_code>rabi_data = rabi.run().block_for_results()
rabi_data.figure(0)
print(rabi_data.analysis_results("rabi_rate"))
pd.DataFrame(**cals.parameters_table(qubit_list=[qubit, ()], parameters="amp"))<jupyter_output><empty_output><jupyter_text>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`.<jupyter_code>cals.get_schedule("sx", qubit)
cals.get_schedule("x", qubit)
cals.get_schedule("y", qubit)<jupyter_output><empty_output><jupyter_text>3. Saving and loading calibrationsThe values of the calibrated parameters can be saved to a `.csv` file and reloaded at a later point in time.<jupyter_code>cals.save(file_type="csv", overwrite=True, file_prefix="Armonk")<jupyter_output>/home/daniel/Documents/IBM/qiskit/qiskit-experiments/qiskit_experiments/calibration_management/calibrations.py:1333: UserWarning: Schedules are only saved in text format. They cannot be re-loaded.
  warnings.warn("Schedules are only saved in text format. They cannot be re-loaded.")<jupyter_text>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.<jupyter_code>cals = Calibrations.from_backend(backend, library)
cals.load_parameter_values(file_name="Armonkparameter_values.csv")
pd.DataFrame(**cals.parameters_table(qubit_list=[qubit, ()], parameters="amp"))<jupyter_output><empty_output><jupyter_text>4. Calibrating the value of the DRAG coefficientA Derivative Removal by Adiabatic Gate (DRAG) pulse is designed to minimize leakageto a neighbouring transition. It is a standard pulse with an additional derivativecomponent. It is designed to reduce the frequency spectrum of a normal pulse nearthe $|1\rangle$ - $|2\rangle$ transition, reducing the chance of leakageto the $|2\rangle$ state. The optimal value of the DRAG parameter is chosen tominimize 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 DRAGparameter and seek to calibrate in this experiment. The DRAG calibration will runseveral 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 rotationwith a negative amplitude.<jupyter_code>from qiskit_experiments.library import RoughDragCal
cal_drag = RoughDragCal(qubit, cals, backend=backend, betas=np.linspace(-20, 20, 25))
cal_drag.set_experiment_options(reps=[3, 5, 7])

cal_drag.circuits()[5].draw(output='mpl')
drag_data = cal_drag.run().block_for_results()
drag_data.figure(0)
print(drag_data.analysis_results("beta"))
pd.DataFrame(**cals.parameters_table(qubit_list=[qubit, ()], parameters="β"))<jupyter_output><empty_output><jupyter_text>5. Fine amplitude calibrationThe `FineAmplitude` calibration experiment repeats $N$ times a gate with a pulseto 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 itthrough the calibrations.<jupyter_code>from qiskit_experiments.library.calibration.fine_amplitude import FineXAmplitudeCal
amp_x_cal = FineXAmplitudeCal(qubit, cals, backend=backend, schedule_name="x")
amp_x_cal.circuits()[5].draw(output="mpl")
data_fine = amp_x_cal.run().block_for_results()
data_fine.figure(0)
print(data_fine.analysis_results("d_theta"))<jupyter_output>DbAnalysisResultV1
- name: d_theta
- value: -0.1259812027299088 ± 0.0015459904802351424
- χ²: 1.1671390569895568
- quality: good
- device_components: ['Q0']
- verified: False<jupyter_text>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.<jupyter_code>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}.")<jupyter_output>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.<jupyter_text>Observe, once again, that the calibrations have automatically been updated.<jupyter_code>pd.DataFrame(**cals.parameters_table(qubit_list=[qubit, ()], parameters="amp"))<jupyter_output><empty_output><jupyter_text>To check that we have managed to reduce the error in the rotation angle we will run the fine amplitude calibration experiment once again.<jupyter_code>data_fine2 = amp_x_cal.run().block_for_results()
data_fine2.figure(0)<jupyter_output><empty_output><jupyter_text>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$.<jupyter_code>print(data_fine2.analysis_results("d_theta"))<jupyter_output>DbAnalysisResultV1
- name: d_theta
- value: -0.02297341948181875 ± 0.0006927301059360831
- χ²: 1.93685218471896
- quality: good
- device_components: ['Q0']
- verified: False<jupyter_text>Fine amplitude calibration of the $\pi/2$ rotationWe now wish to calibrate the amplitude of the $\pi/2$ rotation.<jupyter_code>from qiskit_experiments.library.calibration.fine_amplitude import FineSXAmplitudeCal
amp_sx_cal = FineSXAmplitudeCal(qubit, cals, backend=backend, schedule_name="sx")
amp_sx_cal.circuits()[5].draw(output="mpl")
data_fine_sx = amp_sx_cal.run().block_for_results()
data_fine_sx.figure(0)
print(data_fine_sx.analysis_results(0))
print(data_fine_sx.analysis_results("d_theta"))<jupyter_output>DbAnalysisResultV1
- name: d_theta
- value: 0.035324738012950226 ± 0.0005738510570579542
- χ²: 1.1894079114882583
- quality: good
- device_components: ['Q0']
- verified: False<jupyter_text>The parameter value is reflected in the calibrations.<jupyter_code>pd.DataFrame(**cals.parameters_table(qubit_list=[qubit, ()], parameters="amp"))
cals.get_schedule("sx", qubit)
cals.get_schedule("x", qubit)
cals.get_schedule("y", qubit)
import qiskit.tools.jupyter
%qiskit_copyright<jupyter_output><empty_output>
{'fraction_non_alphanumeric': 0.08877954120174648, 'fraction_numerical': 0.02432601011851133, 'mean_word_length': 6.132970835392981, 'pattern_counts': {'":': 1, '<': 79, '<?xml version=': 0, '>': 80, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '21', 'dir': 'jupyter-structured-clean-dedup', 'id': '56673', 'license': 'Apache-2.0', 'n_tokens_mistral': 4676, 'n_tokens_neox': 4273, 'n_words': 1497, 'path': 'docs/tutorials/calibrating_armonk.ipynb', 'repo_name': 'jakelishman/qiskit-experiments', 'size': '14429'}