First, make sure libraries are installed (you can comment those)
# ! python -m pip install matplotlib
# ! python -m pip install qiskit qiskit-aer
Now, some libraries to load (nothing to modify here)
import numpy as np
import matplotlib.pyplot as plt
from scipy import linalg
from math import pi, gcd
from qiskit import *
from qiskit.circuit import *
from qiskit.circuit.library import *
from qiskit.quantum_info.operators import Operator
from qiskit_aer import AerSimulator, StatevectorSimulator
from scipy import optimize
%matplotlib inline
%config InlineBackend.figure_formats = ['svg']
Small library for pretty-printing (nothing to do)
def nat2bl(pad,n):
if n == 0: r = [0 for i in range(pad)]
elif n % 2 == 1: r = nat2bl(pad-1,(n-1)//2); r.append(1)
else: r = nat2bl(pad-1,n//2); r.append(0)
return r
def bl2nat(s):
if len(s) == 0: return 0
else: a = s.pop(); return (a + 2*bl2nat(s))
def bl2bs(l):
if len(l) == 0: return ""
else: a = l.pop(); return (bl2bs(l) + str(a))
def nat2bs(pad,i): return bl2bs(nat2bl(pad,i))
def bs2bl(s):
l = []
for i in range(len(s)): l.append(int(s[i]))
return l
def bs2nat(s): return bl2nat(bs2bl(s))
def processOneState(st): # Length = power of 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, keys=None): # get a dict as per processStates output
isThereMore = True
if keys == None:
keys = d.keys()
isThereMore = False
for k in sorted(keys):
if k not in d:
print(f"Error: {k} not valid. Maybe the size of the bitstring is invalid?")
isThereMore = False
break
im = d[k].imag
re = d[k].real
if abs(im) >= 0.001 or abs(re) >= 0.001:
print("% .5f + % .5fj |%s>" % (re,im,k))
if isThereMore: print(" ... (there might be more hidden terms)")
def printFinalRes(result, keys=None):
printOneState(processOneState(list(np.asarray(result))), keys)
def runStateVector(qc, keys=None):
simulator = StatevectorSimulator(statevector_parallel_threshold=6)
job = simulator.run(qc.decompose(reps=6), memory=True)
job_result = job.result()
result = job_result.results[0].to_dict()['data']['statevector']
printFinalRes(result, keys)
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 plotDistrib(d):
sorted_items = sorted(d.items())
keys = [k for k, _ in sorted_items]
values = [v for _, v in sorted_items]
plt.figure()
plt.bar(keys, values)
plt.xticks(rotation=90)
plt.show()
def getSample(qc,howmany):
simulator = AerSimulator()
job = simulator.run(qc.decompose(reps=6), shots=howmany)
res = dict(job.result().get_counts(qc))
return res
def plotSample(qc,howmany):
d = getSample(qc,howmany)
ld = len(list(d.keys())[0])
for i in range(2 ** ld):
s = nat2bs(ld,i)
if s not in d: d[s] = 0
plotDistrib(d)
q1 = QuantumRegister(3, name="q1")
q2 = QuantumRegister(1, name="q2")
qc = QuantumCircuit(q1,q2)
qc.x(q1[0])
qc.h(q1[1])
qc.h(q2[0])
qc.cx(q2[0],q1[2])
# Show circuit, just to check
qc.draw()
┌───┐
q1_0: ┤ X ├─────
├───┤
q1_1: ┤ H ├─────
└───┘┌───┐
q1_2: ─────┤ X ├
┌───┐└─┬─┘
q2: ┤ H ├──■──
└───┘
We can now "run" the circuit and see what is the state vector at the end. As we saw in the previous lab session, I wrote a small wrapper function to "nicely" print out the result. The precision is caped at 10${}^{-5}$ for readability.
runStateVector(qc)
0.50000 + 0.00000j |0001> 0.50000 + 0.00000j |0011> 0.50000 + 0.00000j |1101> 0.50000 + 0.00000j |1111>
Note how q2 (that was put at the end in QuantumRegister) appears at the beginning in the ket basis vectors. Compared to the circuit drawing, one must "turn the head to the left". I tried to use the same conventions as the results of measurements.
It is possible to only show some of the basis vectors (when the sum is large and we only care about a handful of them).
runStateVector(qc, keys=["0011", "1111"])
0.50000 + 0.00000j |0011> 0.50000 + 0.00000j |1111> ... (there might be more hidden terms)
0.2 - The HHL Algorithm¶
For the presentation of the algorithm, see Section 3.6 of the lecture notes. In this section, we summarize the notations. The inputs to the algorithm is a hermitian matrix $A$ and a vector $\vec{b}$, and the problem is to solve the system
$$A\cdot \vec{x} = \vec{b}$$
The idea consists in coding the vectors $\vec{b}$ and $\vec{x}$ in the state of registers: if $\vec{b} = (b_0,b_1,b_2,b_3)$, we store the vector in $|b\rangle = b_0\cdot|00\rangle+b_1\cdot|01\rangle+b_2\cdot|10\rangle+b_3\cdot|11\rangle$, modulo some renormalization.
Of course, there are some constraints. But in summary, if
- $N$ is the size of the system
- $s$ is the number of non-zero elements on each line of $A$
- $κ$ is the matrix condition number (the ratio between the largest and the smallest eigenvalue)
- $ϵ$ is the allowed error
The the complexities are
$\mathcal{O}(Nsκ\log(1/ϵ))$ for the classical algorithm
$\mathcal{O}(\log(N)s^2κ^2/ϵ)$ for HHL
Meaning that, in theory, we get an speedup exponential on the size of the system.
1 - Case Study for this Lab Session¶
We are going to use the case study described in Section 6.3.13 of the lecture notes. The system is
$$\begin{align} x_1-\frac{x_2}3 &= 1\\ \frac{-x_1}3 + x_2 &= 0 \end{align} $$
1.1 - Memory Encoding¶
A single qubit is enough to store $\vec{b}$ and $\vec{x}$:
$$\begin{align} \vec{b} &\quad\equiv\quad 1\cdot|0\rangle + 0\cdot|1\rangle\\ \vec{x} &\quad\equiv\quad (D\,x_1)\cdot|0\rangle + (D\,x_2)\cdot|1\rangle \end{align}$$
where $D$ is a renormalization factor. Indeed, if you remember the lecture notes, the solution is
$$\left(\begin{array}{c}9/8\\3/8\end{array}\right)$$
which is not of norm $1$.
1.2 - Coding the algorithm¶
The algorithm relies on several quantum registers:
qb: to store $\vec{b}$ and to read $\vec{x}$. Here, of size $1$.qe: ancilla to store the eigenvalues. For the given example, $2$ qubits are enough.qi: a single qubit, to characterize the "good" subspace (where we can read the result).
We need several subroutines for the algorithm:
Initialization of the input vector $\vec{b}$: In our case it is trivial.
QPE : this has been done in Lab Session #1.
The unitary to use for QPE: built from $A$, it is $U = e^{iAt}$, for a "correct" $t$, depending on $\kappa$: it should be small enough so that the eigenvalues of $tA$ do not "overflow", yet large enough to be readable.
A subroutine implementing the inversion oracle.
The major difficulty lies in the last two points. In this lab session, we shall use a quick-n-dirty road to get a working example (albeit not scalable).
1.3 - The matrix $A$ and the operator $U$¶
For our algorithm, we shall need the exponential $U = e^{iAt}$. In general, when $A$ is very large, computing $U$ is a costly operation: in the context of quantum computation, a strategy consists in using e.g. Trotterization, subject of the next lecture.
For the purpose of our small $2\times 2$ case-study, we shall simply compute the matrix $U$ using the function expm of the library linalg and the built-in generic circuit synthesis implemented in QisKit.
# The matrix A
A = np.array([[1,-1/3],[-1/3,1]])
A
array([[ 1. , -0.33333333],
[-0.33333333, 1. ]])
# The coefficient giving exact results
t = 2*pi*3/8
# The exponential of $A$
EA = linalg.expm((1j)*t*A)
EA
array([[-0.5+0.5j, 0.5+0.5j],
[ 0.5+0.5j, -0.5+0.5j]])
# The corresponding unitary gate, using QisKit magic
U = UnitaryGate(Operator(EA))
U
Instruction(name='unitary', num_qubits=1, num_clbits=0, params=[array([[-0.5+0.5j, 0.5+0.5j],
[ 0.5+0.5j, -0.5+0.5j]])])
1.4 - The inversion subroutine¶
Assume for simplicity that the eigenvalues are non-negative. We want to invert a value $\lambda\gt 0$ coded on 2 bits, for instance as follows:
$$\lambda = \frac{e_0}{2} + \frac{e_1}{4}$$
(with $e_0$ and $e_1$ Boolean values).
Note that $\frac13\leq \frac1{4\lambda} \leq 1$
We now want an operator that produces
$$|e_1e_0\rangle\otimes|0\rangle\mapsto |e_1e_0\rangle\otimes\left(\sqrt{1 - \frac1{16\lambda^2}}|0\rangle + \frac1{4\lambda}|{1}\rangle\right)$$
Here, the value $\frac14$ is the coefficient $C$ in Section 6.3.13 of the lecture notes. It is chosen so that the coefficients of shape $\frac{2\pi\cdot C}{\lambda t}$ obtained from the eigenvalues of $tA$ are indeed within $-1$ and $1$.
TODO¶
Realize this operation with multi-controlled $R_y(\theta)$ gates. The gate is
$$ R_y(\theta) = \left( \begin{array}{cc} \cos(\theta/2) & -\sin(\theta/2)\\ \sin(\theta/2) & \cos(\theta/2) \end{array} \right) $$
To build the circuit:
- Arcsine is
np.arcsinin Python - $R_y$ gates are
RYGate(angle)in QisKit - A gate
gatecan be controlled withgate.control(num_ctrl_qubits=3, ctrl_state='101')if you want 3 controls: 1 positive, 1 negative, 1 positive - A gate can be inserted in a circuit with
mycircuit.append(mygate, list-of-wires).
# For the value to invert
n = 2
r = QuantumRegister(n, name="r")
# To store the invert
inv = QuantumRegister(1, name="inv")
qcinv = QuantumCircuit(r,inv)
C = 1 / 2 ** n
for i in range(1,(2 ** n)):
state = nat2bs(n, i)
val = bs2nat(state) / 2**n
gate = RYGate(2*np.arcsin(C/val))
gate = gate.control(num_ctrl_qubits=n, ctrl_state=state)
qcinv.append(gate, list(r) + [inv[0]])
qcinv.draw()
r_0: ────■─────────o────────────■───────
│ │ │
r_1: ────o─────────■────────────■───────
┌───┴───┐┌────┴────┐┌──────┴──────┐
inv: ┤ Ry(π) ├┤ Ry(π/3) ├┤ Ry(0.67967) ├
└───────┘└─────────┘└─────────────┘
We will now store this circuit inside a (large) unitary gate invCirc acting on 3 wires.
invCirc = qcinv.to_gate(label="inv")
To understand how to use this gate (and make sure it works), here is a small code snippet. Change the value v to check that you get the correct value.
- The gate
invCircis added to a circuit withqc.append(invCirc, list-of-wires) - Where do we read the value $1/v$ ?
- At the level of kets, where are the registers
qandr?
# The value to invert : test 1, 2 and 3 and make sure to understand what's going on
v = 1
# For the value to invert
r = QuantumRegister(n, name="r")
# To store the value
inv = QuantumRegister(1, name="inv")
qctest = QuantumCircuit(r,inv)
bi = list(reversed(nat2bl(n,v)))
for i in range(len(bi)):
if bi[i] == 1:
qctest.x(r[i])
qctest.append(invCirc, list(r) + [inv[0]])
#Execute and get result
runStateVector(qctest)
1.00000 + -0.00000j |101>
3 - Summing-up: Complete HHL Algorithm¶
Below you will find a code with holes to be filled in, realizing HHL with 2 bits of precisions, using the pieces above and QPE that was done in Lab Session #2.
3.1 - TODO¶
Fill in the holes in order to get a circuit with the right shape and that computes the correct answer.
# For |b>
nb = 1
qb = QuantumRegister(nb, name="b")
# For the eigenvalue : ne bits of précision
ne = 2
qe = QuantumRegister(ne, name="eig")
# For the angle : only one qubit
qi = QuantumRegister(1, name="inv")
# Creating the circuit
qc = QuantumCircuit(qi,qe,qb)
## TODO : QPE on qb and qe
qc.h(qe)
for i in reversed(range(ne)):
qc.append(U.power(2 ** (i)).control(),[qe[i]] + list(qb))
qc.append(QFTGate(ne).inverse(),qe)
## TODO invCirc on qe and qi
qc.append(invCirc, list(qe) + [qi[0]])
## TODO : QPE, inverted
qc.append(QFTGate(ne),qe)
for i in range(ne):
qc.append(U.power(-2 ** (i)).control(),[qe[i]] + list(qb))
qc.h(qe)
# Beware ! We do not measure because we want to see the final state vector.
# Let us draw the circuit and check that it looks correct
qc.draw()
┌──────┐ »
inv: ──────────────────────────────────────────┤2 ├──────────────────────»
┌───┐ ┌─────────┐│ │┌──────┐ »
eig_0: ┤ H ├───────────────────■──────┤0 ├┤0 inv ├┤0 ├──────■───────»
├───┤ │ │ qft_dg ││ ││ Qft │ │ »
eig_1: ┤ H ├──────■────────────┼──────┤1 ├┤1 ├┤1 ├──────┼───────»
└───┘┌─────┴─────┐┌─────┴─────┐└─────────┘└──────┘└──────┘┌─────┴──────┐»
b: ─────┤ unitary^2 ├┤ unitary^1 ├───────────────────────────┤ unitary^-1 ├»
└───────────┘└───────────┘ └────────────┘»
«
« inv: ───────────────────
« ┌───┐
«eig_0: ────┤ H ├──────────
« └───┘ ┌───┐
«eig_1: ──────■───────┤ H ├
« ┌─────┴──────┐└───┘
« b: ┤ unitary^-2 ├─────
« └────────────┘
Now, let us test the circuit.
# On lance un simulateur
runStateVector(qc)
0.43301 + 0.00000j |0000> 0.75000 + 0.00000j |0001> -0.43301 + -0.00000j |1000> 0.25000 + 0.00000j |1001>
Remember (or look at Section 6.3.10 Point 4 in the lecture notes) that the final state should be
$$|0\rangle_{qi}\otimes[\text{uninteresting}] + \frac{2\pi C}{t}|1\rangle_{qi}\otimes|x\rangle $$
SOLUTION we can indeed read the answer: the last bit is the register qi, and $\frac{2\pi C}{t}$ is $\frac23$. The value 0.250000 we read in the result is indeed $\frac38\cdot\frac23$, and similarly for 0.750000.