$ \newcommand{\bra}[1]{\langle #1|} $ $ \newcommand{\ket}[1]{|#1\rangle} $ $ \newcommand{\braket}[2]{\langle #1|#2\rangle} $ $ \newcommand{\ketbra}[2]{| #1\rangle \langle #2|} $ $ \newcommand{\tr}{{\rm tr}} $ $ \newcommand{\i}{{\color{blue} i}} $ $ \newcommand{\Hil}{{\cal H}} $ $ \newcommand{\V}{{\cal V}} $ $ \newcommand{\bn}{{\bf n}} $
2. Single-qubit driving¶
The pulse-level implementation of some of the most common native gates will be explored, including the physical meaning of qubit driving, the different techniques and features available in Qmio using the OpenPulse grammar, and illustrative examples of gate calibration and single-qubit operations based on Rabi oscillations.
2.1. Qubit pulse control¶
Now that we understand how a qubit system -such as a transmon- is physically described, we can move into action and learn how to interact with it to change its state and apply transformations equivalent to ideal quantum gates. The physical implementation of this driving depends on the specific hardware -for example, lasers for NV-centers- but in general, it consists of applying pulses to add energy to the system. For example, in supeconducting qubits like transmons, the control is performed by coupling the system to a microwave driving line, which feeds energy to the qubit in a manner analogous to a forced oscillator.
https://doi.org/10.1063/1.5089550
Interpreting these pulses as photons, the general idea is to apply them at the resonance frequency of the qubit, which corresponds to the transition frequency of $\ket{0}\leftrightarrow\ket{1}$. As a result, either excitation -via absorption- or relaxation -via stimulated emission- can happen, depending on the qubit's state:
If the qubit is in $\ket{0}$, only absorption can occur, exciting the system to $\ket{1}$.
If the qubit is in $\ket{1}$, the applied photons can't promote the system to a higher state because of the transmon's anharmonicity. In this case, the only possible effect is stimulated emission (similar to what occurs in lasers), which relaxes de qubit back to $\ket{0}$.
When a continuous pulse of photons is applied, the populations of the states oscillate with time. This behaviour in known as Rabi oscillations. These oscillations are key to implementing quantum gates, because their period can be controlled by adjusting the pulse amplitude and duration. For example, by calibrating the pulse duration to completetly populate $\ket{1}$ starting from $\ket{0}$, one effectively implements an X gate.
The pendulum analogy¶
A natural question arises in this setup: If the qubit is an oscillator, why does its energy sometimes decrease even though we are always "adding energy"? The answer lies in the transmon's anharmonicity. If the qubit were a purely harmonic oscillator, driving it at the resonance frequency would lead to unbounded excitation, promoting the system indefinitely through higher energy states -just as in a classical forced harmonic oscillator. The anharmonicity of the transmon limits the population of the oscillator states, as only the $\ket{0}\leftrightarrow\ket{1}$ transition is allowed.
It may not still be clear how the anharmonicity is responsible of this effect in transmons, fortunatetly, there is a classical system that presents a similar behaviour that could help us visualize it: the forced non-linear pendulum. Bringing our attention to the classical pendulum, when the oscillations are small enough, we can model it as an harmonic oscillator with a resonance frequency $\left(g/L\right)$, but when the oscillations grow in magnitude, the non-linear terms become relevant and the resonance frequency is shifted. If an oscillating force -pulse- is applied on the pendulum at the resonance frequency of its harmonic regime, the energy of the system will be bounded -meaning that it will reach a maximum amplitude- because the frequency will be detuned by the non-linear terms.
Although they are not physically equivalent systems, we can make the following analogy: when the pendulum has the minimum potential energy (no amplitude) we say that it's on the state $\ket{0}$, and it is in state $\ket{1}$ when it has the maximum aplitude. Obviously, for the classical pendulum there is an infinite number of energy eigenstates between those two, but we can understand these intermidiate states as the superposition of $\ket{0}$ and $\ket{1}$. The observed amplitude from the oscillation can be interpreted as the mean value, which depends on the populations of both states. The following video is a representation of this system with a constant amplitude pulse driving, keep in mind that this simulation is for the classical ecuations of the forced non-linear pendulum:
As you can see, the system starts with zero energy at $\ket{0}$. At first, the incoming pulse produces the excitation, promoting the state and increasing the mean energy. This is analogous to the state $\ket{1}$ being populated by absorption. After some time, we arrive to the maximum amplitude at $\ket{1}$, from where the mean energy begins to decrease again. This is analogous to the relaxation of the system by stimulated emission. Finally, the characteristic Rabi-like periodic behaviour appears as the pulse continues to being applied.
As seen before, the application of a quantum gate, for example an X gate, can be performed by choosing the appropiate duration of the pulse -depending also on its amplitude- that leaves the system totally excited. We can also visualize this with the forced pendulum, where we only need to stop applying the pulse when the oscillator reaches its maximum amplitude. On the next video, you can see that the systems starts at the minimum of energy -representing state $\ket{0}$- and when the pulse stops, it leaves the system in the maximum energy that could be achieved by the resonance -representing state $\ket{1}$.
In fact, the similarities of these systems are not a coincidence. If we take a look at their Hamiltonians, we can see that both have a very similar expression:
$$\begin{align*} H_\text{pendulum} &= \frac{p_\theta^2}{2ml^2}-mgl\cos\theta,\\ \\ H_\text{transmon} &= 4E_Cn^2 -E_J\cos\phi, \end{align*}$$
where $\theta$ is the angle of the pendulum and $\phi$ is the superconducting phase in the Josephson junction. Both coordinates can be understood as the position of the oscillator. As we can see, the profile for their potential energy even has the same non-linear term, which explains the similar behaviour when the driving is applied. Obviously, the main difference is the quantization of the energy: in the transmon the detuning is due to the anharmonicity of the energy levels; in the peundulum, the detuning of the resonance frequency happens continuously as the energy increases.
2.2. Pulse-level quantum gates¶
We have seen why pulses produce Rabi oscillations in qubits, but we need to have some physical model to be able to design these pulses for a gate implementations. In this section, first we'll see step-by-step the characterization of pulses, and how they determine the Rabi oscillation period. Then, we'll learn how to implement these pulses with OpenPulse and observe Rabi oscillations on real quantum hardware.
Qubit driving in the rotating frame¶
The driving coupled line can be expressed as follows: $$H_d=-i\Omega V_d(t)(a-a^\dagger),$$ where $\Omega$ is some constant that depends con the capacitances of the superconducting circuit, $V_d(t)$ is the time-dependent potential that characterizes the pulse -this is the signal we have control over- and $a\left(a^\dagger\right)$ are the anihilation(creation) operators for the qubit oscillator. This operators, truncated for a two level system, cab be expressed as: $$H_d=\Omega V_d(t)\sigma_y,$$ where $\sigma_y$ is the Pauli operator. The complete qubit-driving system has the following shape: $$H=H_0+H_d=-\frac{\hbar\omega_q}{2}\sigma_z+\Omega V_d(t)\sigma_y.$$ This Hamiltonian will determine the time evolution of the system, due to both the intrinsic evolution of the oscillator and the driving. We have seen that the part corresponding to the intrinsic evolution can be easily reverted by working on the rotation frame of the qubit. When applying the corresponding transformations, the Hamiltonian of the qubit driving is: $$\tilde{H}=\Omega V_d(t) \bigl[\cos(\omega_q t)\sigma_y-\sin(\omega_q t)\sigma_x\bigl]. $$
Potential characterization¶
The part of the pulse that we have control over is the oscillating potential that generates it: $V_d(t)$. The shape of this potential can be described by a constant amplitude $\left(V_0\right)$, an amplitude envelope $\left(s(t)\right)$ and the oscillatory component with a driving frequency $\left(\omega_d\right)$ and an initial phase $\left(\phi\right)$: $$V_d(t)=V_0\cdot s(t)\cdot \sin(\omega_d t + \phi).$$ Nevertheless, it's usual to decompose oscillatory part in the in-phase $\mathcal{I}$ and out-of-phase $\mathcal{Q}$ terms: $$V_d(t)=V_0s(t)\left[\mathcal{I}\sin(\omega_d t) + \mathcal{Q}\cos(\omega_d t)\right],$$ where: $$\begin{align*} \mathcal{I}=\cos\phi,\quad \mathcal{Q}=\sin\phi. \end{align*}$$ By inserting this expression on the rotating-frame Hamiltonian, we arrive -after eliminating some high frequency terms by the rotating wave approximation- to the following effective Hamiltonian: $$\begin{align*} \tilde{H}=\frac{\Omega V_0}{2}s(t)\Bigl[-\left(\mathcal{I}\cos(\delta\omega t) + \mathcal{Q}\sin(\delta\omega t)\right)\sigma_x+\left(-\mathcal{I}\sin(\delta\omega t) + \mathcal{Q}\cos(\delta\omega t)\right)\sigma_y\Bigl], \end{align*}$$ where $\delta\omega=\omega_q-\omega_d$ is the detuning of the driving frequency from the resonance of the qubit.
Rabi oscillations¶
When we drive the qubit at resonance $\omega_d=\omega_q$, the expression from before is reduced to the much simpler:
$$\tilde{H}=\frac{\Omega V_0}{2}s(t)\Bigl(-\mathcal{I}\sigma_x+\mathcal{Q}\sigma_y\Bigl).$$
In general, this expression is the only thing we need to design the quantum gates. From here, we can choose different amplitudes $\left(V_0\right)$ or waveforms $\left(s(t)\right)$ for the pulse, in order to control the dynamics of the driving.
For example, let's work with the amplitude envelope $s(t)=1$ -also called constant pulse- and no initial phase, meaning that $\mathcal{I}=1$ and $\mathcal{Q}=0$. The Hamiltonian for this particular case is:
$$\tilde{H}=-\frac{\Omega V_0}{2}\sigma_x,$$
that determines an unitary evolution of the system that corresponds to a rotation over the $X$ axis of the Bloch sphere, an $R_X$ gate:
$$U(t)=\exp\left(i\frac{\Omega V_0}{2\hbar}t\cdot\sigma_x\right)=\exp\left(i\frac{\Omega_R}{2}t\cdot\sigma_x\right)=R_X\left(-\Omega_R t\right).$$
In other words, we have implemented a parametrized rotation on the qubit state that depends on some tunnable parameters.
This rotation is the physical representation of the Rabi oscillation we saw before. If we compute the probability of the computational states over time -starting from $\ket{0}$- we get the expected oscillatory behaviour between them:
$$\begin{align*} P_0(t)=\frac{1}{2}\left(1+\cos\left(\Omega_R t\right)\right),\quad P_1(t)=\frac{1}{2}\left(1-\cos\left(\Omega_R t\right)\right). \end{align*}$$
If we wanted to apply an X gate -disregarding global phases- we would just need to choose the appropiate pulse, so the Rabi oscillation stops when the desired populations are found:
$$\Omega_R T = \pi\Rightarrow V_0 T = \frac{\hbar\pi}{\Omega},$$
where $V_0$ -amplitude- and $T$ -duration- are the parameters of the pulse we have control over, and the rest depend on the system and are usually not known. In practice, we have to equivalent options:
Fix the pulse amplitude $V_0$ and calibrate the appropiate duration by sweeping $T$.
Fix the pulse duration $T$ and calibrate the appropiate amplitude by sweeping $V_0$.
2.3. Rabi oscillations with OpenPulse¶
Now that we have the theory, let's start with the hands-on introduction to its implementation with OpenPulse. The first step will be setting up the basic instructions for custom pulse configuration, then, we will develop an experiment to measure this Rabi oscillations in Qmio.
Custom pulses¶
Below there is a basic OpenPulse instruction block for a custom pulse, that recreates the situation described in last section:
OPENQASM 3;
defcalgrammar "openpulse";
cal {
extern frame q8_drive;
waveform wf = constant(1000dt, 0.15);
}
defcal custom_pulse $8{
play(q8_drive, wf);
}
custom_pulse $8;
measure $8;
Let's break it down step-by-step:
OPENQASM 3: specifies to the backend interpreter the language the instruction block uses.defcalgrammar "openpulse": declares the grammar used in the calibration blocks:calanddefcal.calblocks: used to access all the relevant system information, such as accessing qubit frames, declaring pulse shapes or other variables used indefcalblocks.defcalblocks: implement the custom pulse schedule, they are targeted to specific qubits and can be invoked at any time -after its declaration- on the main instruction code.extern frame q8_drive: imports the frame of the physical qubit 8, including its resonant frequency from the calibrations.waveform wf = constant(900dt, 0.15): defines awaveformtype variable, which contains the information for the amplitude, duration and amplitude envelope of the pulse. This is where we can configure our pulse and change its effect. In this case, the waveform is constant $\left(s(t)=1\right)$ with a duration fo 900 time units -for Qmio $dt=0.5$ns- and an amplitude of 0.15 -limited between 0.0 and 1.0- that recreates the pulse described in laste section for some arbitrary parameters.play(q8_drive, wf): used to send the pulse instruction to the qubit, specifying its frame of effect and waveform. Notice that the driving frequency is set by the chosen frame, in this case it corresponds to the resonance frequency of qubit 21, as we directly imported it from calibrations.custom_pulse $8: invokes the custom pulse instructions declared in thedecal.measure $8: invokes a default measurement instruction determined by the hardware provider, it returns a real value that allows to compute the discriminated binary output.
This instructions implement a constant envelope pulse for some arbitrary duration and amplitude equivalent to the situation we studied before. Keep in mind that by seting some duration, we are only seeing one point in time of the Rabi oscillation, and as we have no yet calibrated the pulse, it may not be the one that totally inverts the populations (the X gate).
Let's run this setup just to see how to interact with this instruction blocks in Python. First, we import the required libraries -remember that you need to set your Qmio workspace beforehand:
from qmio import QmioRuntimeService
import numpy as np
import matplotlib.pyplot as plt
%matplotlib inline
service = QmioRuntimeService()
We now define the OpenPulse instruction block inside a string:
instruction = '''
OPENQASM 3;
defcalgrammar "openpulse";
cal {
extern frame q8_drive;
waveform wf = constant(900dt, 0.15);
}
defcal custom_pulse $8{
play(q8_drive, wf);
}
custom_pulse $8;
measure $8;
'''
Now we run the instructions on Qmio's QPU, this is the recommended used to be able to debug errors in the instructions:
shots = 1000 # Number of repetitions for the instructions
with service.backend(name = "qpu") as backend: # Get the Qmio QPU backend
res = backend.run(circuit = instruction, shots = shots, res_format = 'raw') # Send the instructions and retrieve results
try :
res = res["results"].reshape((shots,)) # Access numerical values from the job result
except :
print(res) # Print output message in case of error during instruction execution
Waiting for resources Job started
Now we have the output measurement for the qubit state after the pulse application. Let's understand what this output means by plotting its histogram:
plt.hist(res, bins = 30)
(array([ 2., 1., 2., 10., 9., 17., 32., 45., 53., 48., 58., 48., 43.,
25., 29., 36., 41., 51., 69., 77., 76., 51., 66., 48., 27., 17.,
9., 5., 4., 1.]),
array([-2.99370192, -2.79647398, -2.59924604, -2.40201809, -2.20479015,
-2.00756221, -1.81033427, -1.61310633, -1.41587839, -1.21865045,
-1.02142251, -0.82419456, -0.62696662, -0.42973868, -0.23251074,
-0.0352828 , 0.16194514, 0.35917308, 0.55640102, 0.75362896,
0.95085691, 1.14808485, 1.34531279, 1.54254073, 1.73976867,
1.93699661, 2.13422455, 2.33145249, 2.52868043, 2.72590838,
2.92313632]),
<BarContainer object of 30 artists>)
In each execution of the instructions, one real value is retrieved -usually in [-3,3]- and by repetition we get a distribution that depends on the qubit state in the moment of the measurement. These values are generally distributed as asymetric gaussians around some value:
Positive for measurements from the state $\ket{0}$.
Negative for measurements from the state $\ket{1}$.
This two distributions may present a great overlapping in values near 0, that increases the error for the classification. What we are seing on the output for this instruction is the two distributions at the same time, with different populations. This happens because the pulse has changed the qubit state to a superposition of $\ket{0}$ and $\ket{1}$, so in each shot of the execution we have a probability to measure each of these states, as expected for an arbitrary $R_X$ rotation.We can retrieve the population for each state by just counting the clasificated bits measured over all the executions:
p1 = len(np.where(res<0)[0])/shots
p0 = 1 - p1
print(f'P0 = {p0:0.3f}')
print(f'P1 = {p1:0.3f}')
P0 = 0.573 P1 = 0.427
Rabi oscillation and the X gate¶
Unfortunatetly, we can only measure once per execution, so in order to observe the Rabi oscillation we need to build a specific instruction for each amplitude. This can be achieved by sweeping the amplitude of the applied pulse, executing a different instruction for each. There are many ways to implement this interaction with Python, the following was chosen to be very accessible to change the parameters of the instruction:
qubit = 8
duration = 500
def build_instruction(_amp):
_instruction = f'''
OPENQASM 3;
defcalgrammar "openpulse";
cal {{
extern frame q{qubit}_drive;
waveform wf = constant({duration}dt, {_amp});
}}
defcal custom_pulse ${qubit}{{
play(q{qubit}_drive, wf);
}}
custom_pulse ${qubit};
measure ${qubit};
'''
return _instruction
def run_instruction(_backend, _instruction, _shots):
_res = _backend.run(circuit = _instruction, shots = _shots, res_format = 'raw')
try :
return _res["results"].reshape((_shots,))
except :
print(_res)
def plot_data(_amps, _p1):
plt.plot(_amps, _p1)
plt.xlabel(r'Amplitude')
plt.ylabel(r'$P_1$')
amps = np.linspace(0.0, 0.2, 100)
shots = 700
p1 = []
with service.backend(name = "qpu") as backend:
for a in amps:
instruction = build_instruction(a)
res = run_instruction(backend, instruction, shots)
p1.append(len(np.where(res<0)[0])/shots)
plot_data(amps, p1)
Waiting for resources Job started
In the image we can clearly see the oscillations of the populations when we change the amplitude of the pulse, a calibrated X gate would consist in applying a pulse with the fixed duration -in this case 500dt- and the amplitude where the maximum population for $\ket{1}$ was found.
Let's find this optimal amplitude keeping it with a lower value -later we will wee why- and modify the instructions from before to implement the X gate.
amp_cal = amps[np.where(p1[:15]==np.max(p1[:15]))][0]
fid_cal = np.array(p1)[np.where(amps == amp_cal)][0]
print(f'Calibration duration: {duration}dt')
print(f'Calibrated amplitude: {amp_cal:0.5f}')
print(f'Calibration fidelity: {fid_cal:0.5f}')
Calibration duration: 500dt Calibrated amplitude: 0.02222 Calibration fidelity: 0.92429
Xgate = f'''
OPENQASM 3;
defcalgrammar "openpulse";
cal {{
extern frame q{qubit}_drive;
waveform wf = constant({duration}dt, {amp_cal});
}}
defcal custom_pulse ${qubit}{{
play(q{qubit}_drive, wf);
}}
custom_pulse ${qubit};
measure ${qubit};
'''
shots = 1000
with service.backend(name = "qpu") as backend:
res = backend.run(circuit = Xgate, shots = shots, res_format = 'raw')
try :
res = res["results"].reshape((shots,))
except :
print(res)
plt.hist(res, bins = 30)
Waiting for resources Job started
(array([ 1., 1., 5., 4., 15., 17., 41., 68., 70., 110., 95.,
108., 106., 99., 81., 54., 36., 23., 10., 11., 12., 1.,
8., 8., 5., 4., 4., 0., 1., 2.]),
array([-3.14380488, -2.9644446 , -2.78508432, -2.60572403, -2.42636375,
-2.24700347, -2.06764319, -1.88828291, -1.70892262, -1.52956234,
-1.35020206, -1.17084178, -0.9914815 , -0.81212122, -0.63276093,
-0.45340065, -0.27404037, -0.09468009, 0.08468019, 0.26404047,
0.44340076, 0.62276104, 0.80212132, 0.9814816 , 1.16084188,
1.34020217, 1.51956245, 1.69892273, 1.87828301, 2.05764329,
2.23700357]),
<BarContainer object of 30 artists>)
In the figure we see that most of the measurements seem to correspond to the $\ket{1}$ state, as expected from the calibration that yielded a 0.824 population for the excited state. There are many techniques to enhance the performance -increasing the fidelity- of this gates, like finding some optimal values for amplitude and duration that reduce side effects.
2.4. Pulse waveforms¶
In last section we used a pulse with a constant amplitude envelope, meaning $s(t)=1$, because it's the most simple case and the time evolution is easily solvable. Nevertheless, it's not the only -nor the best- option, as it has some unwanted interaction due to a weak anharmonicity. Effectively, we could use any shape for the pulse, as the Rabi oscillation will depend only on its integral.
There are two main options to reduce the high-level interactions:
Gaussian pulses: it can be proven by simulation that gaussian pulses reduce the off-resonance interactions due to weak anharmonicity. They consist on a pulse that follows a truncated gaussian distribution, with the maximum value at half the duration of the pulse. Gaussian pulses have three parameters: amplitude, duration and standard deviation. It's common to calibrate them by fixing the duration and standard deviation. The general expression for the envelope is: $s(t)=e^{-\frac{(t-T/2)^2}{2\sigma^2}},$ where $A$ is the amplitude, $T$ is the pulse duration and $\sigma$ is the standard deviation.
DRAG (Derivative Reduction by Adiabatic Gate) pulses: although gaussian pulses reduce this effects, the do not remove them, generating leakage out of the computational subspace and inducing phase shifts from the theorical for the gate. The DRAG protocol uses a diferent envelope for the two pulse components $\left(\mathcal{I}\text{ and }\mathcal{Q}\right)$, it's characterized by a correction parameter $\beta$ and a -usually- gaussian envelope $s(t)$: $$ s'(t)=\begin{cases} s(t),\ \text{on } \mathcal{I},\\ \\ -\beta\dot{s}(t),\ \text{on } \mathcal{Q}. \end{cases} $$
DRAG pulses aim to correct the side effect of the in-phase pulse -which performs the gate- by applying an out-of-phase correctio pulse at the same time. There are also other techniques that take into account some detunng terms in order to reduce simultaneously both of the effects we talked about.
OpenPulse lets us configure this envelopes -also called waveforms- and even has some of the most common ones already implemented. For example, the two we talked before can be declared as follows:
gaussian(amplitude, duration, sigma)drag(amplitude, duration, sigma, beta)
Gaussian X gate calibration¶
The gaussian pulse calibration to implement an X gate is similar to the constant, we just need to set a standard deviation for the envelope. This $\sigma$ can also be calibrated to find an optimal duration-width ratio for the gate. Keep in mind that there are limits to how small or large these parameters can be -the backend will raise an error if exceeded- but as a rule of thumb, the gaussian width should not be smaller that 6ns (64dt). Also, a big width compared to the duration will lead to an almost constant pulse.
qubit = 8
duration = 500
sigma = 64
def build_gaussian(_amp):
_instruction = f'''
OPENQASM 3;
defcalgrammar "openpulse";
cal {{
extern frame q{qubit}_drive;
waveform wf = gaussian({_amp}, {duration}dt, {sigma}dt);
}}
defcal custom_pulse ${qubit}{{
play(q{qubit}_drive, wf);
}}
custom_pulse ${qubit};
measure ${qubit};
'''
return _instruction
amps = np.linspace(0.0, 0.2, 100)
shots = 700
p1 = []
with service.backend(name = "qpu") as backend:
for a in amps:
instruction = build_gaussian(a)
res = run_instruction(backend, instruction, shots)
p1.append(len(np.where(res<0)[0])/shots)
plot_data(amps, p1)
Waiting for resources Job started
amp_cal = amps[np.where(p1[:50]==np.max(p1[:50]))][0]
fid_cal = np.array(p1)[np.where(amps == amp_cal)][0]
print(f'Gaussian calibration duration: {duration}dt')
print(f'Gaussian calibration sd: {sigma}dt')
print(f'Gaussian calibrated amplitude: {amp_cal:0.5f}')
print(f'Gaussian calibration fidelity: {fid_cal:0.5f}')
Gaussian calibration duration: 500dt Gaussian calibration sd: 100dt Gaussian calibrated amplitude: 0.04646 Gaussian calibration fidelity: 0.93143
2.5. Single-qubit gates¶
For now, we have only implemented the X gate because its easy to observe it effects. Quantum computers use a small set of native gates to implement any other unitary trassformation, and for Qmio's case these are: the SX, RZ and ECR (for two-qubit interactions). For now, we'll focus on how we can implement any single-qubit gate by just applying pulses configured to perform the SX gate and manipulating the qubit relative phase to virtually implement the RZ gates.
SX gate¶
The SX or $\sqrt{X}$ gate is a rotation $R_X(\pi/2)$ -disregarding global phases- that can be achieved by just applying a constant pulse of half the amplitude -or duration- of the calibrated X gate. Let's use the calibration from before to observe the result:
qubit = 8
amp_cal = 0.02222
duration = 500
SXgate = f'''
OPENQASM 3;
defcalgrammar "openpulse";
cal {{
extern frame q{qubit}_drive;
waveform wf = constant({duration/2}dt, {amp_cal});
}}
defcal custom_pulse ${qubit}{{
play(q{qubit}_drive, wf);
}}
custom_pulse ${qubit};
measure ${qubit};
'''
shots = 5000
with service.backend(name = "qpu") as backend:
res = backend.run(circuit = SXgate, shots = shots, res_format = 'raw')
try :
res = res["results"].reshape((shots,))
except :
print(res)
plt.hist(res, bins = 40)
Waiting for resources Job started
(array([ 1., 0., 4., 4., 13., 15., 33., 62., 63., 129., 148.,
194., 234., 252., 230., 211., 184., 156., 117., 131., 121., 109.,
140., 208., 178., 228., 270., 276., 271., 267., 220., 179., 133.,
101., 57., 32., 15., 9., 3., 2.]),
array([-3.25361505, -3.09732903, -2.94104301, -2.78475699, -2.62847097,
-2.47218495, -2.31589893, -2.15961291, -2.00332688, -1.84704086,
-1.69075484, -1.53446882, -1.3781828 , -1.22189678, -1.06561076,
-0.90932474, -0.75303872, -0.5967527 , -0.44046668, -0.28418066,
-0.12789464, 0.02839138, 0.1846774 , 0.34096342, 0.49724944,
0.65353546, 0.80982148, 0.9661075 , 1.12239353, 1.27867955,
1.43496557, 1.59125159, 1.74753761, 1.90382363, 2.06010965,
2.21639567, 2.37268169, 2.52896771, 2.68525373, 2.84153975,
2.99782577]),
<BarContainer object of 40 artists>)
The result is an distribution that contains an almost equiprobable distibution -remember that the fidelity for our gate was not perfect- that corresponds to the desired superposition state.
Virtual RZ gate¶
The RZ gate implementation is rather different, as we don't need to apply any pulse. When we talked about the qubit frame, we saw that there is an intrinsic rotation over the Z axis that we corrected in order to apply the pulses in the "timeless" frame. We can change it by adding some relative phase and apply the same pulse in this new frame, resulting in a pulse that also applies the RZ.
For example, we want to apply an RY gate, we can decompose it in the following RX and RZ rotations:
$$R_Y(\theta)=R_Z(-\pi/2)R_X(\theta)R_Z(\pi/2),$$
which can be implemented by changing the phase of the frame by $\pi/2$ and the applying the pulse for the RX gate. The final RZ does not change the measurement outcome, because it doesn't change the projection of the state over the Z axis, so we don't need to implement it, unless there are other gates after.
Let's see how we would desing this gate with OpenPulse. There are two ways to apply phase changes to the qubit frame:
set_phase(frame, angle): it changes the total phase of the frame to the input value. It's useful to reset the frame phase to 0 if needed or if you want to keep track of the total phase.shift_phase(frame, angle): it shifts the phase of the frame by the input value. This is equivalent to applying a RZ over the qubit's Z axis.
These functions can only be called in calibration blocks (cal and defcal).
As we already have $R_X(\pi/2)$ calibrated, we are going to apply an $R_Y(\pi/2)$:
qubit = 8
amp_cal = 0.02222
duration = 500
angle = np.pi/2
RYgate = f'''
OPENQASM 3;
defcalgrammar "openpulse";
cal {{
extern frame q{qubit}_drive;
waveform wf = constant({duration/2}dt, {amp_cal});
}}
defcal custom_pulse ${qubit}{{
shift_phase(q{qubit}_drive, {angle});
play(q{qubit}_drive, wf);
}}
custom_pulse ${qubit};
measure ${qubit};
'''
shots = 5000
with service.backend(name = "qpu") as backend:
res = backend.run(circuit = RYgate, shots = shots, res_format = 'raw')
try :
res = res["results"].reshape((shots,))
except :
print(res)
plt.hist(res, bins = 40)
Waiting for resources Job started
(array([ 4., 2., 2., 4., 6., 23., 38., 69., 113., 139., 180.,
184., 223., 238., 231., 208., 193., 140., 131., 125., 122., 139.,
167., 194., 225., 271., 278., 303., 264., 233., 190., 137., 98.,
53., 38., 24., 6., 3., 1., 1.]),
array([-3.17572033, -3.01858032, -2.86144032, -2.70430031, -2.5471603 ,
-2.3900203 , -2.23288029, -2.07574029, -1.91860028, -1.76146028,
-1.60432027, -1.44718026, -1.29004026, -1.13290025, -0.97576025,
-0.81862024, -0.66148024, -0.50434023, -0.34720023, -0.19006022,
-0.03292021, 0.12421979, 0.2813598 , 0.4384998 , 0.59563981,
0.75277981, 0.90991982, 1.06705983, 1.22419983, 1.38133984,
1.53847984, 1.69561985, 1.85275985, 2.00989986, 2.16703987,
2.32417987, 2.48131988, 2.63845988, 2.79559989, 2.95273989,
3.1098799 ]),
<BarContainer object of 40 artists>)
Universal single-qubit gate¶
It can be proven that any single-qubit gate can be expressed as the following parametrized unitary:
$$\begin{align*} \mathcal{U}(\theta,\varphi,\lambda)&=\begin{pmatrix} \cos(\theta/2) & -e^{i\lambda}\sin(\theta/2)\\ e^{i\varphi}\sin(\theta/2) & e^{i(\varphi+\lambda)}\cos(\theta/2) \end{pmatrix}\\ &=R_Z\left(\varphi-\frac{\pi}{2}\right)R_X\left(\frac{\pi}{2}\right)R_Z\left(\pi-\theta\right)R_X\left(\frac{\pi}{2}\right)R_Z\left(\lambda-\frac{\pi}{2}\right). \end{align*} $$
This means that we can apply any single qubit quantum gate with just SX gates -equivalent to the $R_X(\pi/2)$ except for a global phase- and the parametrized virtual $R_Z$.
For example, let's apply the $R_X(\pi/4)$ rotation with the calibrated SX from before, de decomposition of the gate is:
$$R_X(\theta)=R_Z(-\pi)R_X(\pi/2)R_Z(\pi-\theta)R_X(\pi/2)R_Z(0)\equiv SX\cdot R_Z(\pi-\theta)\cdot SX$$
qubit = 8
amp_cal = 0.02222
duration = 500
angle = np.pi - np.pi/4
RYgate = f'''
OPENQASM 3;
defcalgrammar "openpulse";
cal {{
extern frame q{qubit}_drive;
waveform wf = constant({duration/2}dt, {amp_cal});
}}
defcal custom_pulse ${qubit}{{
play(q{qubit}_drive, wf);
shift_phase(q{qubit}_drive, {angle});
play(q{qubit}_drive, wf);
}}
custom_pulse ${qubit};
measure ${qubit};
'''
shots = 5000
with service.backend(name = "qpu") as backend:
res = backend.run(circuit = RYgate, shots = shots, res_format = 'raw')
try :
res = res["results"].reshape((shots,))
except :
print(res)
plt.hist(res, bins = 40)
Waiting for resources Job started
(array([ 2., 2., 2., 2., 13., 15., 16., 23., 29., 46., 58.,
67., 77., 93., 97., 76., 59., 75., 101., 98., 136., 129.,
172., 205., 249., 303., 338., 391., 412., 383., 337., 283., 249.,
163., 129., 86., 44., 23., 11., 6.]),
array([-3.05784742, -2.91195238, -2.76605735, -2.62016232, -2.47426728,
-2.32837225, -2.18247722, -2.03658218, -1.89068715, -1.74479211,
-1.59889708, -1.45300205, -1.30710701, -1.16121198, -1.01531694,
-0.86942191, -0.72352688, -0.57763184, -0.43173681, -0.28584178,
-0.13994674, 0.00594829, 0.15184333, 0.29773836, 0.44363339,
0.58952843, 0.73542346, 0.88131849, 1.02721353, 1.17310856,
1.3190036 , 1.46489863, 1.61079366, 1.7566887 , 1.90258373,
2.04847877, 2.1943738 , 2.34026883, 2.48616387, 2.6320589 ,
2.77795393]),
<BarContainer object of 40 artists>)
2.6. Other examples¶
High amplitude side-effects¶
The pulse can produce some undesires side effects if its amplitude is big enough: reflections, enhancing the weak anharmonicity effects, Mollow peaks... In general, this will result in a loss of coherence due to non-unitary evolutions in the qubit, meaning that the system is interacting with high-energy levels, the resonator or even other qubits. We can see the result od these effects if we repeat the calibration for the constant pulse at higher amplitudes.
amps = np.linspace(0.0, 1.0, 400)
shots = 700
p1 = []
with service.backend(name = "qpu") as backend:
for a in amps:
instruction = build_instruction(a)
res = run_instruction(backend, instruction, shots)
p1.append(len(np.where(res<0)[0])/shots)
plot_data(amps, p1)
Waiting for resources Waiting for resources Job started
Calibration with fixed amplitude¶
We have only calibrated the pulses by sweeping the amplitude, having ser beforehand some values for the time parameters such as the duration or standard deviation. The theory tells us that calibrating the amplitude and the duration is equivalent, because the relevant effect is due to the energy integral of the pulse. But the time discretization performed by the DAG converters makes que duration calibration less convenient, let's see how this calibration would look like
qubit = 8
amplitude = 0.02828
def build_instruction_time(_time):
_instruction = f'''
OPENQASM 3;
defcalgrammar "openpulse";
cal {{
extern frame q{qubit}_drive;
waveform wf = constant({_time}dt, {amplitude});
}}
defcal custom_pulse ${qubit}{{
play(q{qubit}_drive, wf);
}}
custom_pulse ${qubit};
measure ${qubit};
'''
return _instruction
def plot_time_data(_times, _p1):
plt.plot(_times, _p1)
plt.xlabel(r't [dt]')
plt.ylabel(r'$P_1$')
Keep in mind that the minimum duration time for the pulse is 32ns (64dt).
times = np.linspace(64, 2000, 500)
shots = 700
p1 = []
with service.backend(name = "qpu") as backend:
for t in times:
instruction = build_instruction_time(t)
res = run_instruction(backend, instruction, shots)
p1.append(len(np.where(res<0)[0])/shots)
plot_time_data(times, p1)
Waiting for resources Waiting for resources Waiting for resources Job started
References¶
- [1] P. Krantz, M. Kjaergaard, F. Yan, T. P. Orlando, S. Gustavsson, W. D. Oliver; A quantum engineer's guide to superconducting qubits. Appl. Phys. Rev. 1 June 2019; 6 (2): 021318. https://doi.org/10.1063/1.5089550