Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
NVIDIA
GitHub Repository: NVIDIA/cuda-q-academic
Path: blob/main/qaoa-for-max-cut/Example-02-step-3-Solution.py
579 views
1
# SPDX-License-Identifier: Apache-2.0 AND CC-BY-NC-4.0
2
#
3
# Licensed under the Apache License, Version 2.0 (the "License");
4
# you may not use this file except in compliance with the License.
5
# You may obtain a copy of the License at
6
#
7
# http://www.apache.org/licenses/LICENSE-2.0
8
#
9
# Unless required by applicable law or agreed to in writing, software
10
# distributed under the License is distributed on an "AS IS" BASIS,
11
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
# See the License for the specific language governing permissions and
13
# limitations under the License.
14
15
# Defining functions to generate the Hamiltonian and Kernel for a given graph
16
# Necessary packages
17
import networkx as nx
18
from networkx import algorithms
19
from networkx.algorithms import community
20
import cudaq
21
from cudaq import spin
22
from cudaq.qis import *
23
import numpy as np
24
from typing import List, Tuple
25
from mpi4py import MPI
26
27
28
# Getting information about platform
29
cudaq.set_target("nvidia")
30
target = cudaq.get_target()
31
32
# Setting up MPI
33
comm = MPI.COMM_WORLD
34
rank = comm.Get_rank()
35
num_qpus = comm.Get_size()
36
37
38
#######################################################
39
# Step 1
40
#######################################################
41
# Function to return a dictionary of subgraphs of the input graph
42
# using the greedy modularity maximization algorithm
43
44
def subgraphpartition(G,n):
45
"""Divide the graph up into at most n subgraphs
46
47
Parameters
48
----------
49
G: networkX.Graph
50
Graph that we want to subdivdie
51
n : int
52
n is the maximum number of subgraphs in the partition
53
Returns
54
-------
55
dict of str : networkX.Graph
56
Dictionary of networkX graphs with a string as the key
57
"""
58
greedy_partition = community.greedy_modularity_communities(G, weight=None, resolution=1.1, cutoff=1, best_n=n)
59
number_of_subgraphs = len(greedy_partition)
60
61
graph_dictionary = {}
62
graph_names=[]
63
for i in range(number_of_subgraphs):
64
name='G'+str(i)
65
graph_names.append(name)
66
67
for i in range(number_of_subgraphs):
68
nodelist = sorted(list(greedy_partition[i]))
69
graph_dictionary[graph_names[i]] = nx.subgraph(G, nodelist)
70
71
return(graph_dictionary)
72
73
74
if rank ==0:
75
# Defining the example graph
76
# Random graph parameters
77
n = 35 # numnber of nodes
78
m = 80 # number of edges
79
seed = 20160 # seed random number generators for reproducibility
80
81
# Use seed for reproducibility
82
sampleGraph2 = nx.gnm_random_graph(n, m, seed=seed)
83
84
# Subdividing the graph
85
num_subgraphs_limit = min(12, len(sampleGraph2.nodes())) # maximum number of subgraphs for the partition
86
subgraph_dictionary = subgraphpartition(sampleGraph2,num_subgraphs_limit)
87
88
# Assign the subgraphs to the QPUs
89
number_of_subgraphs = len(sorted(subgraph_dictionary))
90
number_of_subgraphs_per_qpu = int(np.ceil(number_of_subgraphs/num_qpus))
91
92
keys_on_qpu ={}
93
94
for q in range(num_qpus):
95
keys_on_qpu[q]=[]
96
for k in range(number_of_subgraphs_per_qpu):
97
if (k*num_qpus+q < number_of_subgraphs):
98
key = sorted(subgraph_dictionary)[k*num_qpus+q]
99
keys_on_qpu[q].append(key)
100
print('Subgraph problems to be computed on each processor have been assigned')
101
102
103
# Distribute the subgraph data to the QPUs
104
for i in range(num_qpus):
105
subgraph_to_qpu ={}
106
for k in keys_on_qpu[i]:
107
subgraph_to_qpu[k]= subgraph_dictionary[k]
108
if i != 0:
109
comm.send(subgraph_to_qpu, dest=i, tag=rank)
110
else:
111
assigned_subgraph_dictionary = subgraph_to_qpu
112
else:
113
# Receive the subgraph data
114
assigned_subgraph_dictionary= comm.recv(source=0, tag=0)
115
print("Processor {} received {} from processor {}".format(rank,assigned_subgraph_dictionary, 0))
116
117
118
119
#######################################################
120
# Step 2
121
#######################################################
122
123
# Define a function to generate the Hamiltonian for a max cut problem using the graph G
124
125
def hamiltonian_max_cut(sources : List[int], targets : List[int]):
126
"""Hamiltonian for finding the max cut for the graph with edges defined by the pairs generated by source and target edges
127
128
Parameters
129
----------
130
sources: List[int]
131
list of the source vertices for edges in the graph
132
targets: List[int]
133
list of the target vertices for the edges in the graph
134
135
Returns
136
-------
137
cudaq.SpinOperator
138
Hamiltonian for finding the max cut of the graph defined by the given edges
139
"""
140
hamiltonian = 0
141
# Since our vertices may not be a list from 0 to n, or may not even be integers,
142
143
for i in range(len(sources)):
144
# Add a term to the Hamiltonian for the edge (u,v)
145
qubitu = sources[i]
146
qubitv = targets[i]
147
hamiltonian += 0.5*(spin.z(qubitu)*spin.z(qubitv)-spin.i(qubitu)*spin.i(qubitv))
148
149
return hamiltonian
150
151
# Problem Kernel
152
153
@cudaq.kernel
154
def qaoaProblem(qubit_0 : cudaq.qubit, qubit_1 : cudaq.qubit, alpha : float):
155
"""Build the QAOA gate sequence between two qubits that represent an edge of the graph
156
Parameters
157
----------
158
qubit_0: cudaq.qubit
159
Qubit representing the first vertex of an edge
160
qubit_1: cudaq.qubit
161
Qubit representing the second vertex of an edge
162
alpha: float
163
Free variable
164
165
166
"""
167
x.ctrl(qubit_0, qubit_1)
168
rz(2.0*alpha, qubit_1)
169
x.ctrl(qubit_0, qubit_1)
170
171
# Mixer Kernel
172
@cudaq.kernel
173
def qaoaMixer(qubit_0 : cudaq.qubit, beta : float):
174
"""Build the QAOA gate sequence that is applied to each qubit in the mixer portion of the circuit
175
Parameters
176
----------
177
qubit_0: cudaq.qubit
178
Qubit
179
beta: float
180
Free variable
181
182
183
"""
184
rx(2.0*beta, qubit_0)
185
186
187
# We now define the kernel_qaoa function which will be the QAOA circuit for our graph
188
# Since the QAOA circuit for max cut depends on the structure of the graph,
189
# we'll feed in global concrete variable values into the kernel_qaoa function for the qubit_count, layer_count, edges_src, edges_tgt.
190
# The types for these variables are restricted to Quake Values (e.g. qubit, int, List[int], ...)
191
# The thetas plaeholder will be our free parameters (the alphas and betas in the circuit diagrams depicted above)
192
@cudaq.kernel
193
def kernel_qaoa(qubit_count :int, layer_count: int, edges_src: List[int], edges_tgt: List[int], thetas : List[float]):
194
"""Build the QAOA circuit for max cut of the graph with given edges and nodes
195
Parameters
196
----------
197
qubit_count: int
198
Number of qubits in the circuit, which is the same as the number of nodes in our graph
199
layer_count : int
200
Number of layers in the QAOA kernel
201
edges_src: List[int]
202
List of the first (source) node listed in each edge of the graph, when the edges of the graph are listed as pairs of nodes
203
edges_tgt: List[int]
204
List of the second (target) node listed in each edge of the graph, when the edges of the graph are listed as pairs of nodes
205
thetas: List[float]
206
Free variables to be optimized
207
208
209
"""
210
# Let's allocate the qubits
211
qreg = cudaq.qvector(qubit_count)
212
213
# And then place the qubits in superposition
214
h(qreg)
215
216
# Each layer has two components: the problem kernel and the mixer
217
for i in range(layer_count):
218
# Add the problem kernel to each layer
219
for edge in range(len(edges_src)):
220
qubitu = edges_src[edge]
221
qubitv = edges_tgt[edge]
222
qaoaProblem(qreg[qubitu], qreg[qubitv], thetas[i])
223
# Add the mixer kernel to each layer
224
for j in range(qubit_count):
225
qaoaMixer(qreg[j],thetas[i+layer_count])
226
227
def find_optimal_parameters(G, layer_count, seed):
228
"""Function for finding the optimal parameters of QAOA for the max cut of a graph
229
Parameters
230
----------
231
G: networkX graph
232
Problem graph whose max cut we aim to find
233
layer_count : int
234
Number of layers in the QAOA circuit
235
seed : int
236
Random seed for reproducibility of results
237
238
Returns
239
-------
240
list[float]
241
Optimal parameters for the QAOA applied to the given graph G
242
"""
243
parameter_count: int = 2 * layer_count
244
245
# Problem parameters
246
nodes = sorted(list(nx.nodes(G)))
247
qubit_src = []
248
qubit_tgt = []
249
for u, v in nx.edges(G):
250
# We can use the index() command to read out the qubits associated with the vertex u and v.
251
qubit_src.append(nodes.index(u))
252
qubit_tgt.append(nodes.index(v))
253
# The number of qubits we'll need is the same as the number of vertices in our graph
254
qubit_count : int = len(nodes)
255
# Each layer of the QAOA kernel contains 2 parameters
256
parameter_count : int = 2*layer_count
257
258
# Specify the optimizer and its initial parameters.
259
optimizer = cudaq.optimizers.COBYLA()
260
np.random.seed(seed)
261
optimizer.initial_parameters = np.random.uniform(-np.pi, np.pi,
262
parameter_count)
263
264
# Pass the kernel, spin operator, and optimizer to `cudaq.vqe`.
265
optimal_expectation, optimal_parameters = cudaq.vqe(
266
kernel=kernel_qaoa,
267
spin_operator=hamiltonian_max_cut(qubit_src, qubit_tgt),
268
argument_mapper=lambda parameter_vector: (qubit_count, layer_count, qubit_src, qubit_tgt, parameter_vector),
269
optimizer=optimizer,
270
parameter_count=parameter_count)
271
272
return optimal_parameters
273
def qaoa_for_graph(G, layer_count, shots, seed):
274
"""Function for finding the max cut of a graph using QAOA
275
276
Parameters
277
----------
278
G: networkX graph
279
Problem graph whose max cut we aim to find
280
layer_count : int
281
Number of layers in the QAOA circuit
282
shots : int
283
Number of shots in the sampling subroutine
284
seed : int
285
Random seed for reproducibility of results
286
287
Returns
288
-------
289
str
290
Binary string representing the max cut coloring of the vertinces of the graph
291
"""
292
293
294
parameter_count: int = 2 * layer_count
295
296
# Problem parameters
297
nodes = sorted(list(nx.nodes(G)))
298
qubit_src = []
299
qubit_tgt = []
300
for u, v in nx.edges(G):
301
# We can use the index() command to read out the qubits associated with the vertex u and v.
302
qubit_src.append(nodes.index(u))
303
qubit_tgt.append(nodes.index(v))
304
# The number of qubits we'll need is the same as the number of vertices in our graph
305
qubit_count : int = len(nodes)
306
# Each layer of the QAOA kernel contains 2 parameters
307
parameter_count : int = 2*layer_count
308
309
optimal_parameters = find_optimal_parameters(G, layer_count, seed)
310
311
# Print the optimized parameters
312
print("Optimal parameters = ", optimal_parameters)
313
314
# Sample the circuit
315
counts = cudaq.sample(kernel_qaoa, qubit_count, layer_count, qubit_src, qubit_tgt, optimal_parameters, shots_count=shots)
316
print('most_probable outcome = ',counts.most_probable())
317
results = str(counts.most_probable())
318
return results
319
320
321
############################################################################
322
# On GPU with rank r, compute the subgraph solutions for the
323
# subgraphs in assigned_subgraph_dictionary that live on GPU r
324
############################################################################
325
layer_count =1
326
results = {}
327
new_seed_for_each_graph = rank # to give each subgraph solution different initial parameters
328
for key in assigned_subgraph_dictionary:
329
G = assigned_subgraph_dictionary[key]
330
results[key] = qaoa_for_graph(G, layer_count, shots = 10000, seed=6543+new_seed_for_each_graph)
331
new_seed_for_each_graph+=1
332
print('The max cut QAOA coloring for the subgraph',key,'is',results[key])
333
print('The results dictionary variable on GPU',rank,'is',results)
334
335
############################################################################
336
# Exercise to copy over the subgraph solutions from the individual GPUs
337
# back to GPU 0.
338
#############################################################################
339
# Let's introduce another MPI function that will be useful to
340
# iterate over all the GPUs.
341
size = comm.Get_size()
342
343
# Copy the results over to QPU 0 for consolidation
344
if rank!=0:
345
comm.send(results, dest=0, tag=0)
346
#print("{} sent by processor {}".format(results, rank))
347
else:
348
for j in range(1,size,1):
349
colors = comm.recv(source=j, tag=0)
350
print("Received {} from processor {}".format(colors, j))
351
for key in colors:
352
results[key]=colors[key]
353
#print("The results dictionary on GPU 0 =", results)
354
355
356
#######################################################
357
# Step 3
358
#######################################################
359
############################################################################
360
# Merge results on QPU 0
361
############################################################################
362
363
# Add color attribute to subgraphs and sampleGraph2 to record the subgraph solutions
364
# Plot sampleGraph2 with node colors inherited from the subgraph solutions
365
366
subgraphColors={}
367
368
for key in subgraph_dictionary:
369
subgraphColors[key]=[int(i) for i in results[key]]
370
371
372
for key in subgraph_dictionary:
373
G = subgraph_dictionary[key]
374
for v in sorted(list(nx.nodes(G))):
375
G.nodes[v]['color']=subgraphColors[key][sorted(list(nx.nodes(G))).index(v)]
376
sampleGraph2.nodes[v]['color']=G.nodes[v]['color']
377
378
379
# A function that takes as input a subgraph partition (in the form of a graph dictionary) and a vertex.
380
# The function should return the key associated with the subgraph that contains the given vertex.
381
382
def subgraph_of_vertex(graph_dictionary, vertex):
383
"""
384
A function that takes as input a subgraph partition (in the form of a graph dictionary) and a vertex.
385
The function should return the key associated with the subgraph that contains the given vertex.
386
387
Parameters
388
----------
389
graph_dictionary: dict of networkX.Graph with str as keys
390
v : int
391
v is a name for a vertex
392
393
Returns
394
-------
395
str
396
the key associated with the subgraph that contains the given vertex.
397
"""
398
# in case a vertex does not appear in the graph_dictionary, return the empty string
399
location = 'Vertex is not in the subgraph_dictionary'
400
401
for key in graph_dictionary:
402
if vertex in graph_dictionary[key].nodes():
403
location = key
404
return location
405
406
407
# First let's define a function that constructs the border graph
408
409
def border(G, subgraph_dictionary):
410
"""Build a graph made up of border vertices from the subgraph partition
411
412
Parameters
413
----------
414
G: networkX.Graph
415
Graph whose max cut we want to find
416
subgraph_dictionary: dict of networkX graph with str as keys
417
Each graph in the dictionary should be a subgraph of G
418
419
Returns
420
-------
421
networkX.Graph
422
Subgraph of G made up of only the edges connecting subgraphs in the subgraph dictionary
423
"""
424
borderG = nx.Graph()
425
for u,v in G.edges():
426
border = True
427
for key in subgraph_dictionary:
428
SubG = subgraph_dictionary[key]
429
edges = list(nx.edges(SubG))
430
if (u,v) in edges:
431
border = False
432
if border==True:
433
borderG.add_edge(u,v)
434
435
return borderG
436
437
438
# Create the borderGraph
439
borderGraph = border(sampleGraph2, subgraph_dictionary)
440
441
442
# Define the Hamiltonian for applying QAOA to the variables
443
# s_i where s_i = 1 means we will not flip the subgraph Gi's colors
444
# and s_i = -1 means we will flip the colors of subgraph G_i
445
446
def mHamiltonian(merger):
447
"""Hamiltonian for finding the optimal swap schedule for the subgraph partitioning encoded in the merger graph
448
449
Parameters
450
----------
451
merger: networkX.Graph
452
Weighted graph
453
454
Returns
455
-------
456
cudaq.SpinOperator
457
Hamiltonian for finding the optimal swap schedule for the subgraph partitioning encoded in the merger graph
458
"""
459
460
mergerHamiltonian = 0
461
mergerNodes = sorted(list(merger.nodes()))
462
# Add Hamiltonian terms for edges within a subgraph that contain a border element
463
for u, v in merger.edges():
464
qubitu = mergerNodes.index(u)
465
qubitv = mergerNodes.index(v)
466
mergerHamiltonian+= -1*merger[u][v]['penalty']*(spin.z(qubitu))*(spin.z(qubitv))
467
return mergerHamiltonian
468
469
470
# Define the mergerGraph and color code the vertices
471
# according to the subgraph that the vertex represents
472
def createMergerGraph(border, subgraphs):
473
"""Build a graph containing a vertex for each subgraph
474
and edges between vertices are added if there is an edge between
475
the corresponding subgraphs
476
477
Parameters
478
----------
479
border: networkX.Graph
480
Graph of connections between vertices in distinct subgraphs
481
subgraphs: dict of networkX graph with str as keys
482
The nodes of border should be a subset of the the graphs in the subgraphs dictionary
483
484
Returns
485
-------
486
networkX.Graph
487
Merger graph containing a vertex for each subgraph
488
and edges between vertices are added if there is an edge between
489
the corresponding subgraphs
490
"""
491
492
H = nx.Graph()
493
494
for u, v in border.edges():
495
subgraph_id_for_u = subgraph_of_vertex(subgraphs, u)
496
subgraph_id_for_v = subgraph_of_vertex(subgraphs, v)
497
if subgraph_id_for_u != subgraph_id_for_v:
498
H.add_edge(subgraph_id_for_u, subgraph_id_for_v)
499
return H
500
501
502
mergerGraph = createMergerGraph(borderGraph, subgraph_dictionary)
503
504
# Add attribute to capture the penalties of changing subgraph colors
505
506
# Initialize all the penalties to 0
507
nx.set_edge_attributes(mergerGraph, int(0), 'penalty')
508
509
# Compute penalties for each edge
510
for i, j in mergerGraph.edges():
511
penalty_ij = 0
512
for u in subgraph_dictionary[i]:
513
for neighbor_u in nx.all_neighbors(sampleGraph2, u):
514
if neighbor_u in subgraph_dictionary[j]:
515
if str(sampleGraph2.nodes[u]['color']) != str(sampleGraph2.nodes[neighbor_u]['color']):
516
penalty_ij += 1
517
else:
518
penalty_ij += -1
519
mergerGraph[i][j]['penalty'] = penalty_ij
520
521
522
# Graph the penalties of each edge
523
edge_labels = nx.get_edge_attributes(mergerGraph, 'penalty')
524
525
526
# Run QAOA on the merger subgraph to identify which subgraphs
527
# if any should change colors
528
529
layer_count_merger = 1 # set arbitrarily
530
parameter_count_merger: int = 2 * layer_count_merger
531
532
# Specify the optimizer and its initial parameters. Make it repeatable.
533
cudaq.set_random_seed(101)
534
optimizer_merger = cudaq.optimizers.COBYLA()
535
np.random.seed(101)
536
optimizer_merger.initial_parameters = np.random.uniform(-np.pi, np.pi,
537
parameter_count_merger)
538
optimizer_merger.max_iterations=150
539
540
merger_nodes = list(mergerGraph.nodes())
541
qubit_count = len(merger_nodes)
542
merger_edge_src = []
543
merger_edge_tgt = []
544
for u, v in nx.edges(mergerGraph):
545
# We can use the index() command to read out the qubits associated with the vertex u and v.
546
merger_edge_src.append(merger_nodes.index(u))
547
merger_edge_tgt.append(merger_nodes.index(v))
548
549
# Pass the kernel, spin operator, and optimizer to `cudaq.vqe`.
550
optimal_expectation, optimal_parameters = cudaq.vqe(
551
kernel=kernel_qaoa,
552
spin_operator=mHamiltonian(mergerGraph),
553
argument_mapper=lambda parameter_vector: (qubit_count, layer_count, merger_edge_src, merger_edge_tgt, parameter_vector),
554
optimizer=optimizer_merger,
555
parameter_count=parameter_count_merger,
556
shots = 10000)
557
558
# Print the optimized value and its parameters
559
print("Optimal value = ", optimal_expectation)
560
print("Optimal parameters = ", optimal_parameters)
561
562
# Sample the circuit using the optimized parameters
563
sample_number=15000
564
counts = cudaq.sample(kernel_qaoa, qubit_count, layer_count, merger_edge_src, merger_edge_tgt, optimal_parameters, shots_count=5000)
565
print(f"most_probable = {counts.most_probable()}")
566
567
# Merger results
568
mergerResultsString=counts.most_probable()
569
570
## Record a new coloring of the sampleGraph2 according to the merger results
571
# with a node vertex attribute 'new_color'
572
flipGraphColors={}
573
574
mergerNodes = sorted(list(nx.nodes(mergerGraph)))
575
for u in mergerNodes:
576
indexu = mergerNodes.index(u)
577
flipGraphColors[u]=int(mergerResultsString[indexu])
578
579
for key in subgraph_dictionary:
580
if flipGraphColors[key]==1:
581
for u in subgraph_dictionary[key].nodes():
582
sampleGraph2.nodes[u]['new_color'] = 1 - sampleGraph2.nodes[u]['color']
583
else:
584
for u in subgraph_dictionary[key].nodes():
585
sampleGraph2.nodes[u]['new_color'] = sampleGraph2.nodes[u]['color']
586
587
# Compute the new cut of the larger graph based on the new colors
588
max_cut = 0
589
max_cut_edges = []
590
for u, v in sampleGraph2.edges():
591
if str(sampleGraph2.nodes[u]['new_color']) != str(sampleGraph2.nodes[v]['new_color']):
592
max_cut+=1
593
max_cut_edges.append((u,v))
594
print('The max cut value approximated from the Divide and Conquer QAOA is',max_cut)
595
596