First, some libraries to load up front
! python -m pip install matplotlib
! python -m pip install qiskit qiskit-aer
Requirement already satisfied: matplotlib in /home/benoit/Teaching/Qnotes/qiskit-env/lib/python3.13/site-packages (3.10.6) Requirement already satisfied: contourpy>=1.0.1 in /home/benoit/Teaching/Qnotes/qiskit-env/lib/python3.13/site-packages (from matplotlib) (1.3.3) Requirement already satisfied: cycler>=0.10 in /home/benoit/Teaching/Qnotes/qiskit-env/lib/python3.13/site-packages (from matplotlib) (0.12.1) Requirement already satisfied: fonttools>=4.22.0 in /home/benoit/Teaching/Qnotes/qiskit-env/lib/python3.13/site-packages (from matplotlib) (4.60.0) Requirement already satisfied: kiwisolver>=1.3.1 in /home/benoit/Teaching/Qnotes/qiskit-env/lib/python3.13/site-packages (from matplotlib) (1.4.9) Requirement already satisfied: numpy>=1.23 in /home/benoit/Teaching/Qnotes/qiskit-env/lib/python3.13/site-packages (from matplotlib) (2.3.3) Requirement already satisfied: packaging>=20.0 in /home/benoit/Teaching/Qnotes/qiskit-env/lib/python3.13/site-packages (from matplotlib) (25.0) Requirement already satisfied: pillow>=8 in /home/benoit/Teaching/Qnotes/qiskit-env/lib/python3.13/site-packages (from matplotlib) (11.3.0) Requirement already satisfied: pyparsing>=2.3.1 in /home/benoit/Teaching/Qnotes/qiskit-env/lib/python3.13/site-packages (from matplotlib) (3.2.5) Requirement already satisfied: python-dateutil>=2.7 in /home/benoit/Teaching/Qnotes/qiskit-env/lib/python3.13/site-packages (from matplotlib) (2.9.0.post0) Requirement already satisfied: six>=1.5 in /home/benoit/Teaching/Qnotes/qiskit-env/lib/python3.13/site-packages (from python-dateutil>=2.7->matplotlib) (1.17.0)
Requirement already satisfied: qiskit in /home/benoit/Teaching/Qnotes/qiskit-env/lib/python3.13/site-packages (2.2.0) Requirement already satisfied: qiskit-aer in /home/benoit/Teaching/Qnotes/qiskit-env/lib/python3.13/site-packages (0.17.2) Requirement already satisfied: rustworkx>=0.15.0 in /home/benoit/Teaching/Qnotes/qiskit-env/lib/python3.13/site-packages (from qiskit) (0.17.1) Requirement already satisfied: numpy<3,>=1.17 in /home/benoit/Teaching/Qnotes/qiskit-env/lib/python3.13/site-packages (from qiskit) (2.3.3) Requirement already satisfied: scipy>=1.5 in /home/benoit/Teaching/Qnotes/qiskit-env/lib/python3.13/site-packages (from qiskit) (1.16.2) Requirement already satisfied: dill>=0.3 in /home/benoit/Teaching/Qnotes/qiskit-env/lib/python3.13/site-packages (from qiskit) (0.4.0) Requirement already satisfied: stevedore>=3.0.0 in /home/benoit/Teaching/Qnotes/qiskit-env/lib/python3.13/site-packages (from qiskit) (5.5.0) Requirement already satisfied: typing-extensions in /home/benoit/Teaching/Qnotes/qiskit-env/lib/python3.13/site-packages (from qiskit) (4.15.0) Requirement already satisfied: psutil>=5 in /home/benoit/Teaching/Qnotes/qiskit-env/lib/python3.13/site-packages (from qiskit-aer) (7.1.0) Requirement already satisfied: python-dateutil>=2.8.0 in /home/benoit/Teaching/Qnotes/qiskit-env/lib/python3.13/site-packages (from qiskit-aer) (2.9.0.post0) Requirement already satisfied: six>=1.5 in /home/benoit/Teaching/Qnotes/qiskit-env/lib/python3.13/site-packages (from python-dateutil>=2.8.0->qiskit-aer) (1.17.0)
import numpy as np
from math import pi
from qiskit import *
from qiskit_aer import StatevectorSimulator, AerSimulator
from qiskit.circuit import *
from qiskit.circuit.library import *
from qiskit.quantum_info.operators import Operator
from matplotlib.pyplot import plot,show
%matplotlib inline
Small library for pretty-printing
def processOneState(st): # Longueur = puissance de 2
s = list(st)
if len(s) == 2: return {'0' : s[0], '1' : s[1]}
else:
a0 = processOneState(s[:len(s)//2])
a1 = processOneState(s[len(s)//2:])
r = {}
for k in a0: r['0' + k] = a0[k]
for k in a1: r['1' + k] = a1[k]
return r
def printOneState(d): # get a dict as per processStates output
for k in d:
im = d[k].imag
re = d[k].real
if abs(im) >= 0.001 or abs(re) >= 0.001:
print("% .3f + % .3fj |%s>" % (re,im,k))
def printFinalRes(result):
printOneState(processOneState(list(np.asarray(result))))
def runStateVector(qc):
simulator = StatevectorSimulator()
job = simulator.run(qc.decompose(reps=6), memory=True)
job_result = job.result()
result = job_result.results[0].to_dict()['data']['statevector']
printFinalRes(result)
def runStateVectorSeveralTimes(qc, howmany):
qc.save_statevector(label = 'collect', pershot = True)
simulator = StatevectorSimulator()
job = simulator.run(qc.decompose(reps=6), memory=True, shots=howmany)
result = job.result()
memory = result.data(0)['memory']
collect = result.data(0)['collect']
r = {}
for i in range(len(collect)):
r[str(collect[i])] = (0, collect[i])
for i in range(len(collect)):
n, v = r[str(collect[i])]
r[str(collect[i])] = (n+1, v)
for k in r:
i, v = r[k]
print(f"With {i} occurences:")
printFinalRes(v)
def runSample(qc,howmany):
simulator = AerSimulator()
job = simulator.run(qc.decompose(reps=6), shots=howmany)
res = dict(job.result().get_counts(qc))
return res
1 - Circuits¶
A circuit in QisKit acts on quantum and classical registers.
A classical register aims at storing the result of the measurement of a quantum register.
# Allocating 2 qubits
q = QuantumRegister(2, name="x")
# Allocating 2 bits
c = ClassicalRegister(2, name="y")
# We build a quantum circuit with both registers.
# By default, everything is initialized to 0 and to |0>
qc = QuantumCircuit(q,c)
# Applying Hadamard on qubit 0:
qc.h(q[0])
# Applying X on qubit 0:
qc.x(q[0])
# Applying z on qubit 0:
qc.z(q[0])
# Applying CNOT on qubits 0 and 1:
qc.cx(q[0],q[1])
# Mesure of all of register q, storing results in c.
# This is still part of the circuit
qc.measure(q, c)
# A summary of native operations can be found here:
# https://docs.quantum.ibm.com/guides/construct-circuits
<qiskit.circuit.instructionset.InstructionSet at 0x7fdde42bc8e0>
The circuit can be drawn in text-style or with mathplotlib. Note how the name given to the registers appear on the drawing. The simple wires are qubit-wires, while the doubled-wires are for bits.
qc.draw()
┌───┐┌───┐┌───┐ ┌─┐
x_0: ┤ H ├┤ X ├┤ Z ├──■──┤M├───
└───┘└───┘└───┘┌─┴─┐└╥┘┌─┐
x_1: ───────────────┤ X ├─╫─┤M├
└───┘ ║ └╥┘
y: 2/═════════════════════╩══╩═
0 1
2 - Runing a circuit¶
One can run a circuit with backends.
Here is a backend emulating the behavior of a quantum co-processor. It makes it possible to lauch a series of runs, keeping track of how many of each results were obtained in the classical registers (remember, each run is probabilistic).
q = QuantumRegister(2, name="x")
c = ClassicalRegister(2, name="y")
qc = QuantumCircuit(q,c)
qc.h(q[0]) # build a
qc.cx(q[0],q[1]) # Bell state
qc.measure(q, c) # measure of all of q
# To run the circuit, we initialize a simulator
simulator = AerSimulator()
# Then performs several runs of the circuit using this backend. Here we ask for 1024 runs.
job = simulator.run(qc, shots=1024)
# To retrieve the results -- note how we only get values for the single bit-register
res = dict(job.result().get_counts(qc))
res
{'00': 482, '11': 542}
In what we just saw, we measured all of the system. The dictionary stores the number of times each key has been found with the final measure.
We can however measure only part of the system: the rest is "forgotten". For instance, in the following we only measure the 1st qubit
q = QuantumRegister(2, name="x")
c = ClassicalRegister(1, name="y")
qc = QuantumCircuit(q,c)
qc.h(q[0]) # on fabrique un
qc.cx(q[0],q[1]) # état de Bell
qc.measure(q[0], c[0]) # mesure du premier qubit
simulator = AerSimulator()
job = simulator.run(qc, shots=1024)
res = dict(job.result().get_counts(qc))
res
{'1': 518, '0': 506}
Note how the keys only contain one bit : the content of the classical register
3 - Order of the bits in the keys¶
Unlike what we saw in class, the bit-vector has to be read "by turning the head on the left" with respect to the circuit: in a register $x$, the qubit $x_0$ (top wire) is the first one. This is also the case for the classical registers.
Below a concrete example:
q = QuantumRegister(2)
c = ClassicalRegister(2)
qc = QuantumCircuit(q,c)
# Applying X on qubit 0
qc.x(q[0])
# So at the end, |x_0 x_1> is in state |10>
qc.measure(q,c)
qc.draw()
# And now, in register c[0] we have 1 and in c[1] we have 0.
simulator = AerSimulator()
job = simulator.run(qc, shots=1024)
dict(job.result().get_counts(qc))
{'01': 1024}
A key should then be read $b_1b_0$ : the register $c$ is written "$c_1c_0$".
4 - Boxing : unitaries and sub-circuits¶
It is possible to look at a sub-circuit as a unitary gate. This circuit can then be used as many time as needed in another circuit.
Beware: only circuits without classical registers can be boxed into a unitary gate...
# Let us build a circuit
q = QuantumRegister(2, name="x")
aux = QuantumCircuit(q) # No classical registers !
aux.h(q[0])
aux.cx(q[0],q[1])
aux.draw()
┌───┐
x_0: ┤ H ├──■──
└───┘┌─┴─┐
x_1: ─────┤ X ├
└───┘
# We can now make a home-made gate using this circuit
o = aux.to_gate(label="mycirc") # name to be used in drawings
# We now have a new gate "o" acting on 2 qubits. We can use it as we want.
q = QuantumRegister(4, name="x")
qc = QuantumCircuit(q)
qc.barrier() # To horizontally "separate" pieces of circuits, in drawings for instance.
qc.append(o,[q[0],q[1]]) # adding an object "UnitaryGate" can be done with .append
qc.barrier()
qc.append(o,[q[2],q[1]]) # Check the numbering in the drawing !
qc.barrier()
qc.append(o.control(),[q[0],q[2],q[3]]) # We can control a door -- the first wire is the control qubit.
qc.draw()
░ ┌─────────┐ ░ ░
x_0: ─░─┤0 ├─░─────────────░──────■─────
░ │ mycirc │ ░ ┌─────────┐ ░ │
x_1: ─░─┤1 ├─░─┤1 ├─░──────┼─────
░ └─────────┘ ░ │ mycirc │ ░ ┌────┴────┐
x_2: ─░─────────────░─┤0 ├─░─┤0 ├
░ ░ └─────────┘ ░ │ mycirc │
x_3: ─░─────────────░─────────────░─┤1 ├
░ ░ ░ └─────────┘
# One can open the boxes
qc.decompose().draw()
# Note how the controlled gate is splitted into native, elementary gates on 1 and 2 qubits.
░ ┌───┐ ░ ░
x_0: ─░─┤ H ├──■───░────────────░──────────────────■───────────────────────■──
░ └───┘┌─┴─┐ ░ ┌───┐ ░ │ │
x_1: ─░──────┤ X ├─░──────┤ X ├─░──────────────────┼───────────────────────┼──
░ └───┘ ░ ┌───┐└─┬─┘ ░ ┌───┐┌───┐┌───┐┌─┴─┐┌─────┐┌───┐┌─────┐ │
x_2: ─░────────────░─┤ H ├──■───░─┤ S ├┤ H ├┤ T ├┤ X ├┤ Tdg ├┤ H ├┤ Sdg ├──■──
░ ░ └───┘ ░ └───┘└───┘└───┘└───┘└─────┘└───┘└─────┘┌─┴─┐
x_3: ─░────────────░────────────░────────────────────────────────────────┤ X ├
░ ░ ░ └───┘
5 - High-level operations on UnitaryGate¶
Once we have a UnitaryGate object (for instance the 'o' object), one can perform various operations in it, such as
- power
- control
- inverse
However, the names get all mangled...
Below some examples
q = QuantumRegister(4, name="x")
qc = QuantumCircuit(q)
qc.append(o.power(2),[q[0],q[1]]) # will perform 'o' twice
qc.append(o.power(5).control().control(),[q[2],q[3],q[0],q[1]]) # power and control can be combined
qc.append(o.inverse(),[q[0],q[1]]) # for the inverse (the _dg in the name stands for "dagger")
qc.append(o.control(num_ctrl_qubits=2, ctrl_state='10'),[q[2],q[3],q[0],q[1]])
# one can perform a bunch of positive and negative controls in one go.
qc.draw()
# Ugly names ! But the name somehow keep the power so we know where it comes from.
┌───────────────┐┌───────────────┐┌────────────────┐┌─────────┐
x_0: ┤0 ├┤0 ├┤0 ├┤0 ├
│ circuit-50^2 ││ circuit-50^5 ││ circuit-50_dg ││ mycirc │
x_1: ┤1 ├┤1 ├┤1 ├┤1 ├
└───────────────┘└───────┬───────┘└────────────────┘└────┬────┘
x_2: ─────────────────────────■───────────────────────────────o─────
│ │
x_3: ─────────────────────────■───────────────────────────────■─────
6 - Operators¶
One can ask QisKit to build a circuit from an operator given as a matrix.
# What is doing this gate ?
U = UnitaryGate(
Operator([[1,0,0,0],
[0,1,0,0],
[0,0,1,0],
[0,0,0,np.exp(pi*1j*1/4)]]), label="MyDoor")
The operator have to be a matrix of size power of $2$ --- the unitary gate then acts on the corresponding number of wires.
q = QuantumRegister(2)
qc = QuantumCircuit(q)
qc.x(q) # State initialized to |11>
qc.append(U,q) # In principle, a phase shift should occur.
qc.draw()
┌───┐┌─────────┐
q1_0: ┤ X ├┤0 ├
├───┤│ MyDoor │
q1_1: ┤ X ├┤1 ├
└───┘└─────────┘
Let us check the phase shift by using another backend: 'statevector_simulator', keeping track of the state vector.
simulator = StatevectorSimulator()
job = simulator.run(qc, memory=True)
job_result = job.result()
output = job_result.results[0].to_dict()['data']['statevector']
output
# There has indeed been a phase shift !
Statevector([0. +0.j , 0. +0.j ,
0. +0.j , 0.70710678+0.70710678j],
dims=(2, 2))
One of the function defined at the top is there to help pretty-print this ugly vector.
printFinalRes(output)
0.707 + 0.707j |11>