Intro to Qiskit¶
The basics of quantum computing and quantum circuits using Qiskit. To try this tutorial out, run the notebook version in IBM Quantum Experience. You can also run this tutorial as a Jupyter notebook using the Qiskit SDK. To set up your environment, follow the installation instructions in the Qiskit tutorials repository.
# required imports:
from qiskit.visualization import array_to_latex
from qiskit.quantum_info import Statevector, random_statevector
from qiskit.quantum_info.operators import Operator, Pauli
from qiskit import QuantumCircuit
from qiskit.circuit.library import HGate, CXGate
import numpy as np
Vectors and Dirac Notation¶
In the lectures you learned different ways of representing quantum states, including how to use bra-ket (Dirac) notation.
Although bra-ket notation cannot be represented exactly in code, we can represent their vector and matrix equivalent with python.
E.g. we can represent $|0\rangle$ using a python list:
ket0 = [[1],[0]]
And we can use one of Qiskit's visualisation tools to make our vectors nicer to look at:
array_to_latex(ket0)
We can do the same with $\langle0|$:
bra0 = [1,0]
array_to_latex(bra0)
ket1 = # your code goes here
bra1 = # your code goes here
Qiskit Statevector
Class¶
In the lectures you learned about using state vectors to represent quantum states. You can represent quantum state vectors in code using Qiskit's Statevector
class.
Qiskit's Statevector
class can take different forms of input (e.g. python list, numpy array, another state vector) to construct a state vector.
Let's take the bra0
object we created earlier and convert it to a Statevector
object:
sv_bra0 = Statevector(bra0)
sv_bra0
The Statevector
class has its own draw()
method:
sv_bra0.draw('latex')
We can create more complex statevectors with multiple qubits like this:
sv_eq = Statevector([1/2, 3/4, 4/5, 6/8])
sv_eq.draw('latex')
Note that the vector above is not a valid state vector as it is not normalised. We can check this with the is_valid()
method:
sv_eq.is_valid()
from qc_grader.challenges.qgss_2023 import grade_lab1_ex2
grade_lab1_ex2(sv_valid)
Qiskit Operator
Class¶
The Operator
class is used in Qiskit to represent matrix operators acting on a quantum system. It has several methods to build composite operators using tensor products of smaller operators, and to compose operators.
One way we can initialise a Qiskit Operator
is by using a python list, like the one we created earlier:
op_bra0 = Operator(bra0)
op_bra0
The Operator class comes with some handy methods for working with operators, for example we can find the tensor product of 2 operators by using the tensor()
method:
op_ket0 = Operator(ket0)
op_bra0.tensor(op_ket0)
We'll use the Operator
and Statevector
classes more in the following exercises.
Inner & Outer Product¶
In the lectures you covered the concepts of the inner and outer product. We can explore these concepts in code using numpy methods .dot()
(the inner product is a generalised form of the dot product) and .outer()
.
For example, we can find the inner product $\langle0|0\rangle$ like this:
braket = np.dot(op_bra0,op_ket0)
array_to_latex(braket)
and the outer product $|0\rangle\langle0|$ like this:
ketbra = np.outer(ket0,bra0)
array_to_latex(ketbra)
braket = np.dot(op_bra0,op_ket0)
array_to_latex(braket)
Note: the numpy methods we used above work with Qiskit Operators as well as regular python lists.
op_bra0 = Operator(bra0)
op_bra1 = Operator(bra1)
op_ket0 = Operator(ket0)
op_ket1 = Operator(ket1)
bra1ket0 = np.dot(op_bra1,op_ket0)
bra0ket1 = # your code goes here
bra1ket1 = # your code goes here
ket1bra0 = # your code goes here
ket0bra1 = # your code goes here
ket1bra1 = # your code goes here
Ex 4 - when the inner product of 2 quantum states is equal to 0, those states are orthogonal. Which of the following states are orthogonal?
a) $\vert 0\rangle$ and $\vert 1\rangle$
b) $\vert 0\rangle$ and $\vert 0\rangle$
c) $\vert 1\rangle$ and $\vert 1\rangle$
# add your answer(s) to this list
answer = []
Deterministic operations¶
As mentioned in the lectures, there are 4 single bit deterministic operations:
f1 = constant-0
f2 = identity
f3 = bit flip / not
f4 = constant-1
$$ \begin{array}{c|c} a & f_1(a)\\ \hline 0 & 0\\ 1 & 0 \end{array} \qquad \begin{array}{c|c} a & f_2(a)\\ \hline 0 & 0\\ 1 & 1 \end{array} \qquad \begin{array}{c|c} a & f_3(a)\\ \hline 0 & 1\\ 1 & 0 \end{array} \qquad \begin{array}{c|c} a & f_4(a)\\ \hline 0 & 1\\ 1 & 1 \end{array} $$
We can create Qiskit Operators for these 4 operations, by passing their matrix representations as arguments to the Operator
class.
E.g. for constant-0 we can create the corresponding matrix m1 like so:
m1 = Operator([[1,1],[0,0]])
array_to_latex(m1)
and similarly for m3:
m3 = Operator([[0,1],[1,0]])
array_to_latex(m3)
We can also use builtin python mutliplication operations (e.g. @
, .dot
, or .matmul
) to check the following equation: $ M|a\rangle = f|a\rangle $
e.g. $ M1|0\rangle = f1|0\rangle $ = 0
array_to_latex(m1@ket0)
m2 = # create an operator for m2 here
m4 = # create and operator for m4 here
Probabilistic operations¶
A Controlled-NOT (or CNOT) operation is a probabilistic operation you can apply on 2 qubits.
Applying a CNOT on a state (X,Y) involves performing a NOT operation on Y when X is 1, otherwise do nothing. X is the control bit, Y is the target bit.
We can implement a CNOT gate (and many other quantum gates) using a class from Qiskit's circuit library:
cnot = CXGate()
array_to_latex(cnot)
Note: this matrix is different from the one that appeared in the lesson because CXGate()
takes the right qubit to be the control rather than the left qubit.
Unitary Operations¶
An operator is unitary if: $ UU^{\dagger} = \mathbb{1} = U^{\dagger} U$
We can check if an operator is Unitary using Qiskit with the is_unitary()
method:
m3.is_unitary()
With small operators like m3 we could probably figure this out easily by ourselves, but with more complex operators it becomes more convenient to use the Qiskit function:
random = Operator(np.array([[ 0.50778085-0.44607116j, -0.1523741 +0.14128434j, 0.44607116+0.50778085j,
-0.14128434-0.1523741j ],
[ 0.16855994+0.12151822j, 0.55868196+0.38038841j, -0.12151822+0.16855994j,
-0.38038841+0.55868196j],
[ 0.50778085-0.44607116j, -0.1523741 +0.14128434j, -0.44607116-0.50778085j,
0.14128434+0.1523741j ],
[ 0.16855994+0.12151822j, 0.55868196+0.38038841j, 0.12151822-0.16855994j,
0.38038841-0.55868196j]]))
random.is_unitary()
non_unitary_op = # your code goes here
Qubit Unitary Operations - Pauli Operations¶
Some of the most common unitary operations in quantum computing are the Pauli operations. Qiskit's Pauli
classes make it easy to interact with Pauli operators in code:
E.g. Pauli X ($\sigma_x$), the bit flip:
pauli_x = Pauli('X')
array_to_latex(pauli_x)
Pauli Y ($\sigma_y$):
pauli_y = Pauli('Y')
array_to_latex(pauli_y)
Pauli Z ($\sigma_z$), the phase flip:
pauli_z = Pauli('Z')
array_to_latex(pauli_z)
We can use the Operator
class with the Pauli
class:
op_x = Operator(pauli_x)
op_x
Let's use the Operator
class and numpy to find the outcome of $\sigma_x|0\rangle$
op_new = np.dot(op_x,ket0)
array_to_latex(op_new)
op_z = # your code goes here
result = # your code goes here
Qubit Unitary Operations - Hadamard¶
The Hadamard gate is one of the most important unitary operations in quantum computing. We can implement a Hadamard gate (and many other quantum gates) using a class from Qiskit's circuit library:
hadamard = HGate()
array_to_latex(hadamard)
You can convert many Qiskit classes to operators to make use of functions specific to the Operator
class, such as is_unitary
hop = Operator(hadamard)
hop.is_unitary()
Quantum Circuits¶
In the lectures you learned how to create a Quantum Circuit using a CNOT and a Hadamard gate. This circuit creates the Bell State $|\phi^+\rangle$. We can implement this using Qiskit's QuantumCircuit
class:
bell = QuantumCircuit(2)
bell.h(0) # apply an H gate to the circuit
bell.cx(0,1) # apply a CNOT gate to the circuit
bell.draw(output="mpl")
If we want to check what the matrix representation is of this quantum state we can convert the circuit directly to an operator:
bell_op = Operator(bell)
array_to_latex(bell_op)
ghz = QuantumCircuit(3)
##############################
# add gates to your circuit here
##############################
ghz.draw(output='mpl')
Measuring Quantum states¶
As explained in the lectures you can find the probability of measurement outcomes by taking the absolute value squared of the entries of a quantum state vector.
For example, when measuring the + state:
$ |+\rangle = \frac{1}{\sqrt2}|0\rangle + \frac{1}{\sqrt2}|1\rangle $
The probability of measuring 0 or 1 is given by the following:
$ Pr(0) = |\frac{1}{\sqrt2}|^2 = \frac{1}{2}$
$ Pr(1) = |\frac{1}{\sqrt2}|^2 = \frac{1}{2}$
Let's create a $|+\rangle$ using the Statevector
class:
plus_state = Statevector.from_label("+")
plus_state.draw('latex')
plus_state
Now we can get the probability of measuring 0 or 1:
plus_state.probabilities_dict()
The dictionary object above shows you all the possible measurement outcomes and what the probability is of getting them. The actual act of measuring forces the state to collapse into either the 0 or 1 state:
# run this cell multiple times to show collapsing into one state or the other
res = plus_state.measure()
res
We can implement the same $|+\rangle$ state with measurement using a quantum circuit:
qc = QuantumCircuit(1,1)
qc.h(0)
qc.measure(0, 0)
qc.draw(output="mpl")
If we ran this circuit using a simulator we would get the same results as we did with the statevector class.
In the next example, let's use the Statevector
class to find the measurement outcomes for a dependent, probabilistic state. We'll find the measurement probilities for the 2-qubit Bell State $|\phi^+\rangle$ :
sv_bell = Statevector([np.sqrt(1/2), 0, 0, np.sqrt(1/2)])
sv_bell.draw('latex')
sv_bell.probabilities_dict()
sv_psi_plus = # create a statevector for |𝜓+⟩ here
prob_psi_plus = # find the measurement probabilities for |𝜓+⟩ here
sv_psi_minus = # create a statevector for |𝜓-⟩ here
prob_psi_minus = # find the measurement probabilities for |𝜓-⟩ here
sv_phi_minus = # create a statevector for |𝜙−⟩ here
prob_phi_minus = # find the measurement probabilities for |𝜙−⟩ here
Final Challenge - generate a QFT circuit¶
The Fourier transform occurs in many different formats throughout classical computing, in areas ranging from signal processing to data compression to complexity theory. The quantum Fourier transform (QFT) is the quantum implementation of the discrete Fourier transform over the amplitudes of a wavefunction. It is part of many quantum algorithms, most notably Shor's factoring algorithm and quantum phase estimation. You'll learn more about this important implementation later on during the Summer School, but for this final challenge of Lab 1 we would like you to use Qiskit to create the following QFT circuit on 2 qubits:
from math import pi
qft = QuantumCircuit(2)
##############################
# add gates to your circuit here
##############################
qft.draw(output='mpl')
To see the matrix that describes the action of this circuit, we can plug the circuit into the Operator
function like this:
U = Operator(qft)
array_to_latex(U)