Part I: Introduction to Qiskit (Cont.)¶
Welcome to Qiskit! Before starting with the exercises, please run the cell below by pressing 'shift' + 'return'. You can run the other following cells in the same way.
import numpy as np
from numpy import pi
# Importing standard Qiskit libraries
from qiskit import QuantumCircuit, transpile, assemble, Aer, IBMQ, execute
from qiskit.quantum_info import Statevector
from qiskit.visualization import plot_bloch_multivector, plot_histogram
#from qiskit_textbook.problems import dj_problem_oracle
I.1: Quantum Circuits Using Multi-Qubit Gates¶
Great job! Now that you've understood the single-qubit gates, let us look at gates on multiple qubits. Check out this chapter if you would like to refresh the theory: https://qiskit.org/textbook/ch-gates/introduction.html
The basic gates on two and three qubits are given by
qc.cx(c,t) # controlled-X (= CNOT) gate with control qubit c and target qubit t
qc.cz(c,t) # controlled-Z gate with control qubit c and target qubit t
qc.ccx(c1,c2,t) # controlled-controlled-X (= Toffoli) gate with control qubits c1 and c2 and target qubit t
qc.swap(a,b) # SWAP gate that swaps the states of qubit a and qubit b
We start with an easy gate on two qubits, the controlled-NOT (also CNOT) gate. The CNOT gate has no effect when applied on two qubits in state $|0\rangle$, but this changes if we apply a Hadamard gate before to the control qubit to bring it in superposition. This way, we can create entanglement. The resulting state is one of the so-called Bell states. There are four Bell states in total, so let's try to also construct another one:
Simple but very important quantum circuits¶
In the lectures we 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:
# First we need more 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
from numpy import *
bell_op = Operator(bell)
array_to_latex(bell_op)
#Validate your answer
import qiskit.quantum_info as qi
Remark 1:¶
Compare this operator with Hadamard operator on a single qubit. What do you observe?
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:
def lab2_ex1():
# This time, we not only want two qubits, but also two classical bits for the measurement
qft = QuantumCircuit(2)
#
#
# FILL YOUR CODE IN HERE
#
#
return qft
lab2_ex1().draw(output="mpl") # we draw the circuit
# Compute the qft statevector
qc = QuantumCircuit(2, 2)
qc = lab2_ex1()
stv2 = qi.Statevector.from_instruction(qc)
stv2.draw('latex', prefix='Statevector1:')
Remark 2:¶
Compare this operator with Bell circuit above and below. What do you observe?
def lab2_ex3():
# This time, we not only want two qubits, but also two classical bits for the measurement
qc = QuantumCircuit(2, 2)
#
#
# FILL YOUR CODE IN HERE
#
#
return qc
qc = lab2_ex3()
qc.draw(output="mpl") # we draw the circuit
Let us now also add a measurement to the above circuit so that we can execute it (using the simulator) and plot the histogram of the corresponding counts.
qc.measure_all() # we measure all the qubits
backend = Aer.get_backend('qasm_simulator') # we choose the simulator as our backend
counts = execute(qc, backend, shots = 1000).result().get_counts() # we run the simulation and get the counts
plot_histogram(counts) # let us plot a histogram to see the possible outcomes and corresponding probabilities
As you can see in the histogram, the only possible outputs are "01" and "10", so the states of the two qubits are always perfectly anti-correlated.
#Validate your answer
def lab2_ex4():
# This time, we need 3 qubits and also add 3 classical bits in case we want to measure
qc = QuantumCircuit(3,3)
#
#
# FILL YOUR CODE IN HERE
#
return qc
qc = lab2_ex4()
qc.draw(output="mpl") # we draw the circuit
We can now also measure this circuit the same way we did before.
qc.measure_all() # we measure all the qubits
backend = Aer.get_backend('qasm_simulator') # we choose the simulator as our backend
counts = execute(qc, backend, shots = 1000).result().get_counts() # we run the simulation and get the counts
plot_histogram(counts) # let us plot a histogram to see the possible outcomes and corresponding probabilities
#Validate your answer
Congratulations for finishing these introductory exercises!
Part II: Quantum Circuits and Complexity¶
II.1: Complexity of the Number of Gates¶
We have seen different complexity classes and the big $O$ notation and how it can be used with Quantum Algorithms, when using gates. One possible way to calculate the complexity of an algorithm is to just count the number of gates used.
Another often seen measure is to count the number of multi qubit gates rather than all gates, since they are normally "more expensive" than other gates. In our case "more expensive" means that they often have a way higher error rate (around 10 times higher) compared to single qubit gates.
So, lets look again at the GHZ state and count the number of gates as well as the number of multi qubit gates:
qc = QuantumCircuit(3)
qc.h(0)
qc.cx(0,1)
qc.cx(0,2)
print(qc.size())
print(qc.num_nonlocal_gates())
3 2
Of course, in this example the number of gates is three and the number of multi qubit gates is 2, this might be obvious in this case, since we added gate by gate ourselves, but it will be less obvious when we use algorithms to construct our quantum circuits.
II.1.1: Quantum Fourier Transform Example¶
Let’s look at the example of the Quantum Fourier transform which was also shown in the lecture. If you want to learn more about it or if you want to refresh your knowledge you can read the following chapter: https://qiskit.org/textbook/ch-algorithms/quantum-fourier-transform.html
def qft_rotations(circuit, n):
"""Performs qft on the first n qubits in circuit (without swaps)"""
if n == 0:
return circuit
n -= 1
circuit.h(n)
for qubit in range(n):
circuit.cp(pi/2**(n-qubit), qubit, n)
# At the end of our function, we call the same function again on
# the next qubits (we reduced n by one earlier in the function)
qft_rotations(circuit, n)
def swap_registers(circuit, n):
"""Swaps registers to match the definition"""
for qubit in range(n//2):
circuit.swap(qubit, n-qubit-1)
return circuit
def qft(circuit, n):
"""QFT on the first n qubits in circuit"""
qft_rotations(circuit, n)
swap_registers(circuit, n)
return circuit
For now, let's not do the whole Quantum Fourier Transformation but only the rotations. And let’s apply them to the quantum state we defined above and measure the number of operations used:
qft_rotations(qc,3)
print(qc.size())
print(qc.num_nonlocal_gates())
qc.draw(output="mpl")
9 5
As we can see, the first 3 gates are from our GHZ state and the other 6 gates are coming from the Fourier transformation, and it looks the same no matter on which state we apply it, it will always take the same amount of gates. This means that we can now for the next examples just consider the all 0 state (the base state which needs no gates to construct) and just consider the number of gates of the Fourier transformation itself.
In the textbook we can use the scalable circuit widget to see how the circuit for the Fourier transformation gets bigger as we apply it to more circuits. See here: https://qiskit.org/textbook/ch-algorithms/quantum-fourier-transform.html#8.2-General-QFT-Function-
NOTE: We do ask for the total number of gates not the number of multi qubit gates. (Although this can be easily calculated as well, if you have found the solution for the total number of gates).
def lab2_ex5(n:int) -> int:
#Here we want you to build a function calculating the number of gates needed by the fourier rotation for n qubits.
numberOfGates=0
#
# FILL YOUR CODE IN HERE
# TRY TO FIND A SIMPLE NON RECURSIVE FUNCTION
#
return numberOfGates
print(lab2_ex5(3))
print(lab2_ex5(4))
print(lab2_ex5(5))
print(lab2_ex5(10))
print(lab2_ex5(100))
print(lab2_ex5(200))
0 0 0 0 0 0
# Remark
# Note that the print is expecting as input a function!
#(And the function takes n as an input and outputs the number of gates constructed)
As you have seen above, the algorithm for the Quantum Fourier Transform needs $O(n^2)$ gates and one can also easily see that it also needs $O(n^2)$ two qubit gates.
If the algorithm would not have been recursive, and instead just uses several loops, this would have been even easier to see.
So, if you ever have problems analysing the complexity of a (recursive) algorithm, try to rewrite it using simple loops.
II.2: Complexity of the Depth of a Circuit¶
When it comes to how well a circuit runs on an actual Quantum Computer the number of gates is not the only important factor.
The depth of the circuit tells how many "layers" of quantum gates, executed in parallel, it takes to complete the computation defined by the circuit. More information about it can be found here, especially the animation comparing it to Tetris can help to understand the concept of depth (open "Quantum Circuit Properties" to see it): https://qiskit.org/documentation/apidoc/circuit.html#supplementary-information
Now we look at two simple examples to show what the depth of a circuit is:
qc = QuantumCircuit(4)
qc.h(0)
qc.s(0)
qc.s(0)
qc.s(0)
qc.cx(0,1)
qc.cx(1,3)
print(qc.depth())
qc.draw(output="mpl")
6
qc2 = QuantumCircuit(4)
qc2.h(0)
qc2.s(1)
qc2.cx(0,1)
qc2.s(2)
qc2.s(3)
qc2.cx(2,3)
print(qc2.depth())
qc2.draw(output="mpl")
2
The length of the circuit above corresponds with the width. And as you can see, both circuits have the same number of gates, but the first circuit has a much higher depth, because all the gates depend on the gates before, so nothing can be done in parallel.
In short, the more of the gates can be applied in parallel, because they apply to different qubits, the lower will the depth of a circuit be. The lower bound on the depth of a circuit (if it has only single qubit gates and they are evenly distributed) is the number of gates divided by the number of qubits.
On the other hand, if every gate in a quantum circuit depends on the same qubit, the depth will be the same as the number of qubits.
II.2.1: Fully Entangled State Example¶
Let's take a look at the example of the naive implementation of a fully entangled state:
qc = QuantumCircuit(16)
#Step 1: Preparing the first qubit in superposition
qc.h(0)
#Step 2: Entangling all other qubits with it (1 is included 16 is exclude)
for x in range(1, 16):
qc.cx(0,x)
print(qc.depth())
qc.draw(output="mpl")
16
As we can see the above quantum circuit has its depth equal to its number of gates. Step 1 adds a depth of 1 and step 2 adds a depth of 15.
Hint: Lets think about what kind of asymptotic running time would cause only 4 operations. And don't forget that the final depth will be 5 (Step 1 and 2 combined).
def lab2_ex6():
qc = QuantumCircuit(16) #Same as above
#Step 1: Preparing the first qubit in superposition
qc.h(0)
#
#
# FILL YOUR CODE IN HERE
#
return qc
qc = lab2_ex6()
print(qc.depth())
qc.draw(output="mpl")
1
Congratulation! You just improved the depth of a circuit by a factor of 9 thanks to your understanding of asymptotic complexity and quantum circuits.
II.2.2 Measuring Quantum states¶
In Lab1 we learned how to meassure the $\Phi^+$ or simple + 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}$
plus_state.probabilities_dict()
{'0': 0.4999999999999999, '1': 0.4999999999999999}
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 = ## Add your code here
# Validate your answer with latex
sv_bell.draw('latex')
Refresh your memory that we can get the probability of measuring 00 or 11:
sv_bell.probabilities_dict()
{'00': 0.5000000000000001, '11': 0.5000000000000001}
# run this cell multiple times to show collapsing into one state or the other
res = sv_bell.measure()
res
('11', Statevector([0.+0.j, 0.+0.j, 0.+0.j, 1.+0.j], dims=(2, 2)))
Let us now also add a measurement to the above circuit so that we can execute it (using the simulator) and plot the histogram of the corresponding counts. Make sure your run earlier cell where the same state is used to build a circuit (bell). Repeate the same measurements with different number of shots. What do you observe?
bell.measure_all() # we measure all the qubits
backend = Aer.get_backend('qasm_simulator') # we choose the simulator as our backend
counts = execute(bell, backend, shots = 10000).result().get_counts() # we run the simulation and get the counts
plot_histogram(counts) # let us plot a histogram to see the possible outcomes and corresponding probabilities
Define Unitary operators using widely defined notation.
from qiskit.quantum_info import Pauli, SparsePauliOp
operator = Pauli('X')
# equivalent to:
X = Pauli('X')
operator = X^X
print("As Pauli Op: ", repr(operator))
# another alternative is:
operator = SparsePauliOp('XX')
print("As Sparse Pauli Op: ", repr(operator))
As Pauli Op: Pauli('XX') As Sparse Pauli Op: SparsePauliOp(['XX'], coeffs=[1.+0.j])
from qiskit.quantum_info import Pauli, SparsePauliOp
# Define which will contain the Paulis
pauli_list = []
# Define Paulis and add them to the list
###INSERT CODE BELOW THIS LINE
###DO NOT EDIT BELOW THIS LINE
for pauli in pauli_list:
print(pauli, '\n')
There's a few operations that we can do on operators which are implemented in Pauli, SparsePauliOp. For example, we can rescale an operator by a scalar factor using *
, we can compose operators using @
and we can take the tensor product of operators using ^
. In the following, let us try to use these operations. Note that we need to be careful with the operations' precedences as python evaluates +
before ^
and that may change the intended value of an expression. For example, I^X+X^I
is actually interpreted as I^(X+X)^I=2(I^X^I)
. Therefore the use of parenthesis is strongly recommended to avoid these types of errors. Also, keep in mind that the imaginary unit i is defined as 1j
in Python.
# Define list of ladder operators
ladder_operator_list = []
# Define ladder operators and add the to the list
###INSERT CODE BELOW THIS LINE
from qiskit.opflow import X, Y, Z, I, CX, T, H, S, PrimitiveOp # this is a hint
###DO NOT EDIT BELOW THIS LINE
for ladder_operator in ladder_operator_list:
print(ladder_operator, '\n')
0.5 * X + 0.5j * Y 0.5 * X + -0.5j * Y
/var/folders/wv/1lb9md89697b1ny5b9s5yldc0000gs/T/ipykernel_77961/986244837.py:6: DeprecationWarning: The ``qiskit.opflow`` module is deprecated as of qiskit-terra 0.24.0. It will be removed no earlier than 3 months after the release date. For code migration guidelines, visit https://qisk.it/opflow_migration. from qiskit.opflow import X, Y, Z, I, CX, T, H, S, PrimitiveOp
We can take the operators defined in Qiskit Opflow and translate them into other representation. For example the to_matrix()
method of an Operator object allows us to retrieve the matrix representation of the operator (as a numpy array)
# Define list which will contain the matrices representing the Pauli operators
matrix_sigma_list = []
# Add matrix representation of Paulis to the list
###INSERT CODE BELOW THIS LINE
###DO NOT EDIT BELOW THIS LINE
for matrix_sigma in matrix_sigma_list:
print(matrix_sigma, '\n')
We can also generate a circuit representation of the operator using the to_circuit()
method
# Define a list which will contain the circuit representation of the Paulis
circuit_sigma_list = []
# Add circuits to list
###INSERT CODE BELOW THIS LINE
###DO NOT EDIT BELOW THIS LINE
for circuit in circuit_sigma_list:
print(circuit, '\n')
Let us start to prepare the building blocks we'll need to calculate a quantum system using a quantum computer. First let's define the Hamiltonian of the system. For now let us use a simple example.
$$ \hat{H} = \frac{1}{2} \left( \hat{I}\otimes \hat{I} + \hat{\sigma}_x \otimes \hat{\sigma}_x + \hat{\sigma}_y \otimes \hat{\sigma}_y + \hat{\sigma}_z \otimes \hat{\sigma}_z \right) $$
# Use the formula above to define the Hamiltonian operator
###INSERT CODE BELOW THIS LINE
H =
###DO NOT EDIT BELOW THIS LINE
# Get its matrix representation
H_matrix = H.to_matrix()
print(H_matrix)
# Use your Lab1 to get an easier to see matrix format
First check if $ \hat{H} $ is validate operator:
# Enter your code here
# Then prepare the state |00> and check you are doing the right think!
#One more step to get your answer and validate your result
#Repeat the same procedure for the rest
ket11 =
ket01 =
ket10 =
First, let's create a quantum circuit for time evolution! We'll parametrize the time t
with a Qiskit Parameter
and exponentiate the Heisenberg Hamiltonian with the Qiskit Opflow method exp_i()
which implements the corressponding time-evolution operator $e^{-i \hat{H} t}$
Hint: Define the time evolution operator for the Heisenberg Hamiltonian $\hat{H}$ and the time step $t$
from qiskit.circuit import Parameter
# Define a parameter t for the time in the time evolution operator
t = Parameter('t')
# Follow the instructions above to define a time-evolution operator
###INSERT CODE BELOW THIS LINE
###DO NOT EDIT BELOW THIS LINE
print(time_evolution_operator)
*Hint: First you'll need to instantiate a MatrixEvolution()
object. This object has a method called convert(operator)
which takes a time-evolution operator and generates a quantum circuit implementing the operation. Finally, you'll need to
bind the value of the evolution time to the circuit.
from qiskit.opflow import MatrixEvolution, PauliTrotterEvolution
# Set a total time for the time evolution
evolution_time = 0.5
# Instantiate a MatrixEvolution() object to convert the time evolution operator
# and bind the value for the time parameter
###INSERT CODE BELOW THIS LINE
###DO NOT EDIT BELOW THIS LINE
print(bound_matrix_exponentiation_circuit)
/var/folders/wv/1lb9md89697b1ny5b9s5yldc0000gs/T/ipykernel_77961/529932411.py:9: DeprecationWarning: The class ``qiskit.opflow.evolutions.matrix_evolution.MatrixEvolution`` is deprecated as of qiskit-terra 0.24.0. It will be removed no earlier than 3 months after the release date. For code migration guidelines, visit https://qisk.it/opflow_migration. bound_matrix_exponentiation_circuit = MatrixEvolution() Evolved Hamiltonian is not composed of only MatrixOps, converting to Matrix representation, which can be expensive.
┌──────────────┐ q_0: ┤0 ├ │ Hamiltonian │ q_1: ┤1 ├ └──────────────┘
# Add to a circuit
circ = QuantumCircuit(2, 2)
circ.append(bound_matrix_exponentiation_circuit, [0,1])
circ.measure([0,1], [0,1])
circ.draw('mpl')
We may also compare operators using the process_fidelity function from the Quantum Information module. This is an information theoretic quantity for how close two quantum channels are to each other, and in the case of unitary operators it does not depend on global phase.
print(circ.depth())
2
from qiskit import BasicAer
backend = BasicAer.get_backend('qasm_simulator')
#circ = transpile(circ, backend, basis_gates=['u1','u2','u3','cx'])
circ = transpile(circ, backend)
job = backend.run(circ)
#job.result().get_counts(0)
job.result()
Result(backend_name='qasm_simulator', backend_version='2.1.0', qobj_id='2b3d1171-6741-4869-9bb3-c3cddd8b3ca4', job_id='c1c04a71-fa7b-4d54-8142-f1ffd15e25ad', success=True, results=[ExperimentResult(shots=1024, success=True, meas_level=2, data=ExperimentResultData(counts={'0x0': 1024}), header=QobjExperimentHeader(qubit_labels=[['q', 0], ['q', 1]], n_qubits=2, qreg_sizes=[['q', 2]], clbit_labels=[['c', 0], ['c', 1]], memory_slots=2, creg_sizes=[['c', 2]], name='circuit-166', global_phase=0.0, metadata={}), status=DONE, name='circuit-166', seed_simulator=1809028683, time_taken=0.002635955810546875)], date=None, status=COMPLETED, header=QobjHeader(backend_name='qasm_simulator', backend_version='2.1.0'), time_taken=0.002655029296875)
import qiskit.tools.jupyter
%qiskit_version_table
#import qiskit
#qiskit.__qiskit_version__
Version Information
Qiskit Software | Version |
---|---|
qiskit-terra | 0.24.2 |
qiskit-aer | 0.12.2 |
qiskit-ignis | 0.7.1 |
qiskit-ibmq-provider | 0.20.2 |
qiskit | 0.43.3 |
qiskit-nature | 0.4.2 |
qiskit-machine-learning | 0.4.0 |
System information | |
Python version | 3.9.12 |
Python compiler | Clang 12.0.0 |
Python build | main, Apr 5 2022 01:53:17 |
OS | Darwin |
CPUs | 4 |
Memory (Gb) | 32.0 |
Fri Aug 25 09:21:19 2023 EDT |