jupytext | kernelspec | language_info | ||||||||||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
|
+++ {"pycharm": {"name": "#%% md\n"}}
+++
In this unit, we'll introduce Grover's algorithm{cite}grover_search,nielsen_chuang_search
and consider the problem of searching for an answer in an unstructured database using this algorithm. After that, We will implement Grover's algorithm using Qiskit.
---
local: true
---
+++
In order to realize quantum advantage over classical computation, one has to exploit quantum algorithm that can take advantage of quantum properties. One example of such algorithms is Grover's algorithm. Grover's algorithm is suited for searching in unstructured database, and it has been proven that Grover's algorithm can find solutions with fewer computational resources than those required for classical counterparts. This algorithm is based on a method known as amplitude amplification and is widely used as a subroutine in various quantum algorithms.
+++
(database)=
Let us consider that there is a list consisting of
+++
(grover)=
Here we will consider
+++
(grover_phaseoracle)=
The Grover's algorithm is characterized by the existence of phase oracle, which changes phase of a certain state. First, let us consider a phase oracle
this leads to the oracle (denoted as
If we use a matrix form, the
then we can get unitary
Here the matrix form of
+++
(grover_circuit)=
The structure of the circuit used to implement Grover's algorithm is shown below. Starting with the
:alt: grover
:width: 600px
:align: center
:alt: grover_iter
:width: 550px
:align: center
The
Together with the Hadamard operator at the beginning of the circuit, we will look at steps involved in a single Grover iteration in detail below.
:alt: grover_iter1
:width: 600px
:align: center
+++
(grover_superposition)=
First, a uniform superposition state is produced by applying Hadamard gates to initial state
This state is denoted as
+++
(grover_geometry)=
Let's view this
In short,
In above equations, the amplitude of
The
:alt: grover_rot1
:width: 300px
:align: center
+++
(grover_oracle)=
Next, we will apply the oracle
:alt: grover_rot2
:width: 300px
:align: center
+++
(grover_diffuser)=
Next is the application of
This means that the diffuser
:alt: grover_rot3
:width: 300px
:align: center
In summary, the Grover iteration
and is equivalent to rotating the
:alt: grover_rot4
:width: 300px
:align: center
This correspondence between
This indicates that
Let's look at the diffuser's role a bit more. We can think of a certain state
+++
(grover_amp)=
Let us visualize how the amplitude of the state corresponding to correct answer is amplified.
First, the Hadamard gates applied at the beginning of the circut create a superposition of all the computational basis states with equal amplitudes ((1) in the figure below). The horizontal axis shows
Next, applying the oracle
Last, the diffuser is applied to the state and all the amplitudes are reversed with respect to the average ((3) of the figure). This increases the amplitude of
:alt: grover_amp
:width: 800px
:align: center
+++
(grover_multidata)=
We have only considered so far searching for a single data. What if we consider finding multiple data? For example, we want to find
If the amplitude
+++ {"pycharm": {"name": "#%% md\n"}}
(imp)=
Now let's try to solve database search problem by implementing Grover's algorithm.
This exercise is to find a single answer "45" in a list containing
+++
(imp_qiskit)=
Set up the environment first.
---
jupyter:
outputs_hidden: false
pycharm:
name: '#%%
'
---
# Tested with python 3.8.12, qiskit 0.34.2, numpy 1.22.2
import matplotlib.pyplot as plt
import numpy as np
# Import Qiskit-related packages
from qiskit import QuantumCircuit, ClassicalRegister, QuantumRegister, transpile
from qiskit.quantum_info import Statevector
from qiskit.visualization import plot_histogram
from qiskit.tools.monitor import job_monitor
from qiskit_aer import AerSimulator
from qiskit_ibm_provider import IBMProvider, least_busy
from qiskit_ibm_provider.accounts import AccountNotFoundError
# Modules necessary for this workbook
from qc_workbook.utils import operational_backend
Prepare a 6-qubit circuit grover_circuit
.
A quantum circuit to perform a single Grover iteration will be something like below, and please write a quantum circuit that implements gates outlined in red (phase oracle and the unitary corresponding to
:alt: grover_6bits_45
:width: 600px
:align: center
Implement the oracle after generating a uniform superposition state
---
jupyter:
outputs_hidden: false
pycharm:
name: '#%%
'
tags: [remove-output]
---
Nsol = 45
n = 6
grover_circuit = QuantumCircuit(n)
grover_circuit.h(range(n))
# Create the oracle and implement it in the circuit
oracle = QuantumCircuit(n)
##################
### EDIT BELOW ###
##################
#oracle.?
##################
### EDIT ABOVE ###
##################
oracle_gate = oracle.to_gate()
oracle_gate.name = "U_w"
print(oracle)
grover_circuit.append(oracle_gate, list(range(n)))
grover_circuit.barrier()
Answer
```{code-block} python
##################
### EDIT BELOW ###
##################
oracle.x(1)
oracle.x(4)
oracle.h(n-1)
oracle.mcx(list(range(n-1)), n-1)
oracle.h(n-1)
oracle.x(1)
oracle.x(4)
##################
### EDIT ABOVE ###
##################
```
Next, implement the diffuser circuit.
---
jupyter:
outputs_hidden: false
pycharm:
name: '#%%
'
---
def diffuser(n):
qc = QuantumCircuit(n)
qc.h(range(n))
##################
### EDIT BELOW ###
##################
#qc.?
##################
### EDIT ABOVE ###
##################
qc.h(range(n))
#print(qc)
U_s = qc.to_gate()
U_s.name = "U_s"
return U_s
grover_circuit.append(diffuser(n), list(range(n)))
grover_circuit.measure_all()
grover_circuit.decompose().draw('mpl')
Answer
```{code-block} python
def diffuser(n):
qc = QuantumCircuit(n)
qc.h(range(n))
##################
### EDIT BELOW ###
##################
qc.rz(2*np.pi, n-1)
qc.x(list(range(n)))
# multi-controlled Zゲート
qc.h(n-1)
qc.mcx(list(range(n-1)), n-1)
qc.h(n-1)
qc.x(list(range(n)))
##################
### EDIT ABOVE ###
##################
qc.h(range(n))
#print(qc)
U_s = qc.to_gate()
U_s.name = "U_s"
return U_s
```
(imp_simulator)=
Once you have implemented the circuit, run the simulator and make a plot of the results. To make the results easy to understand, the measured bitstring is converted to integers before making the plot.
---
jupyter:
outputs_hidden: false
pycharm:
name: '#%%
'
tags: [remove-output]
---
simulator = AerSimulator()
grover_circuit = transpile(grover_circuit, backend=simulator)
results = simulator.run(grover_circuit, shots=1024).result()
answer = results.get_counts()
# Plot the values along the horizontal axis in integers
def show_distribution(answer):
n = len(answer)
x = [int(key,2) for key in list(answer.keys())]
y = list(answer.values())
fig, ax = plt.subplots()
rect = ax.bar(x,y)
def autolabel(rects):
for rect in rects:
height = rect.get_height()
ax.annotate('{:.3f}'.format(height/sum(y)),
xy=(rect.get_x()+rect.get_width()/2, height),xytext=(0,0),
textcoords="offset points",ha='center', va='bottom')
autolabel(rect)
plt.ylabel('Probabilities')
plt.show()
show_distribution(answer)
If the circuit is implemented correctly, you will see that the state
However, as discussed above, a single Grover iteration will produce incorrect answers with non-negligible probabilities in the search of
+++
(imp_qc)=
Before attempting multiple Grover iterations, let us first run a single Grover iteration on a quantum computer.
---
jupyter:
outputs_hidden: false
pycharm:
name: '#%%
'
tags: [raises-exception, remove-output]
---
# Implementation on a quantum computer
instance = 'ibm-q/open/main'
try:
provider = IBMProvider(instance=instance)
except IBMQAccountCredentialsNotFound:
provider = IBMProvider(token='__paste_your_token_here__', instance=instance)
backend_list = provider.backends(filters=operational_backend(min_qubits=6))
backend = least_busy(backend_list)
print("least busy backend: ", backend)
---
jupyter:
outputs_hidden: false
pycharm:
name: '#%%
'
tags: [raises-exception, remove-output]
---
# Execute the circuit on the backend with the highest level of availability. Monitor job execution in the queue.
grover_circuit = transpile(grover_circuit, backend=backend, optimization_level=3)
job = backend.run(grover_circuit, shots=1024)
job_monitor(job, interval=2)
---
jupyter:
outputs_hidden: false
pycharm:
name: '#%%
'
tags: [raises-exception, remove-output]
---
# Calculation results
results = job.result()
answer = results.get_counts(grover_circuit)
show_distribution(answer)
As you can see, the results are much worse than what we got with the simulator. Unfortunately, this is a typical result of running a quantum circuit of Grover's search algorithm on the present quantum computer as it is. We can however expect that the quality of the results will be improved by employing {ref}error mitigation <measurement_error_mitigation>
techniques.
+++ {"pycharm": {"name": "#%% md\n"}}
(imp_simulator_amp)=
Here we will see how the amplitude is amplified by running the Grover's iteration multiple times using simulator.
For example, the circuit to execute the Grover's iteration three times is prepared and executed.
---
pycharm:
name: '#%%
'
---
# Repetition of Grover's iteration
Niter = 3
grover_circuit_iterN = QuantumCircuit(n)
grover_circuit_iterN.h(range(n))
for I in range(Niter):
grover_circuit_iterN.append(oracle_gate, list(range(n)))
grover_circuit_iterN.append(diffuser(n), list(range(n)))
grover_circuit_iterN.measure_all()
grover_circuit_iterN.draw('mpl')
---
pycharm:
name: '#%%
'
---
grover_circuit_iterN_tr = transpile(grover_circuit_iterN, backend=simulator)
results = simulator.run(grover_circuit_iterN_tr, shots=1024).result()
answer = results.get_counts()
show_distribution(answer)
+++ {"pycharm": {"name": "#%% md\n"}}
You will see that the correct answer of
Next, we make a plot of showing the correlation between the number of Grover's iterations and how many times we observe the correct answer. Here the Grover's iteration is repated 10 times.
---
pycharm:
name: '#%%
'
---
x = []
y = []
# Repeating Grover's iteration 10 times
for Niter in range(1,11):
grover_circuit_iterN = QuantumCircuit(n)
grover_circuit_iterN.h(range(n))
for I in range(Niter):
grover_circuit_iterN.append(oracle_gate, list(range(n)))
grover_circuit_iterN.append(diffuser(n), list(range(n)))
grover_circuit_iterN.measure_all()
grover_circuit_iterN_tr = transpile(grover_circuit_iterN, backend=simulator)
results = simulator.run(grover_circuit_iterN_tr, shots=1024).result()
answer = results.get_counts()
x.append(Niter)
y.append(answer[format(Nsol,'b').zfill(n)])
plt.clf()
plt.scatter(x,y)
plt.xlabel('N_iterations')
plt.ylabel('# of correct observations (1 solution)')
plt.show()
+++ {"pycharm": {"name": "#%% md\n"}}
The result will show that the correct answer is observed with the highest probability when repeating the Grover's iteration
+++ {"pycharm": {"name": "#%% md\n"}}
Exercise : Consider the case of a single answer. Examine the most probable number of iterations obtained using simulator and the size of the list,
+++ {"pycharm": {"name": "#%% md\n"}}
(imp_simulator_multi)=
Now we consider searching for multiple data from the list. Let us modify the circuit to be able to find two integers
For example,
---
pycharm:
name: '#%%
'
---
N1 = 45
N2 = 26
oracle_2sol = QuantumCircuit(n)
# 45
oracle_2sol.x(1)
oracle_2sol.x(4)
oracle_2sol.h(n-1)
oracle_2sol.mcx(list(range(n-1)), n-1)
oracle_2sol.h(n-1)
oracle_2sol.x(1)
oracle_2sol.x(4)
# 26
oracle_2sol.x(0)
oracle_2sol.x(2)
oracle_2sol.x(5)
oracle_2sol.h(n-1)
oracle_2sol.mcx(list(range(n-1)), n-1)
oracle_2sol.h(n-1)
oracle_2sol.x(0)
oracle_2sol.x(2)
oracle_2sol.x(5)
oracle_2sol_gate = oracle_2sol.to_gate()
oracle_2sol_gate.name = "U_w(2sol)"
print(oracle_2sol)
x = []
y = []
for Niter in range(1,11):
grover_circuit_2sol_iterN = QuantumCircuit(n)
grover_circuit_2sol_iterN.h(range(n))
for I in range(Niter):
grover_circuit_2sol_iterN.append(oracle_2sol_gate, list(range(n)))
grover_circuit_2sol_iterN.append(diffuser(n), list(range(n)))
grover_circuit_2sol_iterN.measure_all()
#print('----- Niter =',Niter,' -----------')
#print(grover_circuit_2sol_iterN)
grover_circuit_2sol_iterN_tr = transpile(grover_circuit_2sol_iterN, backend=simulator)
results = simulator.run(grover_circuit_2sol_iterN_tr, shots=1024).result()
answer = results.get_counts()
#show_distribution(answer)
x.append(Niter)
y.append(answer[format(N1,'06b')]+answer[format(N2,'06b')])
plt.clf()
plt.scatter(x,y)
plt.xlabel('N_iterations')
plt.ylabel('# of correct observations (2 solutions)')
plt.show()
+++ {"pycharm": {"name": "#%% md\n"}}
In this case, the number of iterations to have the highest probability is smaller than that in the case of single answer. Is this what you expected, right?