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