Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
NVIDIA
GitHub Repository: NVIDIA/cuda-q-academic
Path: blob/main/qaoa-for-max-cut/Example-04-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
# Necessary packages
16
import networkx as nx
17
from networkx import algorithms
18
from networkx.algorithms import community
19
import cudaq
20
from cudaq import spin
21
from cudaq.qis import *
22
import numpy as np
23
from mpi4py import MPI
24
from typing import List
25
26
27
# Getting information about platform
28
cudaq.set_target("nvidia")
29
target = cudaq.get_target()
30
31
# Setting up MPI
32
comm = MPI.COMM_WORLD
33
rank = comm.Get_rank()
34
num_qpus = comm.Get_size()
35
36
# Define a function to generate the Hamiltonian for a max cut problem using the graph G
37
def hamiltonian_max_cut(sources : List[int], targets : List[int], weights : List[float]):
38
"""Hamiltonian for finding the max cut for the graph with edges defined by the pairs generated by source and target edges
39
40
Parameters
41
----------
42
sources: List[int]
43
list of the source vertices for edges in the graph
44
targets: List[int]
45
list of the target vertices for the edges in the graph
46
weights : List[float]
47
list of the weight of the edge determined by the source and target with the same index
48
Returns
49
-------
50
cudaq.SpinOperator
51
Hamiltonian for finding the max cut of the graph defined by the given edges
52
"""
53
hamiltonian = 0
54
# Since our vertices may not be a list from 0 to n, or may not even be integers,
55
56
for i in range(len(sources)):
57
# Add a term to the Hamiltonian for the edge (u,v)
58
qubitu = sources[i]
59
qubitv = targets[i]
60
edge_weight = weights[i]
61
hamiltonian += 0.5*edge_weight*(spin.z(qubitu)*spin.z(qubitv)-spin.i(qubitu)*spin.i(qubitv))
62
63
return hamiltonian
64
65
# Problem Kernel
66
67
@cudaq.kernel
68
def qaoaProblem(qubit_0 : cudaq.qubit, qubit_1 : cudaq.qubit, alpha : float):
69
"""Build the QAOA gate sequence between two qubits that represent an edge of the graph
70
Parameters
71
----------
72
qubit_0: cudaq.qubit
73
Qubit representing the first vertex of an edge
74
qubit_1: cudaq.qubit
75
Qubit representing the second vertex of an edge
76
alpha: float
77
Free variable
78
79
80
"""
81
x.ctrl(qubit_0, qubit_1)
82
rz(2.0*alpha, qubit_1)
83
x.ctrl(qubit_0, qubit_1)
84
85
# Mixer Kernel
86
@cudaq.kernel
87
def qaoaMixer(qubit_0 : cudaq.qubit, beta : float):
88
"""Build the QAOA gate sequence that is applied to each qubit in the mixer portion of the circuit
89
Parameters
90
----------
91
qubit_0: cudaq.qubit
92
Qubit
93
beta: float
94
Free variable
95
96
97
"""
98
rx(2.0*beta, qubit_0)
99
100
101
# We now define the kernel_qaoa function which will be the QAOA circuit for our graph
102
# Since the QAOA circuit for max cut depends on the structure of the graph,
103
# we'll feed in global concrete variable values into the kernel_qaoa function for the qubit_count, layer_count, edges_src, edges_tgt.
104
# The types for these variables are restricted to Quake Values (e.g. qubit, int, List[int], ...)
105
# The thetas plaeholder will be our free parameters (the alphas and betas in the circuit diagrams depicted above)
106
@cudaq.kernel
107
def kernel_qaoa(qubit_count :int, layer_count: int, edges_src: List[int], edges_tgt: List[int], thetas : List[float]):
108
"""Build the QAOA circuit for max cut of the graph with given edges and nodes
109
Parameters
110
----------
111
qubit_count: int
112
Number of qubits in the circuit, which is the same as the number of nodes in our graph
113
layer_count : int
114
Number of layers in the QAOA kernel
115
edges_src: List[int]
116
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
117
edges_tgt: List[int]
118
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
119
thetas: List[float]
120
Free variables to be optimized
121
122
"""
123
# Let's allocate the qubits
124
qreg = cudaq.qvector(qubit_count)
125
126
# And then place the qubits in superposition
127
h(qreg)
128
129
# Each layer has two components: the problem kernel and the mixer
130
for i in range(layer_count):
131
# Add the problem kernel to each layer
132
for edge in range(len(edges_src)):
133
qubitu = edges_src[edge]
134
qubitv = edges_tgt[edge]
135
qaoaProblem(qreg[qubitu], qreg[qubitv], thetas[i])
136
# Add the mixer kernel to each layer
137
for j in range(qubit_count):
138
qaoaMixer(qreg[j],thetas[i+layer_count])
139
140
def find_optimal_parameters(G, layer_count, seed):
141
"""Function for finding the optimal parameters of QAOA for the max cut of a graph
142
Parameters
143
----------
144
G: networkX graph
145
Problem graph whose max cut we aim to find
146
layer_count : int
147
Number of layers in the QAOA circuit
148
seed : int
149
Random seed for reproducibility of results
150
151
Returns
152
-------
153
list[float]
154
Optimal parameters for the QAOA applied to the given graph G
155
"""
156
157
158
# Problem parameters
159
nodes = sorted(list(nx.nodes(G)))
160
qubit_src = []
161
qubit_tgt = []
162
weights = []
163
for u, v in nx.edges(G):
164
# We can use the index() command to read out the qubits associated with the vertex u and v.
165
qubit_src.append(nodes.index(u))
166
qubit_tgt.append(nodes.index(v))
167
weights.append(G.edges[u,v]['weight'])
168
# The number of qubits we'll need is the same as the number of vertices in our graph
169
qubit_count : int = len(nodes)
170
# Each layer of the QAOA kernel contains 2 parameters
171
parameter_count : int = 2*layer_count
172
173
# Specify the optimizer and its initial parameters.
174
optimizer = cudaq.optimizers.COBYLA()
175
np.random.seed(seed)
176
cudaq.set_random_seed(seed)
177
optimizer.initial_parameters = np.random.uniform(-np.pi, np.pi,
178
parameter_count)
179
180
# Pass the kernel, spin operator, and optimizer to `cudaq.vqe`.
181
optimal_expectation, optimal_parameters = cudaq.vqe(
182
kernel=kernel_qaoa,
183
spin_operator=hamiltonian_max_cut(qubit_src, qubit_tgt, weights),
184
argument_mapper=lambda parameter_vector: (qubit_count, layer_count, qubit_src, qubit_tgt, parameter_vector),
185
optimizer=optimizer,
186
parameter_count=parameter_count)
187
188
return optimal_parameters
189
190
# These function from Lab 2 are used to identify the subgraph
191
# that contains a given vertex, and identify the vertices of the parent graph
192
# that lie on the border of the subgraphs in the subgraph dictionary
193
194
def subgraph_of_vertex(graph_dictionary, vertex):
195
"""
196
A function that takes as input a subgraph partition (in the form of a graph dictionary) and a vertex.
197
The function should return the key associated with the subgraph that contains the given vertex.
198
199
Parameters
200
----------
201
graph_dictionary: dict of networkX.Graph with str as keys
202
v : int
203
v is a name for a vertex
204
205
Returns
206
-------
207
str
208
the key associated with the subgraph that contains the given vertex.
209
"""
210
# in case a vertex does not appear in the graph_dictionary, return the empty string
211
location = ''
212
213
for key in graph_dictionary:
214
if vertex in graph_dictionary[key].nodes():
215
location = key
216
return location
217
218
def border(G, subgraph_dictionary):
219
"""Build a graph made up of border vertices from the subgraph partition
220
221
Parameters
222
----------
223
G: networkX.Graph
224
Graph whose max cut we want to find
225
subgraph_dictionary: dict of networkX graph with str as keys
226
Each graph in the dictionary should be a subgraph of G
227
228
Returns
229
-------
230
networkX.Graph
231
Subgraph of G made up of only the edges connecting subgraphs in the subgraph dictionary
232
"""
233
borderGraph = nx.Graph()
234
for u,v in G.edges():
235
border = True
236
for key in subgraph_dictionary:
237
SubG = subgraph_dictionary[key]
238
edges = list(nx.edges(SubG))
239
if (u,v) in edges:
240
border = False
241
if border==True:
242
borderGraph.add_edge(u,v)
243
244
return borderGraph
245
246
def cutvalue(G):
247
"""Returns the cut value of G based on the coloring of the nodes of G
248
249
Parameters
250
----------
251
G: networkX.Graph
252
Graph with weighted edges and with binary value colors assigned to the vertices
253
254
Returns
255
-------
256
int
257
cut value of the graph determined by the vertex colors and edge weights
258
"""
259
260
cut = 0
261
for u, v in G.edges():
262
if str(G.nodes[u]['color']) != str(G.nodes[v]['color']):
263
cut+=G.edges[u,v]['weight']
264
return cut
265
266
def subgraphpartition(G,n, name, globalGraph):
267
"""Divide the graph up into at most n subgraphs
268
269
Parameters
270
----------
271
G: networkX.Graph
272
Graph that we want to subdivivde which lives inside of or is equatl to globalGraph
273
n : int
274
n is the maximum number of subgraphs in the partition
275
name : str
276
prefix for the graphs (in our case we'll use 'Global')
277
globalGraph: networkX.Graph
278
original problem graph
279
280
Returns
281
-------
282
dict of str : networkX.Graph
283
Dictionary of networkX graphs with a string as the key
284
"""
285
greedy_partition = community.greedy_modularity_communities(G, weight='weight', resolution=1.1, cutoff=1, best_n=n)
286
number_of_subgraphs = len(greedy_partition)
287
288
graph_dictionary = {}
289
graph_names=[]
290
for i in range(number_of_subgraphs):
291
subgraphname=name+':'+str(i)
292
graph_names.append(subgraphname)
293
294
for i in range(number_of_subgraphs):
295
nodelist = sorted(list(greedy_partition[i]))
296
graph_dictionary[graph_names[i]] = nx.subgraph(globalGraph, nodelist)
297
298
return(graph_dictionary)
299
300
301
def qaoa_for_graph(G, layer_count, shots, seed):
302
"""Function for finding the max cut of a graph using QAOA
303
304
Parameters
305
----------
306
G: networkX graph
307
Problem graph whose max cut we aim to find
308
layer_count : int
309
Number of layers in the QAOA circuit
310
shots : int
311
Number of shots in the sampling subroutine
312
seed : int
313
Random seed for reproducibility of results
314
315
Returns
316
-------
317
str
318
Binary string representing the max cut coloring of the vertinces of the graph
319
"""
320
if nx.number_of_nodes(G) ==1 or nx.number_of_edges(G) ==0:
321
# The first condition implies the second condition so we really don't need
322
# to consider the case nx.number_of_nodes(G) ==1
323
results = ''
324
for u in list(nx.nodes(G)):
325
np.random.seed(seed)
326
random_assignment = str(np.random.randint(0, 1))
327
results+=random_assignment
328
329
else:
330
parameter_count: int = 2 * layer_count
331
332
# Problem parameters
333
nodes = sorted(list(nx.nodes(G)))
334
qubit_src = []
335
qubit_tgt = []
336
for u, v in nx.edges(G):
337
# We can use the index() command to read out the qubits associated with the vertex u and v.
338
qubit_src.append(nodes.index(u))
339
qubit_tgt.append(nodes.index(v))
340
# The number of qubits we'll need is the same as the number of vertices in our graph
341
qubit_count : int = len(nodes)
342
# Each layer of the QAOA kernel contains 2 parameters
343
parameter_count : int = 2*layer_count
344
345
optimal_parameters = find_optimal_parameters(G, layer_count, seed)
346
347
# Print the optimized parameters
348
print("Optimal parameters = ", optimal_parameters)
349
cudaq.set_random_seed(seed)
350
# Sample the circuit
351
counts = cudaq.sample(kernel_qaoa, qubit_count, layer_count, qubit_src, qubit_tgt, optimal_parameters, shots_count=shots)
352
print('most_probable outcome = ',counts.most_probable())
353
results = str(counts.most_probable())
354
return results
355
356
# The functions below are based on code from Lab 2
357
# Define the mergerGraph for a given border graph and subgraph
358
# partitioning. And color code the vertices
359
# according to the subgraph that the vertex represents
360
def createMergerGraph(border, subgraphs):
361
"""Build a graph containing a vertex for each subgraph
362
and edges between vertices are added if there is an edge between
363
the corresponding subgraphs
364
365
Parameters
366
----------
367
border : networkX.Graph
368
Graph of connections between vertices in distinct subgraphs
369
subgraphs : dict of networkX graph with str as keys
370
The nodes of border should be a subset of the the graphs in the subgraphs dictionary
371
372
Returns
373
-------
374
networkX.Graph
375
Merger graph containing a vertex for each subgraph
376
and edges between vertices are added if there is an edge between
377
the corresponding subgraphs
378
"""
379
M = nx.Graph()
380
381
for u, v in border.edges():
382
subgraph_id_for_u = subgraph_of_vertex(subgraphs, u)
383
subgraph_id_for_v = subgraph_of_vertex(subgraphs, v)
384
if subgraph_id_for_u != subgraph_id_for_v:
385
M.add_edge(subgraph_id_for_u, subgraph_id_for_v)
386
return M
387
388
389
# Compute the penalties for edges in the supplied mergerGraph
390
# for the subgraph partitioning of graph G
391
def merger_graph_penalties(mergerGraph, subgraph_dictionary, G):
392
"""Compute penalties for the edges in the mergerGraph and add them
393
as edge attributes.
394
395
Parameters
396
----------
397
mergerGraph : networkX.Graph
398
Graph of connections between vertices in distinct subgraphs of G
399
subgraph_dictionary : dict of networkX graph with str as keys
400
subgraphs of G that are represented as nodes in the mergerGraph
401
G : networkX.Graph
402
graph whose vertices has an attribute 'color'
403
404
Returns
405
-------
406
networkX.Graph
407
Merger graph containing penalties
408
"""
409
nx.set_edge_attributes(mergerGraph, int(0), 'penalty')
410
for i, j in mergerGraph.edges():
411
penalty_ij = 0
412
for u in nx.nodes(subgraph_dictionary[i]):
413
for neighbor_u in nx.all_neighbors(G, u):
414
if neighbor_u in nx.nodes(subgraph_dictionary[j]):
415
if str(G.nodes[u]['color']) != str(G.nodes[neighbor_u]['color']):
416
penalty_ij += G.edges[u,neighbor_u]['weight']
417
else:
418
penalty_ij += -G.edges[u,neighbor_u]['weight']
419
mergerGraph[i][j]['penalty'] = penalty_ij
420
return mergerGraph
421
422
423
# Define the Hamiltonian for applying QAOA during the merger stage
424
# The variables s_i are defined so that s_i = 1 means we will not
425
# flip the subgraph Gi's colors and s_i = -1 means we will flip the colors of subgraph G_i
426
def mHamiltonian(merger_edge_src, merger_edge_tgt, penalty):
427
"""Hamiltonian for finding the optimal swap schedule for the subgraph partitioning encoded in the merger graph
428
429
Parameters
430
----------
431
merger_edge_src: List[int]
432
list of the source vertices of edges of a graph
433
merger_edge_tgt: List[int]
434
list of target vertices of edges of a graph
435
penalty: List[int]
436
list of penalty terms associated with the edge determined by the source and target vertex of the same index
437
438
Returns
439
-------
440
cudaq.SpinOperator
441
Hamiltonian for finding the optimal swap schedule for the subgraph partitioning encoded in the merger graph
442
"""
443
mergerHamiltonian = 0
444
445
# Add Hamiltonian terms for edges within a subgraph that contain a border element
446
for i in range(len(merger_edge_src)):
447
# Add a term to the Hamiltonian for the edge (u,v)
448
qubitu = merger_edge_src[i]
449
qubitv = merger_edge_tgt[i]
450
mergerHamiltonian+= -penalty[i]*(spin.z(qubitu))*(spin.z(qubitv))
451
return mergerHamiltonian
452
453
# A function to carry out QAOA during the merger stage of the
454
# divide-and-conquer QAOA algorithm for graph G, its subgraphs (graph_dictionary)
455
# and merger_graph
456
457
def merging(G, graph_dictionary, merger_graph):
458
"""
459
Using QAOA, identify which subgraphs should be in the swap schedule (e.g. which subgraphs will require
460
flipping of the colors when merging the subgraph solutions into a solution of the graph G
461
462
Parameters
463
----------
464
G : networkX.Graph
465
Graph with vertex color attributes
466
graph_dictionary : dict of networkX graph with str as keys
467
subgraphs of G
468
mergerGraph : networkX.Graph
469
Graph whose vertices represent subgraphs in the graph_dictionary
470
471
Returns
472
-------
473
str
474
returns string of 0s and 1s indicating which subgraphs should have their colors swapped
475
"""
476
477
merger_graph_with_penalties = merger_graph_penalties(merger_graph,graph_dictionary, G)
478
# In the event that the merger penalties are not trivial, run QAOA, else don't flip any graph colors
479
if (True in (merger_graph_with_penalties[u][v]['penalty'] != 0 for u, v in nx.edges(merger_graph_with_penalties))):
480
481
penalty = []
482
merger_edge_src = []
483
merger_edge_tgt = []
484
merger_nodes = sorted(list(merger_graph_with_penalties.nodes()))
485
for u, v in nx.edges(merger_graph_with_penalties):
486
# We can use the index() command to read out the qubits associated with the vertex u and v.
487
merger_edge_src.append(merger_nodes.index(u))
488
merger_edge_tgt.append(merger_nodes.index(v))
489
penalty.append(merger_graph_with_penalties[u][v]['penalty'])
490
491
merger_Hamiltonian = mHamiltonian(merger_edge_src, merger_edge_tgt, penalty)
492
493
# Run QAOA on the merger subgraph to identify which subgraphs
494
# if any should change colors
495
layer_count_merger = 1 # Edit this line to change the layer count
496
parameter_count_merger: int = 2 * layer_count_merger
497
merger_seed = 12345 # Edit this line to change the seed for the merger call to QAOA
498
nodes_merger = sorted(list(nx.nodes(merger_graph)))
499
merger_edge_src = []
500
merger_edge_tgt = []
501
for u, v in nx.edges(merger_graph_with_penalties):
502
# We can use the index() command to read out the qubits associated with the vertex u and v.
503
merger_edge_src.append(nodes_merger.index(u))
504
merger_edge_tgt.append(nodes_merger.index(v))
505
# The number of qubits we'll need is the same as the number of vertices in our graph
506
qubit_count_merger : int = len(nodes_merger)
507
508
# Specify the optimizer and its initial parameters. Make it repeatable.
509
cudaq.set_random_seed(merger_seed)
510
optimizer_merger = cudaq.optimizers.COBYLA()
511
np.random.seed(merger_seed)
512
optimizer_merger.initial_parameters = np.random.uniform(-np.pi, np.pi,
513
parameter_count_merger)
514
optimizer_merger.max_iterations=150
515
# Pass the kernel, spin operator, and optimizer to `cudaq.vqe`.
516
optimal_expectation, optimal_parameters = cudaq.vqe(
517
kernel=kernel_qaoa,
518
spin_operator=merger_Hamiltonian,
519
argument_mapper=lambda parameter_vector: (qubit_count_merger, layer_count_merger, merger_edge_src, merger_edge_tgt, parameter_vector),
520
optimizer=optimizer_merger,
521
parameter_count=parameter_count_merger,
522
shots = 20000)
523
524
# Sample the circuit using the optimized parameters
525
# Sample enough times to distinguish the most_probable outcome for
526
# merger graphs with 12 vertices
527
sample_number=20000
528
counts = cudaq.sample(kernel_qaoa, qubit_count_merger, layer_count_merger, merger_edge_src, merger_edge_tgt, optimal_parameters, shots_count=sample_number)
529
mergerResultsString = str(counts.most_probable())
530
531
else:
532
mergerResultsList = [0]*nx.number_of_nodes(merger_graph)
533
mergerResultsString = ''.join(str(x) for x in mergerResultsList)
534
print('Merging stage is trivial')
535
return mergerResultsString
536
537
538
539
# Next we define some functions to keep track of the unaltered cuts
540
# (recorded as unaltered_colors) and the merged cuts (recorded as new_colors).
541
# The new_colors are derived from flipping the colors
542
# of all the nodes in a subgraph based on the flip_colors variable which
543
# captures the solution to the merger QAOA problem.
544
545
def unaltered_colors(G, graph_dictionary, max_cuts):
546
"""Adds colors to each vertex, v, of G based on the color of v in the subgraph containing v which is
547
read from the max_cuts dictionary
548
549
Parameters
550
----------
551
G : networkX.Graph
552
Graph with vertex color attributes
553
subgraph_dictionary : dict of networkX graph with str as keys
554
subgraphs of G
555
max_cuts : dict of str
556
dictionary of node colors for subgraphs in the subgraph_dictionary
557
558
Returns
559
-------
560
networkX.Graph, str
561
returns G with colored nodes
562
"""
563
subgraphColors={}
564
565
566
for key in graph_dictionary:
567
SubG = graph_dictionary[key]
568
sorted_subgraph_nodes = sorted(list(nx.nodes(SubG)))
569
for v in sorted_subgraph_nodes:
570
G.nodes[v]['color']=max_cuts[key][sorted_subgraph_nodes.index(v)]
571
# returns the input graph G with a coloring of the nodes based on the unaltered merger
572
# of the max cut solutions of the subgraphs in the graph_dictionary
573
return G
574
575
def new_colors(graph_dictionary, G, mergerGraph, flip_colors):
576
"""For each subgraph in the flip_colors list, changes the color of all the vertices in that subgraph
577
and records this information in the color attribute of G
578
579
Parameters
580
----------
581
graph_dictionary : dict of networkX graph with str as keys
582
subgraphs of G
583
G : networkX.Graph
584
Graph with vertex color attributes
585
mergerGraph: networkX.Graph
586
Graph whose vertices represent subgraphs in the graph_dictionary
587
flip_colors : dict of str
588
dictionary of binary strings for subgraphs in the subgraph_dictionary
589
key:0 indicates the node colors remain fixed in subgraph called key
590
key:1 indicates the node colors should be flipped in subgraph key
591
592
Returns
593
-------
594
networkX.Graph, str
595
returns G with the revised vertex colors
596
"""
597
flipGraphColors={}
598
mergerNodes = sorted(list(nx.nodes(mergerGraph)))
599
for u in mergerNodes:
600
indexu = mergerNodes.index(u)
601
flipGraphColors[u]=int(flip_colors[indexu])
602
603
for key in graph_dictionary:
604
if flipGraphColors[key]==1:
605
for u in graph_dictionary[key].nodes():
606
G.nodes[u]['color'] = str(1 - int(G.nodes[u]['color']))
607
608
revised_colors = ''
609
for u in sorted(G.nodes()):
610
revised_colors += str(G.nodes[u]['color'])
611
612
return G, revised_colors
613
614
615
def subgraph_solution(G, key, vertex_limit, subgraph_limit, layer_count, global_graph,seed ):
616
"""
617
Recursively finds max cut approximations of the subgraphs of the global_graph
618
Parameters
619
----------
620
G : networkX.Graph
621
Graph with vertex color attributes
622
key : str
623
name of subgraph
624
vertex_limit : int
625
maximum size of graph to which QAOA will be applied directly
626
subgraph_limit : int
627
maximum size of the merger graphs, or maximum number of subgraphs in any subgraph decomposition
628
layer_count : int
629
number of layers in QAOA circuit for finding max cut solutions
630
global_graph : networkX.Graph
631
the parent graph
632
seed : int
633
random seed for reproducibility
634
635
Returns
636
-------
637
str
638
returns string of 0s and 1s representing colors of vertices of global_graph for the approx max cut solution
639
"""
640
results ={}
641
# Find the max cut of G using QAOA, provided G is small enough
642
if nx.number_of_nodes(G)<vertex_limit+1:
643
print('Working on finding max cut approximations for ',key)
644
645
result =qaoa_for_graph(G, seed=seed, shots = 10000, layer_count=layer_count)
646
results[key]=result
647
# color the global graph's nodes according to the results
648
nodes_of_G = sorted(list(G.nodes()))
649
for u in G.nodes():
650
global_graph.nodes[u]['color']=results[key][nodes_of_G.index(u)]
651
return result
652
else: # Recursively apply the algorithm in case G is too big
653
# Divide the graph and identify the subgraph dictionary
654
subgraph_limit =min(subgraph_limit, nx.number_of_nodes(G) )
655
subgraph_dictionary = subgraphpartition(G,subgraph_limit, str(key), global_graph)
656
657
# Conquer: solve the subgraph problems recursively
658
for skey in subgraph_dictionary:
659
results[skey]=subgraph_solution(subgraph_dictionary[skey], skey, vertex_limit, subgraph_limit, \
660
layer_count, global_graph, seed )
661
662
print('Found max cut approximations for ',list(subgraph_dictionary.keys()))
663
664
665
# Color the nodes of G to indicate subgraph max cut solutions
666
G = unaltered_colors(G, subgraph_dictionary, results)
667
unaltered_cut_value = cutvalue(G)
668
print('prior to merging, the max cut value of',key,'is', unaltered_cut_value)
669
670
# Merge: merge the results from the conquer stage
671
print('Merging these solutions together for a solution to',key)
672
# Define the border graph
673
bordergraph = border(G, subgraph_dictionary)
674
# Define the merger graph
675
merger_graph = createMergerGraph(bordergraph, subgraph_dictionary)
676
677
try:
678
# Apply QAOA to the merger graph
679
merger_results = merging(G, subgraph_dictionary, merger_graph)
680
except:
681
# In case QAOA for merger graph does not converge, don't flip any of the colors for the merger
682
mergerResultsList = [0]*nx.number_of_nodes(merger_graph)
683
merger_results = ''.join(str(x) for x in mergerResultsList)
684
print('Merging subroutine opted out with an error for', key)
685
686
# Color the nodes of G to indicate the merged subgraph solutions
687
alteredG, new_color_list = new_colors(subgraph_dictionary, G, merger_graph, merger_results)
688
newcut = cutvalue(alteredG)
689
print('the merger algorithm produced a new coloring of',key,'with cut value,',newcut)
690
691
692
693
return new_color_list
694
##################################################################################
695
# end of definitions
696
# beginning of algorithm
697
##################################################################################
698
if rank ==0:
699
# Load graph
700
# Use the graph from Lab 3 to test out the algorithm
701
702
# Newman Watts Strogatz network model
703
#n = 100 # number of nodes
704
#k = 4 # each node joined to k nearest neighbors
705
#p =0.8 # probability of adding a new edge
706
#seed = 1234
707
#sampleGraph3=nx.newman_watts_strogatz_graph(n, k, p, seed=seed)
708
709
710
# Random d-regular graphs used in the paper arxiv:2205.11762
711
# d from 3, 9 inclusive
712
# number of vertices from from 60 to 80
713
# taking d=6 and n =100, works well
714
d = 6
715
n =70
716
graph_seed = 1234
717
sampleGraph3=nx.random_regular_graph(d,n,seed=graph_seed)
718
719
#random graph from lab 2
720
#n = 30 # number of nodes
721
#m = 70 # number of edges
722
#seed= 20160 # seed random number generators for reproducibility
723
# Use seed for reproducibility
724
#sampleGraph3= nx.gnm_random_graph(n, m, seed=seed)
725
726
# set edge weights equal to 1
727
# all weights = 1 is equivalent to solving the unweighted max cut problem
728
nx.set_edge_attributes(sampleGraph3, values = 1, name = 'weight')
729
730
# set edge weights of -1 and 1 from a non uniform distribution
731
#np.random.seed(seed)
732
#for e in sampleGraph3.edges():
733
# random_assignment = np.random.randint(0, 1)
734
# sampleGraph3.edges[e]['weight'] = -1**random_assignment
735
736
# set edge weights of 0 and 5 from a non uniform distribution
737
#np.random.seed(seed)
738
#for e in sampleGraph3.edges():
739
# random_assignment = np.random.randint(0, 5)
740
# sampleGraph3.edges[e]['weight'] = random_assignment
741
742
743
# subdivide once
744
def Lab2SubgraphPartition(G,n):
745
"""Divide the graph up into at most n subgraphs
746
747
Parameters
748
----------
749
G: networkX.Graph
750
Graph that we want to subdivide
751
n : int
752
n is the maximum number of subgraphs in the partition
753
754
Returns
755
-------
756
dict of str : networkX.Graph
757
Dictionary of networkX graphs with a string as the key
758
"""
759
# n is the maximum number of subgraphs in the partition
760
greedy_partition = community.greedy_modularity_communities(G, weight=None, resolution=1.1, cutoff=1, best_n=n)
761
number_of_subgraphs = len(greedy_partition)
762
763
graph_dictionary = {}
764
graph_names=[]
765
for i in range(number_of_subgraphs):
766
name='Global:'+str(i)
767
graph_names.append(name)
768
769
for i in range(number_of_subgraphs):
770
nodelist = sorted(list(greedy_partition[i]))
771
graph_dictionary[graph_names[i]] = nx.subgraph(G, nodelist)
772
773
return(graph_dictionary)
774
775
subgraph_dictionary = Lab2SubgraphPartition(sampleGraph3,12)
776
777
# Assign the subgraphs to the QPUs
778
number_of_subgraphs = len(sorted(subgraph_dictionary))
779
number_of_subgraphs_per_qpu = int(np.ceil(number_of_subgraphs/num_qpus))
780
781
keys_on_qpu ={}
782
783
for q in range(num_qpus):
784
keys_on_qpu[q]=[]
785
for k in range(number_of_subgraphs_per_qpu):
786
if (k*num_qpus+q < number_of_subgraphs):
787
key = sorted(subgraph_dictionary)[k*num_qpus+q]
788
keys_on_qpu[q].append(key)
789
print('Subgraph problems to be computed on each processor have been assigned')
790
# Allocate subgraph problems to the GPUs
791
# Distribute the subgraph data to the QPUs
792
for i in range(num_qpus):
793
subgraph_to_qpu ={}
794
for k in keys_on_qpu[i]:
795
subgraph_to_qpu[k]= subgraph_dictionary[k]
796
if i != 0:
797
comm.send(subgraph_to_qpu, dest=i, tag=rank)
798
else:
799
assigned_subgraph_dictionary = subgraph_to_qpu
800
else:
801
# Receive the subgraph data
802
assigned_subgraph_dictionary= comm.recv(source=0, tag=0)
803
print("Processor {} received {} from processor {}".format(rank,assigned_subgraph_dictionary, 0))
804
805
806
#########################################################################
807
# Recursively solve subgraph problems assigned to GPU
808
# and return results back to GPU0 for final merger
809
#########################################################################
810
num_subgraphs=11 # limits the size of the merger graphs
811
num_qubits = 14 # max number of qubits allowed in a quantum circuit
812
layer_count =1 # Layer count for the QAOA max cut
813
seed = 13 # Seed for QAOA for max cut
814
results = {}
815
for key in assigned_subgraph_dictionary:
816
G = assigned_subgraph_dictionary[key]
817
newcoloring_of_G = subgraph_solution(G, key, num_subgraphs, num_qubits, layer_count, G, seed = seed)
818
results[key]=newcoloring_of_G
819
820
821
############################################################################
822
# Copy over the subgraph solutions from the individual GPUs
823
# back to GPU 0.
824
#############################################################################
825
# Copy the results over to QPU 0 for consolidation
826
if rank!=0:
827
comm.send(results, dest=0, tag = 0)
828
print("{} sent by processor {}".format(results, rank))
829
830
else:
831
for j in range(1,num_qpus,1):
832
colors = comm.recv(source = j, tag =0)
833
print("Received {} from processor {}".format(colors, j))
834
for key in colors:
835
results[key]=colors[key]
836
print("The results dictionary on GPU 0 =", results)
837
838
839
#######################################################
840
# Step 3
841
#######################################################
842
############################################################################
843
# Merge results on QPU 0
844
############################################################################
845
846
# Add color attribute to subgraphs and sampleGraph3 to record the subgraph solutions
847
848
subgraphColors={}
849
850
for key in subgraph_dictionary:
851
subgraphColors[key]=[int(i) for i in results[key]]
852
853
for key in subgraph_dictionary:
854
G = subgraph_dictionary[key]
855
for v in sorted(list(nx.nodes(G))):
856
G.nodes[v]['color']=subgraphColors[key][sorted(list(nx.nodes(G))).index(v)]
857
sampleGraph3.nodes[v]['color']=G.nodes[v]['color']
858
859
860
861
862
print('The divide-and-conquer QAOA unaltered cut approximation of the graph, prior to the final merge, is ',cutvalue(sampleGraph3))
863
864
# Merge
865
borderGraph = border(sampleGraph3, subgraph_dictionary)
866
mergerGraph = createMergerGraph(borderGraph, subgraph_dictionary)
867
merger_results = merging(sampleGraph3, subgraph_dictionary, mergerGraph)
868
maxcutSampleGraph3, G_colors_with_maxcut = new_colors(subgraph_dictionary, sampleGraph3, mergerGraph, merger_results)
869
870
871
872
873
print('The divide-and-conquer QAOA max cut approximation of the graph is ',cutvalue(maxcutSampleGraph3))
874
875
###### can parallelize this
876
number_of_approx =10
877
randomlist = np.random.choice(3000,number_of_approx)
878
minapprox = nx.algorithms.approximation.one_exchange(sampleGraph3, initial_cut=None, seed=int(randomlist[0]))[0]
879
maxapprox = minapprox
880
sum_of_approximations = 0
881
for i in range(number_of_approx):
882
seed = int(randomlist[i])
883
ith_approximation = nx.algorithms.approximation.one_exchange(sampleGraph3, initial_cut=None, seed=seed)[0]
884
if ith_approximation < minapprox:
885
minapprox = ith_approximation
886
if ith_approximation > maxapprox:
887
maxapprox = ith_approximation
888
sum_of_approximations +=ith_approximation
889
890
average_approx = sum_of_approximations/number_of_approx
891
892
print('This compares to a few runs of the greedy modularity maximization algorithm gives an average approximate Max Cut value of',average_approx)
893
print('with approximations ranging from',minapprox,'to',maxapprox)
894
895