Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
NVIDIA
GitHub Repository: NVIDIA/cuda-q-academic
Path: blob/main/qaoa-for-max-cut/Example-02-step-2.py
579 views
1
# SPDX-License-Identifier: Apache-2.0 AND CC-BY-NC-4.0
2
#
3
# Licensed under the Apache License, Version 2.0 (the "License");
4
# you may not use this file except in compliance with the License.
5
# You may obtain a copy of the License at
6
#
7
# http://www.apache.org/licenses/LICENSE-2.0
8
#
9
# Unless required by applicable law or agreed to in writing, software
10
# distributed under the License is distributed on an "AS IS" BASIS,
11
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
# See the License for the specific language governing permissions and
13
# limitations under the License.
14
15
# Defining functions to generate the Hamiltonian and Kernel for a given graph
16
# Necessary packages
17
import networkx as nx
18
from networkx import algorithms
19
from networkx.algorithms import community
20
import cudaq
21
from cudaq import spin
22
from cudaq.qis import *
23
import numpy as np
24
from typing import List, Tuple
25
from mpi4py import MPI
26
27
28
# Getting information about platform
29
cudaq.set_target("nvidia")
30
target = cudaq.get_target()
31
32
# Setting up MPI
33
comm = MPI.COMM_WORLD
34
rank = comm.Get_rank()
35
num_qpus = comm.Get_size()
36
37
38
#######################################################
39
# Step 1
40
#######################################################
41
# Function to return a dictionary of subgraphs of the input graph
42
# using the greedy modularity maximization algorithm
43
44
def subgraphpartition(G,n):
45
"""Divide the graph up into at most n subgraphs
46
47
Parameters
48
----------
49
G: networkX.Graph
50
Graph that we want to subdivdie
51
n : int
52
n is the maximum number of subgraphs in the partition
53
Returns
54
-------
55
dict of str : networkX.Graph
56
Dictionary of networkX graphs with a string as the key
57
"""
58
greedy_partition = community.greedy_modularity_communities(G, weight=None, resolution=1.1, cutoff=1, best_n=n)
59
number_of_subgraphs = len(greedy_partition)
60
61
graph_dictionary = {}
62
graph_names=[]
63
for i in range(number_of_subgraphs):
64
name='G'+str(i)
65
graph_names.append(name)
66
67
for i in range(number_of_subgraphs):
68
nodelist = sorted(list(greedy_partition[i]))
69
graph_dictionary[graph_names[i]] = nx.subgraph(G, nodelist)
70
71
return(graph_dictionary)
72
73
74
if rank ==0:
75
# Defining the example graph
76
# Random graph parameters
77
n = 35 # numnber of nodes
78
m = 80 # number of edges
79
seed = 20160 # seed random number generators for reproducibility
80
81
# Use seed for reproducibility
82
sampleGraph2 = nx.gnm_random_graph(n, m, seed=seed)
83
84
# Subdividing the graph
85
num_subgraphs_limit = min(12, len(sampleGraph2.nodes())) # maximum number of subgraphs for the partition
86
subgraph_dictionary = subgraphpartition(sampleGraph2,num_subgraphs_limit)
87
88
# Assign the subgraphs to the QPUs
89
number_of_subgraphs = len(sorted(subgraph_dictionary))
90
number_of_subgraphs_per_qpu = int(np.ceil(number_of_subgraphs/num_qpus))
91
92
keys_on_qpu ={}
93
94
for q in range(num_qpus):
95
keys_on_qpu[q]=[]
96
for k in range(number_of_subgraphs_per_qpu):
97
if (k*num_qpus+q < number_of_subgraphs):
98
key = sorted(subgraph_dictionary)[k*num_qpus+q]
99
keys_on_qpu[q].append(key)
100
print(keys_on_qpu[q],'=subgraph problems to be computed on processor',q)
101
102
103
# Distribute the subgraph data to the QPUs
104
for i in range(num_qpus):
105
subgraph_to_qpu ={}
106
for k in keys_on_qpu[i]:
107
subgraph_to_qpu[k]= subgraph_dictionary[k]
108
if i != 0:
109
comm.send(subgraph_to_qpu, dest=i, tag=rank)
110
print("{} sent by processor {}".format(subgraph_to_qpu, rank))
111
else:
112
assigned_subgraph_dictionary = subgraph_to_qpu
113
else:
114
# Receive the subgraph data
115
assigned_subgraph_dictionary= comm.recv(source=0, tag=0)
116
print("Processor {} received {} from processor {}".format(rank,assigned_subgraph_dictionary, 0))
117
118
119
120
#######################################################
121
# Step 2
122
#######################################################
123
124
# Define a function to generate the Hamiltonian for a max cut problem using the graph G
125
126
def hamiltonian_max_cut(sources : List[int], targets : List[int]):
127
"""Hamiltonian for finding the max cut for the graph with edges defined by the pairs generated by source and target edges
128
129
Parameters
130
----------
131
sources: List[int]
132
list of the source vertices for edges in the graph
133
targets: List[int]
134
list of the target vertices for the edges in the graph
135
136
Returns
137
-------
138
cudaq.SpinOperator
139
Hamiltonian for finding the max cut of the graph defined by the given edges
140
"""
141
hamiltonian = 0
142
# Since our vertices may not be a list from 0 to n, or may not even be integers,
143
144
for i in range(len(sources)):
145
# Add a term to the Hamiltonian for the edge (u,v)
146
qubitu = sources[i]
147
qubitv = targets[i]
148
hamiltonian += 0.5*(spin.z(qubitu)*spin.z(qubitv)-spin.i(qubitu)*spin.i(qubitv))
149
150
return hamiltonian
151
152
# Problem Kernel
153
154
@cudaq.kernel
155
def qaoaProblem(qubit_0 : cudaq.qubit, qubit_1 : cudaq.qubit, alpha : float):
156
"""Build the QAOA gate sequence between two qubits that represent an edge of the graph
157
Parameters
158
----------
159
qubit_0: cudaq.qubit
160
Qubit representing the first vertex of an edge
161
qubit_1: cudaq.qubit
162
Qubit representing the second vertex of an edge
163
alpha: float
164
Free variable
165
166
"""
167
x.ctrl(qubit_0, qubit_1)
168
rz(2.0*alpha, qubit_1)
169
x.ctrl(qubit_0, qubit_1)
170
171
# Mixer Kernel
172
@cudaq.kernel
173
def qaoaMixer(qubit_0 : cudaq.qubit, beta : float):
174
"""Build the QAOA gate sequence that is applied to each qubit in the mixer portion of the circuit
175
Parameters
176
----------
177
qubit_0: cudaq.qubit
178
Qubit
179
beta: float
180
Free variable
181
182
"""
183
rx(2.0*beta, qubit_0)
184
185
186
# We now define the kernel_qaoa function which will be the QAOA circuit for our graph
187
# Since the QAOA circuit for max cut depends on the structure of the graph,
188
# we'll feed in global concrete variable values into the kernel_qaoa function for the qubit_count, layer_count, edges_src, edges_tgt.
189
# The types for these variables are restricted to Quake Values (e.g. qubit, int, List[int], ...)
190
# The thetas plaeholder will be our free parameters (the alphas and betas in the circuit diagrams depicted above)
191
@cudaq.kernel
192
def kernel_qaoa(qubit_count :int, layer_count: int, edges_src: List[int], edges_tgt: List[int], thetas : List[float]):
193
"""Build the QAOA circuit for max cut of the graph with given edges and nodes
194
Parameters
195
----------
196
qubit_count: int
197
Number of qubits in the circuit, which is the same as the number of nodes in our graph
198
layer_count : int
199
Number of layers in the QAOA kernel
200
edges_src: List[int]
201
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
202
edges_tgt: List[int]
203
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
204
thetas: List[float]
205
Free variables to be optimized
206
207
"""
208
# Let's allocate the qubits
209
qreg = cudaq.qvector(qubit_count)
210
211
# And then place the qubits in superposition
212
h(qreg)
213
214
# Each layer has two components: the problem kernel and the mixer
215
for i in range(layer_count):
216
# Add the problem kernel to each layer
217
for edge in range(len(edges_src)):
218
qubitu = edges_src[edge]
219
qubitv = edges_tgt[edge]
220
qaoaProblem(qreg[qubitu], qreg[qubitv], thetas[i])
221
# Add the mixer kernel to each layer
222
for j in range(qubit_count):
223
qaoaMixer(qreg[j],thetas[i+layer_count])
224
225
def find_optimal_parameters(G, layer_count, seed):
226
"""Function for finding the optimal parameters of QAOA for the max cut of a graph
227
Parameters
228
----------
229
G: networkX graph
230
Problem graph whose max cut we aim to find
231
layer_count : int
232
Number of layers in the QAOA circuit
233
seed : int
234
Random seed for reproducibility of results
235
236
Returns
237
-------
238
list[float]
239
Optimal parameters for the QAOA applied to the given graph G
240
"""
241
parameter_count: int = 2 * layer_count
242
243
# Problem parameters
244
nodes = sorted(list(nx.nodes(G)))
245
qubit_src = []
246
qubit_tgt = []
247
for u, v in nx.edges(G):
248
# We can use the index() command to read out the qubits associated with the vertex u and v.
249
qubit_src.append(nodes.index(u))
250
qubit_tgt.append(nodes.index(v))
251
# The number of qubits we'll need is the same as the number of vertices in our graph
252
qubit_count : int = len(nodes)
253
# Each layer of the QAOA kernel contains 2 parameters
254
parameter_count : int = 2*layer_count
255
256
# Specify the optimizer and its initial parameters.
257
optimizer = cudaq.optimizers.COBYLA()
258
np.random.seed(seed)
259
optimizer.initial_parameters = np.random.uniform(-np.pi, np.pi,
260
parameter_count)
261
262
# Pass the kernel, spin operator, and optimizer to `cudaq.vqe`.
263
optimal_expectation, optimal_parameters = cudaq.vqe(
264
kernel=kernel_qaoa,
265
spin_operator=hamiltonian_max_cut(qubit_src, qubit_tgt),
266
argument_mapper=lambda parameter_vector: (qubit_count, layer_count, qubit_src, qubit_tgt, parameter_vector),
267
optimizer=optimizer,
268
parameter_count=parameter_count)
269
270
return optimal_parameters
271
def qaoa_for_graph(G, layer_count, shots, seed):
272
"""Function for finding the max cut of a graph using QAOA
273
274
Parameters
275
----------
276
G: networkX graph
277
Problem graph whose max cut we aim to find
278
layer_count : int
279
Number of layers in the QAOA circuit
280
shots : int
281
Number of shots in the sampling subroutine
282
seed : int
283
Random seed for reproducibility of results
284
285
Returns
286
-------
287
str
288
Binary string representing the max cut coloring of the vertinces of the graph
289
"""
290
291
292
parameter_count: int = 2 * layer_count
293
294
# Problem parameters
295
nodes = sorted(list(nx.nodes(G)))
296
qubit_src = []
297
qubit_tgt = []
298
for u, v in nx.edges(G):
299
# We can use the index() command to read out the qubits associated with the vertex u and v.
300
qubit_src.append(nodes.index(u))
301
qubit_tgt.append(nodes.index(v))
302
# The number of qubits we'll need is the same as the number of vertices in our graph
303
qubit_count : int = len(nodes)
304
# Each layer of the QAOA kernel contains 2 parameters
305
parameter_count : int = 2*layer_count
306
307
optimal_parameters = find_optimal_parameters(G, layer_count, seed)
308
309
# Print the optimized parameters
310
print("Optimal parameters = ", optimal_parameters)
311
312
# Sample the circuit
313
counts = cudaq.sample(kernel_qaoa, qubit_count, layer_count, qubit_src, qubit_tgt, optimal_parameters, shots_count=shots)
314
print('most_probable outcome = ',counts.most_probable())
315
results = str(counts.most_probable())
316
return results
317
318
############################################################################
319
# On GPU with rank r, compute the subgraph solutions for the
320
# subgraphs in assigned_subgraph_dictionary that live on GPU r
321
############################################################################
322
layer_count =1
323
results = {}
324
new_seed_for_each_graph = rank # to give each subgraph solution different initial parameters
325
for key in assigned_subgraph_dictionary:
326
G = assigned_subgraph_dictionary[key]
327
results[key] = qaoa_for_graph(G, layer_count, shots = 10000, seed=6543+new_seed_for_each_graph)
328
new_seed_for_each_graph+=1
329
print('The max cut QAOA coloring for the subgraph',key,'is',results[key])
330
print('The results dictionary variable on GPU',rank,'is',results)
331
332
333
334
335
336