Introduction to QisKit¶

There is nothing to hand out here¶

First, some libraries to load up front

In [1]:
! 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)
In [2]:
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

In [3]:
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.

In [4]:
# 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
Out[4]:
<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.

In [5]:
qc.draw()
Out[5]:
     ┌───┐┌───┐┌───┐     ┌─┐   
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).

In [6]:
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
Out[6]:
{'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

In [7]:
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
Out[7]:
{'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:

In [8]:
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))
Out[8]:
{'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...

In [9]:
# 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()
Out[9]:
     ┌───┐     
x_0: ┤ H ├──■──
     └───┘┌─┴─┐
x_1: ─────┤ X ├
          └───┘
In [10]:
# We can now make a home-made gate using this circuit

o = aux.to_gate(label="mycirc") # name to be used in drawings
In [11]:
# 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()
Out[11]:
      ░ ┌─────────┐ ░             ░            
x_0: ─░─┤0        ├─░─────────────░──────■─────
      ░ │  mycirc │ ░ ┌─────────┐ ░      │     
x_1: ─░─┤1        ├─░─┤1        ├─░──────┼─────
      ░ └─────────┘ ░ │  mycirc │ ░ ┌────┴────┐
x_2: ─░─────────────░─┤0        ├─░─┤0        ├
      ░             ░ └─────────┘ ░ │  mycirc │
x_3: ─░─────────────░─────────────░─┤1        ├
      ░             ░             ░ └─────────┘
In [12]:
# One can open the boxes

qc.decompose().draw()

# Note how the controlled gate is splitted into native, elementary gates on 1 and 2 qubits.
Out[12]:
      ░ ┌───┐      ░            ░                                             
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

In [13]:
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.
Out[13]:
     ┌───────────────┐┌───────────────┐┌────────────────┐┌─────────┐
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.

In [14]:
# 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.

In [15]:
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()
Out[15]:
      ┌───┐┌─────────┐
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.

In [16]:
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.

In [17]:
printFinalRes(output)
 0.707 +  0.707j |11>
In [ ]:
 
In [ ]: