Path: blob/main/qaoa-for-max-cut/Example-04-Solution.py
579 views
# SPDX-License-Identifier: Apache-2.0 AND CC-BY-NC-4.01#2# Licensed under the Apache License, Version 2.0 (the "License");3# you may not use this file except in compliance with the License.4# You may obtain a copy of the License at5#6# http://www.apache.org/licenses/LICENSE-2.07#8# Unless required by applicable law or agreed to in writing, software9# distributed under the License is distributed on an "AS IS" BASIS,10# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.11# See the License for the specific language governing permissions and12# limitations under the License.1314# Necessary packages15import networkx as nx16from networkx import algorithms17from networkx.algorithms import community18import cudaq19from cudaq import spin20from cudaq.qis import *21import numpy as np22from mpi4py import MPI23from typing import List242526# Getting information about platform27cudaq.set_target("nvidia")28target = cudaq.get_target()2930# Setting up MPI31comm = MPI.COMM_WORLD32rank = comm.Get_rank()33num_qpus = comm.Get_size()3435# Define a function to generate the Hamiltonian for a max cut problem using the graph G36def hamiltonian_max_cut(sources : List[int], targets : List[int], weights : List[float]):37"""Hamiltonian for finding the max cut for the graph with edges defined by the pairs generated by source and target edges3839Parameters40----------41sources: List[int]42list of the source vertices for edges in the graph43targets: List[int]44list of the target vertices for the edges in the graph45weights : List[float]46list of the weight of the edge determined by the source and target with the same index47Returns48-------49cudaq.SpinOperator50Hamiltonian for finding the max cut of the graph defined by the given edges51"""52hamiltonian = 053# Since our vertices may not be a list from 0 to n, or may not even be integers,5455for i in range(len(sources)):56# Add a term to the Hamiltonian for the edge (u,v)57qubitu = sources[i]58qubitv = targets[i]59edge_weight = weights[i]60hamiltonian += 0.5*edge_weight*(spin.z(qubitu)*spin.z(qubitv)-spin.i(qubitu)*spin.i(qubitv))6162return hamiltonian6364# Problem Kernel6566@cudaq.kernel67def 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 graph69Parameters70----------71qubit_0: cudaq.qubit72Qubit representing the first vertex of an edge73qubit_1: cudaq.qubit74Qubit representing the second vertex of an edge75alpha: float76Free variable777879"""80x.ctrl(qubit_0, qubit_1)81rz(2.0*alpha, qubit_1)82x.ctrl(qubit_0, qubit_1)8384# Mixer Kernel85@cudaq.kernel86def 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 circuit88Parameters89----------90qubit_0: cudaq.qubit91Qubit92beta: float93Free variable949596"""97rx(2.0*beta, qubit_0)9899100# We now define the kernel_qaoa function which will be the QAOA circuit for our graph101# 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.kernel106def 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 nodes108Parameters109----------110qubit_count: int111Number of qubits in the circuit, which is the same as the number of nodes in our graph112layer_count : int113Number of layers in the QAOA kernel114edges_src: List[int]115List of the first (source) node listed in each edge of the graph, when the edges of the graph are listed as pairs of nodes116edges_tgt: List[int]117List of the second (target) node listed in each edge of the graph, when the edges of the graph are listed as pairs of nodes118thetas: List[float]119Free variables to be optimized120121"""122# Let's allocate the qubits123qreg = cudaq.qvector(qubit_count)124125# And then place the qubits in superposition126h(qreg)127128# Each layer has two components: the problem kernel and the mixer129for i in range(layer_count):130# Add the problem kernel to each layer131for edge in range(len(edges_src)):132qubitu = edges_src[edge]133qubitv = edges_tgt[edge]134qaoaProblem(qreg[qubitu], qreg[qubitv], thetas[i])135# Add the mixer kernel to each layer136for j in range(qubit_count):137qaoaMixer(qreg[j],thetas[i+layer_count])138139def find_optimal_parameters(G, layer_count, seed):140"""Function for finding the optimal parameters of QAOA for the max cut of a graph141Parameters142----------143G: networkX graph144Problem graph whose max cut we aim to find145layer_count : int146Number of layers in the QAOA circuit147seed : int148Random seed for reproducibility of results149150Returns151-------152list[float]153Optimal parameters for the QAOA applied to the given graph G154"""155156157# Problem parameters158nodes = sorted(list(nx.nodes(G)))159qubit_src = []160qubit_tgt = []161weights = []162for 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.164qubit_src.append(nodes.index(u))165qubit_tgt.append(nodes.index(v))166weights.append(G.edges[u,v]['weight'])167# The number of qubits we'll need is the same as the number of vertices in our graph168qubit_count : int = len(nodes)169# Each layer of the QAOA kernel contains 2 parameters170parameter_count : int = 2*layer_count171172# Specify the optimizer and its initial parameters.173optimizer = cudaq.optimizers.COBYLA()174np.random.seed(seed)175cudaq.set_random_seed(seed)176optimizer.initial_parameters = np.random.uniform(-np.pi, np.pi,177parameter_count)178179# Pass the kernel, spin operator, and optimizer to `cudaq.vqe`.180optimal_expectation, optimal_parameters = cudaq.vqe(181kernel=kernel_qaoa,182spin_operator=hamiltonian_max_cut(qubit_src, qubit_tgt, weights),183argument_mapper=lambda parameter_vector: (qubit_count, layer_count, qubit_src, qubit_tgt, parameter_vector),184optimizer=optimizer,185parameter_count=parameter_count)186187return optimal_parameters188189# These function from Lab 2 are used to identify the subgraph190# that contains a given vertex, and identify the vertices of the parent graph191# that lie on the border of the subgraphs in the subgraph dictionary192193def subgraph_of_vertex(graph_dictionary, vertex):194"""195A function that takes as input a subgraph partition (in the form of a graph dictionary) and a vertex.196The function should return the key associated with the subgraph that contains the given vertex.197198Parameters199----------200graph_dictionary: dict of networkX.Graph with str as keys201v : int202v is a name for a vertex203204Returns205-------206str207the 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 string210location = ''211212for key in graph_dictionary:213if vertex in graph_dictionary[key].nodes():214location = key215return location216217def border(G, subgraph_dictionary):218"""Build a graph made up of border vertices from the subgraph partition219220Parameters221----------222G: networkX.Graph223Graph whose max cut we want to find224subgraph_dictionary: dict of networkX graph with str as keys225Each graph in the dictionary should be a subgraph of G226227Returns228-------229networkX.Graph230Subgraph of G made up of only the edges connecting subgraphs in the subgraph dictionary231"""232borderGraph = nx.Graph()233for u,v in G.edges():234border = True235for key in subgraph_dictionary:236SubG = subgraph_dictionary[key]237edges = list(nx.edges(SubG))238if (u,v) in edges:239border = False240if border==True:241borderGraph.add_edge(u,v)242243return borderGraph244245def cutvalue(G):246"""Returns the cut value of G based on the coloring of the nodes of G247248Parameters249----------250G: networkX.Graph251Graph with weighted edges and with binary value colors assigned to the vertices252253Returns254-------255int256cut value of the graph determined by the vertex colors and edge weights257"""258259cut = 0260for u, v in G.edges():261if str(G.nodes[u]['color']) != str(G.nodes[v]['color']):262cut+=G.edges[u,v]['weight']263return cut264265def subgraphpartition(G,n, name, globalGraph):266"""Divide the graph up into at most n subgraphs267268Parameters269----------270G: networkX.Graph271Graph that we want to subdivivde which lives inside of or is equatl to globalGraph272n : int273n is the maximum number of subgraphs in the partition274name : str275prefix for the graphs (in our case we'll use 'Global')276globalGraph: networkX.Graph277original problem graph278279Returns280-------281dict of str : networkX.Graph282Dictionary of networkX graphs with a string as the key283"""284greedy_partition = community.greedy_modularity_communities(G, weight='weight', resolution=1.1, cutoff=1, best_n=n)285number_of_subgraphs = len(greedy_partition)286287graph_dictionary = {}288graph_names=[]289for i in range(number_of_subgraphs):290subgraphname=name+':'+str(i)291graph_names.append(subgraphname)292293for i in range(number_of_subgraphs):294nodelist = sorted(list(greedy_partition[i]))295graph_dictionary[graph_names[i]] = nx.subgraph(globalGraph, nodelist)296297return(graph_dictionary)298299300def qaoa_for_graph(G, layer_count, shots, seed):301"""Function for finding the max cut of a graph using QAOA302303Parameters304----------305G: networkX graph306Problem graph whose max cut we aim to find307layer_count : int308Number of layers in the QAOA circuit309shots : int310Number of shots in the sampling subroutine311seed : int312Random seed for reproducibility of results313314Returns315-------316str317Binary string representing the max cut coloring of the vertinces of the graph318"""319if 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 need321# to consider the case nx.number_of_nodes(G) ==1322results = ''323for u in list(nx.nodes(G)):324np.random.seed(seed)325random_assignment = str(np.random.randint(0, 1))326results+=random_assignment327328else:329parameter_count: int = 2 * layer_count330331# Problem parameters332nodes = sorted(list(nx.nodes(G)))333qubit_src = []334qubit_tgt = []335for 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.337qubit_src.append(nodes.index(u))338qubit_tgt.append(nodes.index(v))339# The number of qubits we'll need is the same as the number of vertices in our graph340qubit_count : int = len(nodes)341# Each layer of the QAOA kernel contains 2 parameters342parameter_count : int = 2*layer_count343344optimal_parameters = find_optimal_parameters(G, layer_count, seed)345346# Print the optimized parameters347print("Optimal parameters = ", optimal_parameters)348cudaq.set_random_seed(seed)349# Sample the circuit350counts = cudaq.sample(kernel_qaoa, qubit_count, layer_count, qubit_src, qubit_tgt, optimal_parameters, shots_count=shots)351print('most_probable outcome = ',counts.most_probable())352results = str(counts.most_probable())353return results354355# The functions below are based on code from Lab 2356# Define the mergerGraph for a given border graph and subgraph357# partitioning. And color code the vertices358# according to the subgraph that the vertex represents359def createMergerGraph(border, subgraphs):360"""Build a graph containing a vertex for each subgraph361and edges between vertices are added if there is an edge between362the corresponding subgraphs363364Parameters365----------366border : networkX.Graph367Graph of connections between vertices in distinct subgraphs368subgraphs : dict of networkX graph with str as keys369The nodes of border should be a subset of the the graphs in the subgraphs dictionary370371Returns372-------373networkX.Graph374Merger graph containing a vertex for each subgraph375and edges between vertices are added if there is an edge between376the corresponding subgraphs377"""378M = nx.Graph()379380for u, v in border.edges():381subgraph_id_for_u = subgraph_of_vertex(subgraphs, u)382subgraph_id_for_v = subgraph_of_vertex(subgraphs, v)383if subgraph_id_for_u != subgraph_id_for_v:384M.add_edge(subgraph_id_for_u, subgraph_id_for_v)385return M386387388# Compute the penalties for edges in the supplied mergerGraph389# for the subgraph partitioning of graph G390def merger_graph_penalties(mergerGraph, subgraph_dictionary, G):391"""Compute penalties for the edges in the mergerGraph and add them392as edge attributes.393394Parameters395----------396mergerGraph : networkX.Graph397Graph of connections between vertices in distinct subgraphs of G398subgraph_dictionary : dict of networkX graph with str as keys399subgraphs of G that are represented as nodes in the mergerGraph400G : networkX.Graph401graph whose vertices has an attribute 'color'402403Returns404-------405networkX.Graph406Merger graph containing penalties407"""408nx.set_edge_attributes(mergerGraph, int(0), 'penalty')409for i, j in mergerGraph.edges():410penalty_ij = 0411for u in nx.nodes(subgraph_dictionary[i]):412for neighbor_u in nx.all_neighbors(G, u):413if neighbor_u in nx.nodes(subgraph_dictionary[j]):414if str(G.nodes[u]['color']) != str(G.nodes[neighbor_u]['color']):415penalty_ij += G.edges[u,neighbor_u]['weight']416else:417penalty_ij += -G.edges[u,neighbor_u]['weight']418mergerGraph[i][j]['penalty'] = penalty_ij419return mergerGraph420421422# Define the Hamiltonian for applying QAOA during the merger stage423# The variables s_i are defined so that s_i = 1 means we will not424# flip the subgraph Gi's colors and s_i = -1 means we will flip the colors of subgraph G_i425def mHamiltonian(merger_edge_src, merger_edge_tgt, penalty):426"""Hamiltonian for finding the optimal swap schedule for the subgraph partitioning encoded in the merger graph427428Parameters429----------430merger_edge_src: List[int]431list of the source vertices of edges of a graph432merger_edge_tgt: List[int]433list of target vertices of edges of a graph434penalty: List[int]435list of penalty terms associated with the edge determined by the source and target vertex of the same index436437Returns438-------439cudaq.SpinOperator440Hamiltonian for finding the optimal swap schedule for the subgraph partitioning encoded in the merger graph441"""442mergerHamiltonian = 0443444# Add Hamiltonian terms for edges within a subgraph that contain a border element445for i in range(len(merger_edge_src)):446# Add a term to the Hamiltonian for the edge (u,v)447qubitu = merger_edge_src[i]448qubitv = merger_edge_tgt[i]449mergerHamiltonian+= -penalty[i]*(spin.z(qubitu))*(spin.z(qubitv))450return mergerHamiltonian451452# A function to carry out QAOA during the merger stage of the453# divide-and-conquer QAOA algorithm for graph G, its subgraphs (graph_dictionary)454# and merger_graph455456def merging(G, graph_dictionary, merger_graph):457"""458Using QAOA, identify which subgraphs should be in the swap schedule (e.g. which subgraphs will require459flipping of the colors when merging the subgraph solutions into a solution of the graph G460461Parameters462----------463G : networkX.Graph464Graph with vertex color attributes465graph_dictionary : dict of networkX graph with str as keys466subgraphs of G467mergerGraph : networkX.Graph468Graph whose vertices represent subgraphs in the graph_dictionary469470Returns471-------472str473returns string of 0s and 1s indicating which subgraphs should have their colors swapped474"""475476merger_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 colors478if (True in (merger_graph_with_penalties[u][v]['penalty'] != 0 for u, v in nx.edges(merger_graph_with_penalties))):479480penalty = []481merger_edge_src = []482merger_edge_tgt = []483merger_nodes = sorted(list(merger_graph_with_penalties.nodes()))484for 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.486merger_edge_src.append(merger_nodes.index(u))487merger_edge_tgt.append(merger_nodes.index(v))488penalty.append(merger_graph_with_penalties[u][v]['penalty'])489490merger_Hamiltonian = mHamiltonian(merger_edge_src, merger_edge_tgt, penalty)491492# Run QAOA on the merger subgraph to identify which subgraphs493# if any should change colors494layer_count_merger = 1 # Edit this line to change the layer count495parameter_count_merger: int = 2 * layer_count_merger496merger_seed = 12345 # Edit this line to change the seed for the merger call to QAOA497nodes_merger = sorted(list(nx.nodes(merger_graph)))498merger_edge_src = []499merger_edge_tgt = []500for 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.502merger_edge_src.append(nodes_merger.index(u))503merger_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 graph505qubit_count_merger : int = len(nodes_merger)506507# Specify the optimizer and its initial parameters. Make it repeatable.508cudaq.set_random_seed(merger_seed)509optimizer_merger = cudaq.optimizers.COBYLA()510np.random.seed(merger_seed)511optimizer_merger.initial_parameters = np.random.uniform(-np.pi, np.pi,512parameter_count_merger)513optimizer_merger.max_iterations=150514# Pass the kernel, spin operator, and optimizer to `cudaq.vqe`.515optimal_expectation, optimal_parameters = cudaq.vqe(516kernel=kernel_qaoa,517spin_operator=merger_Hamiltonian,518argument_mapper=lambda parameter_vector: (qubit_count_merger, layer_count_merger, merger_edge_src, merger_edge_tgt, parameter_vector),519optimizer=optimizer_merger,520parameter_count=parameter_count_merger,521shots = 20000)522523# Sample the circuit using the optimized parameters524# Sample enough times to distinguish the most_probable outcome for525# merger graphs with 12 vertices526sample_number=20000527counts = cudaq.sample(kernel_qaoa, qubit_count_merger, layer_count_merger, merger_edge_src, merger_edge_tgt, optimal_parameters, shots_count=sample_number)528mergerResultsString = str(counts.most_probable())529530else:531mergerResultsList = [0]*nx.number_of_nodes(merger_graph)532mergerResultsString = ''.join(str(x) for x in mergerResultsList)533print('Merging stage is trivial')534return mergerResultsString535536537538# Next we define some functions to keep track of the unaltered cuts539# (recorded as unaltered_colors) and the merged cuts (recorded as new_colors).540# The new_colors are derived from flipping the colors541# of all the nodes in a subgraph based on the flip_colors variable which542# captures the solution to the merger QAOA problem.543544def 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 is546read from the max_cuts dictionary547548Parameters549----------550G : networkX.Graph551Graph with vertex color attributes552subgraph_dictionary : dict of networkX graph with str as keys553subgraphs of G554max_cuts : dict of str555dictionary of node colors for subgraphs in the subgraph_dictionary556557Returns558-------559networkX.Graph, str560returns G with colored nodes561"""562subgraphColors={}563564565for key in graph_dictionary:566SubG = graph_dictionary[key]567sorted_subgraph_nodes = sorted(list(nx.nodes(SubG)))568for v in sorted_subgraph_nodes:569G.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 merger571# of the max cut solutions of the subgraphs in the graph_dictionary572return G573574def 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 subgraph576and records this information in the color attribute of G577578Parameters579----------580graph_dictionary : dict of networkX graph with str as keys581subgraphs of G582G : networkX.Graph583Graph with vertex color attributes584mergerGraph: networkX.Graph585Graph whose vertices represent subgraphs in the graph_dictionary586flip_colors : dict of str587dictionary of binary strings for subgraphs in the subgraph_dictionary588key:0 indicates the node colors remain fixed in subgraph called key589key:1 indicates the node colors should be flipped in subgraph key590591Returns592-------593networkX.Graph, str594returns G with the revised vertex colors595"""596flipGraphColors={}597mergerNodes = sorted(list(nx.nodes(mergerGraph)))598for u in mergerNodes:599indexu = mergerNodes.index(u)600flipGraphColors[u]=int(flip_colors[indexu])601602for key in graph_dictionary:603if flipGraphColors[key]==1:604for u in graph_dictionary[key].nodes():605G.nodes[u]['color'] = str(1 - int(G.nodes[u]['color']))606607revised_colors = ''608for u in sorted(G.nodes()):609revised_colors += str(G.nodes[u]['color'])610611return G, revised_colors612613614def subgraph_solution(G, key, vertex_limit, subgraph_limit, layer_count, global_graph,seed ):615"""616Recursively finds max cut approximations of the subgraphs of the global_graph617Parameters618----------619G : networkX.Graph620Graph with vertex color attributes621key : str622name of subgraph623vertex_limit : int624maximum size of graph to which QAOA will be applied directly625subgraph_limit : int626maximum size of the merger graphs, or maximum number of subgraphs in any subgraph decomposition627layer_count : int628number of layers in QAOA circuit for finding max cut solutions629global_graph : networkX.Graph630the parent graph631seed : int632random seed for reproducibility633634Returns635-------636str637returns string of 0s and 1s representing colors of vertices of global_graph for the approx max cut solution638"""639results ={}640# Find the max cut of G using QAOA, provided G is small enough641if nx.number_of_nodes(G)<vertex_limit+1:642print('Working on finding max cut approximations for ',key)643644result =qaoa_for_graph(G, seed=seed, shots = 10000, layer_count=layer_count)645results[key]=result646# color the global graph's nodes according to the results647nodes_of_G = sorted(list(G.nodes()))648for u in G.nodes():649global_graph.nodes[u]['color']=results[key][nodes_of_G.index(u)]650return result651else: # Recursively apply the algorithm in case G is too big652# Divide the graph and identify the subgraph dictionary653subgraph_limit =min(subgraph_limit, nx.number_of_nodes(G) )654subgraph_dictionary = subgraphpartition(G,subgraph_limit, str(key), global_graph)655656# Conquer: solve the subgraph problems recursively657for skey in subgraph_dictionary:658results[skey]=subgraph_solution(subgraph_dictionary[skey], skey, vertex_limit, subgraph_limit, \659layer_count, global_graph, seed )660661print('Found max cut approximations for ',list(subgraph_dictionary.keys()))662663664# Color the nodes of G to indicate subgraph max cut solutions665G = unaltered_colors(G, subgraph_dictionary, results)666unaltered_cut_value = cutvalue(G)667print('prior to merging, the max cut value of',key,'is', unaltered_cut_value)668669# Merge: merge the results from the conquer stage670print('Merging these solutions together for a solution to',key)671# Define the border graph672bordergraph = border(G, subgraph_dictionary)673# Define the merger graph674merger_graph = createMergerGraph(bordergraph, subgraph_dictionary)675676try:677# Apply QAOA to the merger graph678merger_results = merging(G, subgraph_dictionary, merger_graph)679except:680# In case QAOA for merger graph does not converge, don't flip any of the colors for the merger681mergerResultsList = [0]*nx.number_of_nodes(merger_graph)682merger_results = ''.join(str(x) for x in mergerResultsList)683print('Merging subroutine opted out with an error for', key)684685# Color the nodes of G to indicate the merged subgraph solutions686alteredG, new_color_list = new_colors(subgraph_dictionary, G, merger_graph, merger_results)687newcut = cutvalue(alteredG)688print('the merger algorithm produced a new coloring of',key,'with cut value,',newcut)689690691692return new_color_list693##################################################################################694# end of definitions695# beginning of algorithm696##################################################################################697if rank ==0:698# Load graph699# Use the graph from Lab 3 to test out the algorithm700701# Newman Watts Strogatz network model702#n = 100 # number of nodes703#k = 4 # each node joined to k nearest neighbors704#p =0.8 # probability of adding a new edge705#seed = 1234706#sampleGraph3=nx.newman_watts_strogatz_graph(n, k, p, seed=seed)707708709# Random d-regular graphs used in the paper arxiv:2205.11762710# d from 3, 9 inclusive711# number of vertices from from 60 to 80712# taking d=6 and n =100, works well713d = 6714n =70715graph_seed = 1234716sampleGraph3=nx.random_regular_graph(d,n,seed=graph_seed)717718#random graph from lab 2719#n = 30 # number of nodes720#m = 70 # number of edges721#seed= 20160 # seed random number generators for reproducibility722# Use seed for reproducibility723#sampleGraph3= nx.gnm_random_graph(n, m, seed=seed)724725# set edge weights equal to 1726# all weights = 1 is equivalent to solving the unweighted max cut problem727nx.set_edge_attributes(sampleGraph3, values = 1, name = 'weight')728729# set edge weights of -1 and 1 from a non uniform distribution730#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_assignment734735# set edge weights of 0 and 5 from a non uniform distribution736#np.random.seed(seed)737#for e in sampleGraph3.edges():738# random_assignment = np.random.randint(0, 5)739# sampleGraph3.edges[e]['weight'] = random_assignment740741742# subdivide once743def Lab2SubgraphPartition(G,n):744"""Divide the graph up into at most n subgraphs745746Parameters747----------748G: networkX.Graph749Graph that we want to subdivide750n : int751n is the maximum number of subgraphs in the partition752753Returns754-------755dict of str : networkX.Graph756Dictionary of networkX graphs with a string as the key757"""758# n is the maximum number of subgraphs in the partition759greedy_partition = community.greedy_modularity_communities(G, weight=None, resolution=1.1, cutoff=1, best_n=n)760number_of_subgraphs = len(greedy_partition)761762graph_dictionary = {}763graph_names=[]764for i in range(number_of_subgraphs):765name='Global:'+str(i)766graph_names.append(name)767768for i in range(number_of_subgraphs):769nodelist = sorted(list(greedy_partition[i]))770graph_dictionary[graph_names[i]] = nx.subgraph(G, nodelist)771772return(graph_dictionary)773774subgraph_dictionary = Lab2SubgraphPartition(sampleGraph3,12)775776# Assign the subgraphs to the QPUs777number_of_subgraphs = len(sorted(subgraph_dictionary))778number_of_subgraphs_per_qpu = int(np.ceil(number_of_subgraphs/num_qpus))779780keys_on_qpu ={}781782for q in range(num_qpus):783keys_on_qpu[q]=[]784for k in range(number_of_subgraphs_per_qpu):785if (k*num_qpus+q < number_of_subgraphs):786key = sorted(subgraph_dictionary)[k*num_qpus+q]787keys_on_qpu[q].append(key)788print('Subgraph problems to be computed on each processor have been assigned')789# Allocate subgraph problems to the GPUs790# Distribute the subgraph data to the QPUs791for i in range(num_qpus):792subgraph_to_qpu ={}793for k in keys_on_qpu[i]:794subgraph_to_qpu[k]= subgraph_dictionary[k]795if i != 0:796comm.send(subgraph_to_qpu, dest=i, tag=rank)797else:798assigned_subgraph_dictionary = subgraph_to_qpu799else:800# Receive the subgraph data801assigned_subgraph_dictionary= comm.recv(source=0, tag=0)802print("Processor {} received {} from processor {}".format(rank,assigned_subgraph_dictionary, 0))803804805#########################################################################806# Recursively solve subgraph problems assigned to GPU807# and return results back to GPU0 for final merger808#########################################################################809num_subgraphs=11 # limits the size of the merger graphs810num_qubits = 14 # max number of qubits allowed in a quantum circuit811layer_count =1 # Layer count for the QAOA max cut812seed = 13 # Seed for QAOA for max cut813results = {}814for key in assigned_subgraph_dictionary:815G = assigned_subgraph_dictionary[key]816newcoloring_of_G = subgraph_solution(G, key, num_subgraphs, num_qubits, layer_count, G, seed = seed)817results[key]=newcoloring_of_G818819820############################################################################821# Copy over the subgraph solutions from the individual GPUs822# back to GPU 0.823#############################################################################824# Copy the results over to QPU 0 for consolidation825if rank!=0:826comm.send(results, dest=0, tag = 0)827print("{} sent by processor {}".format(results, rank))828829else:830for j in range(1,num_qpus,1):831colors = comm.recv(source = j, tag =0)832print("Received {} from processor {}".format(colors, j))833for key in colors:834results[key]=colors[key]835print("The results dictionary on GPU 0 =", results)836837838#######################################################839# Step 3840#######################################################841############################################################################842# Merge results on QPU 0843############################################################################844845# Add color attribute to subgraphs and sampleGraph3 to record the subgraph solutions846847subgraphColors={}848849for key in subgraph_dictionary:850subgraphColors[key]=[int(i) for i in results[key]]851852for key in subgraph_dictionary:853G = subgraph_dictionary[key]854for v in sorted(list(nx.nodes(G))):855G.nodes[v]['color']=subgraphColors[key][sorted(list(nx.nodes(G))).index(v)]856sampleGraph3.nodes[v]['color']=G.nodes[v]['color']857858859860861print('The divide-and-conquer QAOA unaltered cut approximation of the graph, prior to the final merge, is ',cutvalue(sampleGraph3))862863# Merge864borderGraph = border(sampleGraph3, subgraph_dictionary)865mergerGraph = createMergerGraph(borderGraph, subgraph_dictionary)866merger_results = merging(sampleGraph3, subgraph_dictionary, mergerGraph)867maxcutSampleGraph3, G_colors_with_maxcut = new_colors(subgraph_dictionary, sampleGraph3, mergerGraph, merger_results)868869870871872print('The divide-and-conquer QAOA max cut approximation of the graph is ',cutvalue(maxcutSampleGraph3))873874###### can parallelize this875number_of_approx =10876randomlist = np.random.choice(3000,number_of_approx)877minapprox = nx.algorithms.approximation.one_exchange(sampleGraph3, initial_cut=None, seed=int(randomlist[0]))[0]878maxapprox = minapprox879sum_of_approximations = 0880for i in range(number_of_approx):881seed = int(randomlist[i])882ith_approximation = nx.algorithms.approximation.one_exchange(sampleGraph3, initial_cut=None, seed=seed)[0]883if ith_approximation < minapprox:884minapprox = ith_approximation885if ith_approximation > maxapprox:886maxapprox = ith_approximation887sum_of_approximations +=ith_approximation888889average_approx = sum_of_approximations/number_of_approx890891print('This compares to a few runs of the greedy modularity maximization algorithm gives an average approximate Max Cut value of',average_approx)892print('with approximations ranging from',minapprox,'to',maxapprox)893894895