-
Notifications
You must be signed in to change notification settings - Fork 3
/
Copy pathgrovers_algorithm.py
361 lines (291 loc) · 15.5 KB
/
grovers_algorithm.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
from argparse import ArgumentParser, Namespace, BooleanOptionalAction
from qiskit import QuantumCircuit as qc
from qiskit import QuantumRegister as qr
from qiskit import transpile
from qiskit_aer import AerSimulator
from qiskit.result import Counts
from matplotlib.pyplot import show, subplots, xticks, yticks
from math import pi, sqrt
from heapq import nlargest
class GroversAlgorithm:
def __init__(self,
title: str = "Grover's Algorithm",
n_qubits: int = 5,
search: set[int] = { 11, 9, 0, 3 },
shots: int = 1000,
fontsize: int = 10,
print: bool = False,
combine_states: bool = False) -> None:
"""
Simulate Grover's algorithm using Qiskit's AerSimulator.
Args:
title (str, optional): Window title. Defaults to "Grover's Algorithm".
n_qubits (int, optional): Number of qubits. Defaults to 5.
search (set[int], optional): Set of nonnegative integers to search for using Grover's algorithm. Defaults to { 11, 9, 0, 3 }.
shots (int, optional): Amount of times the algorithm is simulated. Defaults to 10000.
fontsize (int, optional): Histogram's font size. Defaults to 10.
print (bool, optional): Whether or not to print quantum circuit(s). Defaults to False.
combine_states (bool, optional): Whether to combine all non-winning states into 1 bar labeled "Others" or not. Defaults to False.
"""
# Parsing command line arguments
self._parser: ArgumentParser = ArgumentParser(description = "Run grover's algorithm via command line", add_help = False)
self._init_parser(title, n_qubits, search, shots, fontsize, print, combine_states)
self._args: Namespace = self._parser.parse_args()
# Set of nonnegative ints to search for
self.search: set[int] = set(self._args.search)
# Set of m N-qubit binary strings representing target state(s) (i.e. self.search in base 2)
self._targets: set[str] = { f"{s:0{self._args.n_qubits}b}" for s in self.search }
# N-qubit quantum register
self._qubits: qr = qr(self._args.n_qubits, "qubit")
def _print_circuit(self, circuit: qc, name: str) -> None:
"""Print quantum circuit.
Args:
circuit (qc): Quantum circuit to print.
name (str): Quantum circuit's name.
"""
print(f"\n{name}:\n{circuit}")
def _oracle(self, targets: set[str]) -> qc:
"""Mark target state(s) with negative phase.
Args:
targets (set[str]): N-qubit binary string(s) representing target state(s).
Returns:
qc: Quantum circuit representation of oracle.
"""
# Create N-qubit quantum circuit for oracle
oracle = qc(self._qubits, name = "Oracle")
for target in targets:
# Reverse target state since Qiskit uses little-endian for qubit ordering
target = target[::-1]
# Flip zero qubits in target
for i in range(self._args.n_qubits):
if target[i] == "0":
# Pauli-X gate
oracle.x(i)
# Simulate (N - 1)-control Z gate
# 1. Hadamard gate
oracle.h(self._args.n_qubits - 1)
# 2. (N - 1)-control Toffoli gate
oracle.mcx(list(range(self._args.n_qubits - 1)), self._args.n_qubits - 1)
# 3. Hadamard gate
oracle.h(self._args.n_qubits - 1)
# Flip back to original state
for i in range(self._args.n_qubits):
if target[i] == "0":
# Pauli-X gate
oracle.x(i)
# Display oracle, if applicable
if self._args.print: self._print_circuit(oracle, "ORACLE")
return oracle
def _diffuser(self) -> qc:
"""Amplify target state(s) amplitude, which decreases the amplitudes of other states
and increases the probability of getting the correct solution (i.e. target state(s)).
Returns:
qc: Quantum circuit representation of diffuser (i.e. Grover's diffusion operator).
"""
# Create N-qubit quantum circuit for diffuser
diffuser = qc(self._qubits, name = "Diffuser")
# Hadamard gate
diffuser.h(self._qubits)
# Oracle with all zero target state
diffuser.append(self._oracle({"0" * self._args.n_qubits}), list(range(self._args.n_qubits)))
# Hadamard gate
diffuser.h(self._qubits)
# Display diffuser, if applicable
if self._args.print: self._print_circuit(diffuser, "DIFFUSER")
return diffuser
def _grover(self) -> qc:
"""Create quantum circuit representation of Grover's algorithm,
which consists of 4 parts: (1) state preparation/initialization,
(2) oracle, (3) diffuser, and (4) measurement of resulting state.
Steps 2-3 are repeated an optimal number of times (i.e. Grover's
iterate) in order to maximize probability of success of Grover's algorithm.
Returns:
qc: Quantum circuit representation of Grover's algorithm.
"""
# Create N-qubit quantum circuit for Grover's algorithm
grover = qc(self._qubits, name = "Grover Circuit")
# Intialize qubits with Hadamard gate (i.e. uniform superposition)
grover.h(self._qubits)
# # Apply barrier to separate steps
grover.barrier()
# Apply oracle and diffuser (i.e. Grover operator) optimal number of times
for _ in range(int((pi / 4) * sqrt((2 ** self._args.n_qubits) / len(self._targets)))):
grover.append(self._oracle(self._targets), list(range(self._args.n_qubits)))
grover.append(self._diffuser(), list(range(self._args.n_qubits)))
# Measure all qubits once finished
grover.measure_all()
# Display grover circuit, if applicable
if self._args.print: self._print_circuit(grover, "GROVER CIRCUIT")
return grover
def _outcome(self, winners: list[str], counts: Counts) -> None:
"""Print top measurement(s) (state(s) with highest frequency)
and target state(s) in binary and decimal form, determine
if top measurement(s) equals target state(s), then print result.
Args:
winners (list[str]): State(s) (N-qubit binary string(s))
with highest probability of being measured.
counts (Counts): Each state and its respective frequency.
"""
print("WINNER(S):")
print(f"Binary = {winners}\nDecimal = {[ int(key, 2) for key in winners ]}\n")
print("TARGET(S):")
print(f"Binary = {self._targets}\nDecimal = {self.search}\n")
if not all(key in self._targets for key in winners): print("Target(s) not found...")
else:
winners_frequency, total = 0, 0
for value, frequency in counts.items():
if value in winners:
winners_frequency += frequency
total += frequency
print(f"Target(s) found with {winners_frequency / total:.2%} accuracy!")
def _show_histogram(self, histogram_data) -> None:
"""Print outcome and display histogram of simulation results.
Args:
data: Each state and its respective frequency.
"""
# State(s) with highest count and their frequencies
winners = { winner : histogram_data.get(winner) for winner in nlargest(len(self._targets), histogram_data, key = histogram_data.get) }
# Print outcome
self._outcome(list(winners.keys()), histogram_data)
# X-axis and y-axis value(s) for winners, respectively
winners_x_axis = [ str(winner) for winner in [*winners] ]
winners_y_axis = [ *winners.values() ]
# All other states (i.e. non-winners) and their frequencies
others = { state : frequency for state, frequency in histogram_data.items() if state not in winners }
# X-axis and y-axis value(s) for all other states, respectively
other_states_x_axis = "Others" if self._args.combine else [*others]
other_states_y_axis = [ sum([*others.values()]) ] if self._args.combine else [ *others.values() ]
# Create histogram for simulation results
figure, axes = subplots(num = "Grover's Algorithm — Results", layout = "constrained")
axes.bar(winners_x_axis, winners_y_axis, color = "green", label = "Target")
axes.bar(other_states_x_axis, other_states_y_axis, color = "red", label = "Non-target")
axes.legend(fontsize = self._args.fontsize)
axes.grid(axis = "y", ls = "dashed")
axes.set_axisbelow(True)
# Set histogram title, x-axis title, and y-axis title respectively
axes.set_title(f"Outcome of {self._args.shots} Simulations", fontsize = int(self._args.fontsize * 1.45))
axes.set_xlabel("States (Qubits)", fontsize = int(self._args.fontsize * 1.3))
axes.set_ylabel("Frequency", fontsize = int(self._args.fontsize * 1.3))
# Set font properties for x-axis and y-axis labels respectively
xticks(fontsize = self._args.fontsize, family = "monospace", rotation = 0 if self._args.combine else 70)
yticks(fontsize = self._args.fontsize, family = "monospace")
# Set properties for annotations displaying frequency above each bar
annotation = axes.annotate("",
xy = (0, 0),
xytext = (5, 5),
xycoords = "data",
textcoords = "offset pixels",
ha = "center",
va = "bottom",
family = "monospace",
weight = "bold",
fontsize = self._args.fontsize,
bbox = dict(facecolor = "white", alpha = 0.4, edgecolor = "None", pad = 0)
)
def _hover(event) -> None:
"""Display frequency above each bar upon hovering over it.
Args:
event: Matplotlib event.
"""
visibility = annotation.get_visible()
if event.inaxes == axes:
for bars in axes.containers:
for bar in bars:
cont, _ = bar.contains(event)
if cont:
x, y = bar.get_x() + bar.get_width() / 2, bar.get_y() + bar.get_height()
annotation.xy = (x, y)
annotation.set_text(y)
annotation.set_visible(True)
figure.canvas.draw_idle()
return
if visibility:
annotation.set_visible(False)
figure.canvas.draw_idle()
# Display histogram
id = figure.canvas.mpl_connect("motion_notify_event", _hover)
show()
figure.canvas.mpl_disconnect(id)
def run(self) -> None:
"""
Run Grover's algorithm simulation.
"""
# Simulate Grover's algorithm locally
backend = AerSimulator(method = "density_matrix")
# Generate optimized grover circuit for simulation
transpiled_circuit = transpile(self._grover(), backend, optimization_level = 2)
# Run Grover's algorithm simulation
job = backend.run(transpiled_circuit, shots = self._args.shots)
# Get simulation results
results = job.result()
# Get each state's histogram data (including frequency) from simulation results
data = results.get_counts()
# Display simulation results
self._show_histogram(data)
def _init_parser(self,
title: str,
n_qubits: int,
search: set[int],
shots: int,
fontsize: int,
print: bool,
combine_states: bool) -> None:
"""
Helper method to initialize command line argument parser.
Args:
title (str): Window title.
n_qubits (int): Number of qubits.
search (set[int]): Set of nonnegative integers to search for using Grover's algorithm.
shots (int): Amount of times the algorithm is simulated.
fontsize (int): Histogram's font size.
print (bool): Whether or not to print quantum circuit(s).
combine_states (bool): Whether to combine all non-winning states into 1 bar labeled "Others" or not.
"""
self._parser.add_argument("-H, --help",
action = "help",
help = "show this help message and exit")
self._parser.add_argument("-T, --title",
type = str,
default = title,
dest = "title",
metavar = "<title>",
help = f"window title (default: \"{title}\")")
self._parser.add_argument("-n, --n-qubits",
type = int,
default = n_qubits,
dest = "n_qubits",
metavar = "<n_qubits>",
help = f"number of qubits (default: {n_qubits})")
self._parser.add_argument("-s, --search",
default = search,
type = int,
nargs = "+",
dest = "search",
metavar = "<search>",
help = f"nonnegative integers to search for with Grover's algorithm (default: {search})")
self._parser.add_argument("-S, --shots",
type = int,
default = shots,
dest = "shots",
metavar = "<shots>",
help = f"amount of times the algorithm is simulated (default: {shots})")
self._parser.add_argument("-f, --font-size",
type = int,
default = fontsize,
dest = "fontsize",
metavar = "<font_size>",
help = f"histogram's font size (default: {fontsize})")
self._parser.add_argument("-p, --print",
action = BooleanOptionalAction,
type = bool,
default = print,
dest = "print",
help = f"whether or not to print quantum circuit(s) (default: {print})")
self._parser.add_argument("-c, --combine",
action = BooleanOptionalAction,
type = bool,
default = combine_states,
dest = "combine",
help = f"whether to combine all non-winning states into 1 bar labeled \"Others\" or not (default: {combine_states})")
if __name__ == "__main__":
GroversAlgorithm().run()