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