Import Libraries

In [2]:
from numpy import *
from scipy.linalg import hadamard
import matplotlib.pyplot as plt
import time
from tabulate import tabulate
from scipy.optimize import curve_fit

Handle and arbitrary number of qubits

Program Grover's search for an arbitrary number of qubits N. Check that the optimum number of Grover iterations is $\frac{\pi}{4}\sqrt{2^{N}}$, for $N>3$. Then increase $N$ as much as possible.

The Hadamard Gate
A Hadamard gate is described by the matrix

$\begin{equation*}\hat{H} \leftrightarrow \frac{1}{\sqrt{2}}\begin{bmatrix} 1&1\\ 1&-1\\ \end{bmatrix}\end{equation*}$

For example, with $N=4$ qubits, the operation that applies a Hadamard gate to qubit 1 is
$\hat{H}^{(1)}=\hat{H}\otimes\hat{I}\otimes\hat{I}\otimes\hat{I}$
In [3]:
nn = 4
N = 2**nn
I = eye(2)
H = hadamard(2)
factor = 1/sqrt(2)

def had(i):
    
    if i == nn: 
        product = H
    else:
        product = I
     
    for j in range(nn-1,0,-1):
        
        if j!=i: 
            product = kron(I,product)
        else:
            product = kron(H,product)

    return array(product).astype(int)


H1, H2, H3 = had(1), had(2), had(3)
print(f"The Hadamard gate that applied to qubit 1 is ~{round(factor,4)} multiplied by: \n{H1}")
            
The Hadamard gate that applied to qubit 1 is ~0.7071 multiplied by: 
[[ 1  0  0  0  0  0  0  0  1  0  0  0  0  0  0  0]
 [ 0  1  0  0  0  0  0  0  0  1  0  0  0  0  0  0]
 [ 0  0  1  0  0  0  0  0  0  0  1  0  0  0  0  0]
 [ 0  0  0  1  0  0  0  0  0  0  0  1  0  0  0  0]
 [ 0  0  0  0  1  0  0  0  0  0  0  0  1  0  0  0]
 [ 0  0  0  0  0  1  0  0  0  0  0  0  0  1  0  0]
 [ 0  0  0  0  0  0  1  0  0  0  0  0  0  0  1  0]
 [ 0  0  0  0  0  0  0  1  0  0  0  0  0  0  0  1]
 [ 1  0  0  0  0  0  0  0 -1  0  0  0  0  0  0  0]
 [ 0  1  0  0  0  0  0  0  0 -1  0  0  0  0  0  0]
 [ 0  0  1  0  0  0  0  0  0  0 -1  0  0  0  0  0]
 [ 0  0  0  1  0  0  0  0  0  0  0 -1  0  0  0  0]
 [ 0  0  0  0  1  0  0  0  0  0  0  0 -1  0  0  0]
 [ 0  0  0  0  0  1  0  0  0  0  0  0  0 -1  0  0]
 [ 0  0  0  0  0  0  1  0  0  0  0  0  0  0 -1  0]
 [ 0  0  0  0  0  0  0  1  0  0  0  0  0  0  0 -1]]

Grover's Algorithm
This algorthm requires the oracle $\hat{O}$ and the $\hat{J}$ matricies. For $N=4$, the product $\hat{H}^{(4)}\cdot\hat{H}^{(3)}\cdot \hat{H}^{(2)}\cdot \hat{H}^{(1)}$ is an operator that applies a Hadamard gate to qubits 1, 2, 3, and 4. This operator is equvalent to the 'hadamard' function from 'scipy.linalg' for $2^{N}$, where $N=4$, multiplied by the factor $\frac{1}{4}$. Let's program Grover's search for an arbitrary number of qubits $N$.

In [4]:
def opO(state,N):
    O = identity(N,dtype=int)
    O[state,state] = -1
    return O

def opJ(N):
    J = identity(N,dtype=int)
    J[0,0] = -1
    return J

def groverN(nn,N,state,rep,question):
    qb = zeros(N)
    qb[state] = 1
    qb = array([qb]).T
    oracle = opO(question,N)
    J = opJ(N)
    factor = (1/(sqrt(2)))**nn
    H = factor*hadamard(N)
    
    qb = H @ qb
    for i in range(rep):
        qb = H @ J @ H @ oracle @ qb
  
    return qb

Let's test Grover's Algorithm for $N=4$ qubits and $3$ iterations (approximately the optimum number) on the state $\lvert0000\rangle$, with the correct question $\lvert0000\rangle$.

In [5]:
print(f"The optimum # of repititions is {round((pi/4)*sqrt(N),2)}"
      f" for ~90% return on your state\n")
state = 0
rep = 3
g = groverN(nn,N,state,rep,state)
g = g**2
x = linspace(0,N-1,N)
y = g.T
y = y[0]
plt.bar(x,y,color='purple')
plt.title("Result Probabilities for each Quantum State")
plt.ylim(0,1)
plt.show()


reps = 19
arr = zeros(reps)
rarr = linspace(1,reps,reps)
for i in range(1,reps):
    g = groverN(nn,N,state,i,state)
    arr[i] = g[0]**2
    
    
def g(x,A,w,s,C):
    return A*sin(w*x+s)+C
guesses = (1,1,1,1)
(A,w,s,C), cc = curve_fit(g,rarr,arr,p0=guesses)
xmod = linspace(rarr[0],rarr[-1],100)
ymod = g(xmod,A,w,s,C)
plt.plot(rarr,arr,'ko')
plt.plot(xmod,ymod,color='purple')
plt.xlim(1,reps)
plt.title("Quantum State Probabilities vs. Number of Iterations")
plt.show()
The optimum # of repititions is 3.14 for ~90% return on your state

The measured result is $\lvert0000\rangle$ more than 90% of the time, as expected, and the return rate on the quantum state is cyclic. Let's see how long it takes to compute Grover's Algorithm.

In [6]:
arr = list()
m = 12
qarr = linspace(2,m,m-1).astype(int)
times = list()

for r in range(len(qarr)):
    time_start = time.perf_counter()
    g = groverN(qarr[r],2**qarr[r],state,rep,state)
    time_elapsed = (time.perf_counter() - time_start)
    arr.append([qarr[r],time_elapsed])
    times.append(time_elapsed)
print(tabulate(arr,headers=["N-qubits","Time (s)"]))
  N-qubits    Time (s)
----------  ----------
         2   0.0005503
         3   0.0003905
         4   0.0004222
         5   0.0008165
         6   0.0007176
         7   0.004496
         8   0.0147223
         9   0.0793245
        10   0.47496
        11   3.01297
        12  23.192

Now we can plot this data and create a best fit!

In [7]:
def f(x,A,p):
    return A*exp(x*p)

guesses = (1,1)
(A,p), cc = curve_fit(f,qarr,array(times),p0=guesses)
xmod = linspace(qarr[0],qarr[-1],100)
ymod = f(xmod,A,p)
plt.plot(qarr,array(times),'ko')
plt.plot(xmod,ymod,color='purple')
plt.xlabel("N-qubits")
plt.ylabel("Grover Execution time (s)")
plt.show()

The expoential fit can be used to predict how long it should take the algorithm to do N-qubit computations (given it has enough RAM). For 4GB RAM and 16 bytes per complex amplitude, the largest possible state is 27-qubit.

In [24]:
c = 27
v = 21
predict = list()
prx = linspace(v,c,c-v-1).astype(int)
pry = f(prx,A,p)/(60**2*24*365*100)
for g in range(len(prx)): predict.append([prx[g],pry[g]])
print(tabulate(predict,headers=["N-qubits","Time (centuries)"]))
  N-qubits    Time (centuries)
----------  ------------------
        21            0.653293
        22            4.99213
        24          291.503
        25         2227.52
        27       130070