Path: blob/main/qaoa-for-max-cut/Example-02-step-3-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# Defining functions to generate the Hamiltonian and Kernel for a given graph15# Necessary packages16import networkx as nx17from networkx import algorithms18from networkx.algorithms import community19import cudaq20from cudaq import spin21from cudaq.qis import *22import numpy as np23from typing import List, Tuple24from mpi4py import MPI252627# Getting information about platform28cudaq.set_target("nvidia")29target = cudaq.get_target()3031# Setting up MPI32comm = MPI.COMM_WORLD33rank = comm.Get_rank()34num_qpus = comm.Get_size()353637#######################################################38# Step 139#######################################################40# Function to return a dictionary of subgraphs of the input graph41# using the greedy modularity maximization algorithm4243def subgraphpartition(G,n):44"""Divide the graph up into at most n subgraphs4546Parameters47----------48G: networkX.Graph49Graph that we want to subdivdie50n : int51n is the maximum number of subgraphs in the partition52Returns53-------54dict of str : networkX.Graph55Dictionary of networkX graphs with a string as the key56"""57greedy_partition = community.greedy_modularity_communities(G, weight=None, resolution=1.1, cutoff=1, best_n=n)58number_of_subgraphs = len(greedy_partition)5960graph_dictionary = {}61graph_names=[]62for i in range(number_of_subgraphs):63name='G'+str(i)64graph_names.append(name)6566for i in range(number_of_subgraphs):67nodelist = sorted(list(greedy_partition[i]))68graph_dictionary[graph_names[i]] = nx.subgraph(G, nodelist)6970return(graph_dictionary)717273if rank ==0:74# Defining the example graph75# Random graph parameters76n = 35 # numnber of nodes77m = 80 # number of edges78seed = 20160 # seed random number generators for reproducibility7980# Use seed for reproducibility81sampleGraph2 = nx.gnm_random_graph(n, m, seed=seed)8283# Subdividing the graph84num_subgraphs_limit = min(12, len(sampleGraph2.nodes())) # maximum number of subgraphs for the partition85subgraph_dictionary = subgraphpartition(sampleGraph2,num_subgraphs_limit)8687# Assign the subgraphs to the QPUs88number_of_subgraphs = len(sorted(subgraph_dictionary))89number_of_subgraphs_per_qpu = int(np.ceil(number_of_subgraphs/num_qpus))9091keys_on_qpu ={}9293for q in range(num_qpus):94keys_on_qpu[q]=[]95for k in range(number_of_subgraphs_per_qpu):96if (k*num_qpus+q < number_of_subgraphs):97key = sorted(subgraph_dictionary)[k*num_qpus+q]98keys_on_qpu[q].append(key)99print('Subgraph problems to be computed on each processor have been assigned')100101102# Distribute the subgraph data to the QPUs103for i in range(num_qpus):104subgraph_to_qpu ={}105for k in keys_on_qpu[i]:106subgraph_to_qpu[k]= subgraph_dictionary[k]107if i != 0:108comm.send(subgraph_to_qpu, dest=i, tag=rank)109else:110assigned_subgraph_dictionary = subgraph_to_qpu111else:112# Receive the subgraph data113assigned_subgraph_dictionary= comm.recv(source=0, tag=0)114print("Processor {} received {} from processor {}".format(rank,assigned_subgraph_dictionary, 0))115116117118#######################################################119# Step 2120#######################################################121122# Define a function to generate the Hamiltonian for a max cut problem using the graph G123124def hamiltonian_max_cut(sources : List[int], targets : List[int]):125"""Hamiltonian for finding the max cut for the graph with edges defined by the pairs generated by source and target edges126127Parameters128----------129sources: List[int]130list of the source vertices for edges in the graph131targets: List[int]132list of the target vertices for the edges in the graph133134Returns135-------136cudaq.SpinOperator137Hamiltonian for finding the max cut of the graph defined by the given edges138"""139hamiltonian = 0140# Since our vertices may not be a list from 0 to n, or may not even be integers,141142for i in range(len(sources)):143# Add a term to the Hamiltonian for the edge (u,v)144qubitu = sources[i]145qubitv = targets[i]146hamiltonian += 0.5*(spin.z(qubitu)*spin.z(qubitv)-spin.i(qubitu)*spin.i(qubitv))147148return hamiltonian149150# Problem Kernel151152@cudaq.kernel153def qaoaProblem(qubit_0 : cudaq.qubit, qubit_1 : cudaq.qubit, alpha : float):154"""Build the QAOA gate sequence between two qubits that represent an edge of the graph155Parameters156----------157qubit_0: cudaq.qubit158Qubit representing the first vertex of an edge159qubit_1: cudaq.qubit160Qubit representing the second vertex of an edge161alpha: float162Free variable163164165"""166x.ctrl(qubit_0, qubit_1)167rz(2.0*alpha, qubit_1)168x.ctrl(qubit_0, qubit_1)169170# Mixer Kernel171@cudaq.kernel172def qaoaMixer(qubit_0 : cudaq.qubit, beta : float):173"""Build the QAOA gate sequence that is applied to each qubit in the mixer portion of the circuit174Parameters175----------176qubit_0: cudaq.qubit177Qubit178beta: float179Free variable180181182"""183rx(2.0*beta, qubit_0)184185186# We now define the kernel_qaoa function which will be the QAOA circuit for our graph187# 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.kernel192def 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 nodes194Parameters195----------196qubit_count: int197Number of qubits in the circuit, which is the same as the number of nodes in our graph198layer_count : int199Number of layers in the QAOA kernel200edges_src: List[int]201List of the first (source) node listed in each edge of the graph, when the edges of the graph are listed as pairs of nodes202edges_tgt: List[int]203List of the second (target) node listed in each edge of the graph, when the edges of the graph are listed as pairs of nodes204thetas: List[float]205Free variables to be optimized206207208"""209# Let's allocate the qubits210qreg = cudaq.qvector(qubit_count)211212# And then place the qubits in superposition213h(qreg)214215# Each layer has two components: the problem kernel and the mixer216for i in range(layer_count):217# Add the problem kernel to each layer218for edge in range(len(edges_src)):219qubitu = edges_src[edge]220qubitv = edges_tgt[edge]221qaoaProblem(qreg[qubitu], qreg[qubitv], thetas[i])222# Add the mixer kernel to each layer223for j in range(qubit_count):224qaoaMixer(qreg[j],thetas[i+layer_count])225226def find_optimal_parameters(G, layer_count, seed):227"""Function for finding the optimal parameters of QAOA for the max cut of a graph228Parameters229----------230G: networkX graph231Problem graph whose max cut we aim to find232layer_count : int233Number of layers in the QAOA circuit234seed : int235Random seed for reproducibility of results236237Returns238-------239list[float]240Optimal parameters for the QAOA applied to the given graph G241"""242parameter_count: int = 2 * layer_count243244# Problem parameters245nodes = sorted(list(nx.nodes(G)))246qubit_src = []247qubit_tgt = []248for u, v in nx.edges(G):249# We can use the index() command to read out the qubits associated with the vertex u and v.250qubit_src.append(nodes.index(u))251qubit_tgt.append(nodes.index(v))252# The number of qubits we'll need is the same as the number of vertices in our graph253qubit_count : int = len(nodes)254# Each layer of the QAOA kernel contains 2 parameters255parameter_count : int = 2*layer_count256257# Specify the optimizer and its initial parameters.258optimizer = cudaq.optimizers.COBYLA()259np.random.seed(seed)260optimizer.initial_parameters = np.random.uniform(-np.pi, np.pi,261parameter_count)262263# Pass the kernel, spin operator, and optimizer to `cudaq.vqe`.264optimal_expectation, optimal_parameters = cudaq.vqe(265kernel=kernel_qaoa,266spin_operator=hamiltonian_max_cut(qubit_src, qubit_tgt),267argument_mapper=lambda parameter_vector: (qubit_count, layer_count, qubit_src, qubit_tgt, parameter_vector),268optimizer=optimizer,269parameter_count=parameter_count)270271return optimal_parameters272def qaoa_for_graph(G, layer_count, shots, seed):273"""Function for finding the max cut of a graph using QAOA274275Parameters276----------277G: networkX graph278Problem graph whose max cut we aim to find279layer_count : int280Number of layers in the QAOA circuit281shots : int282Number of shots in the sampling subroutine283seed : int284Random seed for reproducibility of results285286Returns287-------288str289Binary string representing the max cut coloring of the vertinces of the graph290"""291292293parameter_count: int = 2 * layer_count294295# Problem parameters296nodes = sorted(list(nx.nodes(G)))297qubit_src = []298qubit_tgt = []299for u, v in nx.edges(G):300# We can use the index() command to read out the qubits associated with the vertex u and v.301qubit_src.append(nodes.index(u))302qubit_tgt.append(nodes.index(v))303# The number of qubits we'll need is the same as the number of vertices in our graph304qubit_count : int = len(nodes)305# Each layer of the QAOA kernel contains 2 parameters306parameter_count : int = 2*layer_count307308optimal_parameters = find_optimal_parameters(G, layer_count, seed)309310# Print the optimized parameters311print("Optimal parameters = ", optimal_parameters)312313# Sample the circuit314counts = cudaq.sample(kernel_qaoa, qubit_count, layer_count, qubit_src, qubit_tgt, optimal_parameters, shots_count=shots)315print('most_probable outcome = ',counts.most_probable())316results = str(counts.most_probable())317return results318319320############################################################################321# On GPU with rank r, compute the subgraph solutions for the322# subgraphs in assigned_subgraph_dictionary that live on GPU r323############################################################################324layer_count =1325results = {}326new_seed_for_each_graph = rank # to give each subgraph solution different initial parameters327for key in assigned_subgraph_dictionary:328G = assigned_subgraph_dictionary[key]329results[key] = qaoa_for_graph(G, layer_count, shots = 10000, seed=6543+new_seed_for_each_graph)330new_seed_for_each_graph+=1331print('The max cut QAOA coloring for the subgraph',key,'is',results[key])332print('The results dictionary variable on GPU',rank,'is',results)333334############################################################################335# Exercise to copy over the subgraph solutions from the individual GPUs336# back to GPU 0.337#############################################################################338# Let's introduce another MPI function that will be useful to339# iterate over all the GPUs.340size = comm.Get_size()341342# Copy the results over to QPU 0 for consolidation343if rank!=0:344comm.send(results, dest=0, tag=0)345#print("{} sent by processor {}".format(results, rank))346else:347for j in range(1,size,1):348colors = comm.recv(source=j, tag=0)349print("Received {} from processor {}".format(colors, j))350for key in colors:351results[key]=colors[key]352#print("The results dictionary on GPU 0 =", results)353354355#######################################################356# Step 3357#######################################################358############################################################################359# Merge results on QPU 0360############################################################################361362# Add color attribute to subgraphs and sampleGraph2 to record the subgraph solutions363# Plot sampleGraph2 with node colors inherited from the subgraph solutions364365subgraphColors={}366367for key in subgraph_dictionary:368subgraphColors[key]=[int(i) for i in results[key]]369370371for key in subgraph_dictionary:372G = subgraph_dictionary[key]373for v in sorted(list(nx.nodes(G))):374G.nodes[v]['color']=subgraphColors[key][sorted(list(nx.nodes(G))).index(v)]375sampleGraph2.nodes[v]['color']=G.nodes[v]['color']376377378# A function that takes as input a subgraph partition (in the form of a graph dictionary) and a vertex.379# The function should return the key associated with the subgraph that contains the given vertex.380381def subgraph_of_vertex(graph_dictionary, vertex):382"""383A function that takes as input a subgraph partition (in the form of a graph dictionary) and a vertex.384The function should return the key associated with the subgraph that contains the given vertex.385386Parameters387----------388graph_dictionary: dict of networkX.Graph with str as keys389v : int390v is a name for a vertex391392Returns393-------394str395the key associated with the subgraph that contains the given vertex.396"""397# in case a vertex does not appear in the graph_dictionary, return the empty string398location = 'Vertex is not in the subgraph_dictionary'399400for key in graph_dictionary:401if vertex in graph_dictionary[key].nodes():402location = key403return location404405406# First let's define a function that constructs the border graph407408def border(G, subgraph_dictionary):409"""Build a graph made up of border vertices from the subgraph partition410411Parameters412----------413G: networkX.Graph414Graph whose max cut we want to find415subgraph_dictionary: dict of networkX graph with str as keys416Each graph in the dictionary should be a subgraph of G417418Returns419-------420networkX.Graph421Subgraph of G made up of only the edges connecting subgraphs in the subgraph dictionary422"""423borderG = nx.Graph()424for u,v in G.edges():425border = True426for key in subgraph_dictionary:427SubG = subgraph_dictionary[key]428edges = list(nx.edges(SubG))429if (u,v) in edges:430border = False431if border==True:432borderG.add_edge(u,v)433434return borderG435436437# Create the borderGraph438borderGraph = border(sampleGraph2, subgraph_dictionary)439440441# Define the Hamiltonian for applying QAOA to the variables442# s_i where s_i = 1 means we will not flip the subgraph Gi's colors443# and s_i = -1 means we will flip the colors of subgraph G_i444445def mHamiltonian(merger):446"""Hamiltonian for finding the optimal swap schedule for the subgraph partitioning encoded in the merger graph447448Parameters449----------450merger: networkX.Graph451Weighted graph452453Returns454-------455cudaq.SpinOperator456Hamiltonian for finding the optimal swap schedule for the subgraph partitioning encoded in the merger graph457"""458459mergerHamiltonian = 0460mergerNodes = sorted(list(merger.nodes()))461# Add Hamiltonian terms for edges within a subgraph that contain a border element462for u, v in merger.edges():463qubitu = mergerNodes.index(u)464qubitv = mergerNodes.index(v)465mergerHamiltonian+= -1*merger[u][v]['penalty']*(spin.z(qubitu))*(spin.z(qubitv))466return mergerHamiltonian467468469# Define the mergerGraph and color code the vertices470# according to the subgraph that the vertex represents471def createMergerGraph(border, subgraphs):472"""Build a graph containing a vertex for each subgraph473and edges between vertices are added if there is an edge between474the corresponding subgraphs475476Parameters477----------478border: networkX.Graph479Graph of connections between vertices in distinct subgraphs480subgraphs: dict of networkX graph with str as keys481The nodes of border should be a subset of the the graphs in the subgraphs dictionary482483Returns484-------485networkX.Graph486Merger graph containing a vertex for each subgraph487and edges between vertices are added if there is an edge between488the corresponding subgraphs489"""490491H = nx.Graph()492493for u, v in border.edges():494subgraph_id_for_u = subgraph_of_vertex(subgraphs, u)495subgraph_id_for_v = subgraph_of_vertex(subgraphs, v)496if subgraph_id_for_u != subgraph_id_for_v:497H.add_edge(subgraph_id_for_u, subgraph_id_for_v)498return H499500501mergerGraph = createMergerGraph(borderGraph, subgraph_dictionary)502503# Add attribute to capture the penalties of changing subgraph colors504505# Initialize all the penalties to 0506nx.set_edge_attributes(mergerGraph, int(0), 'penalty')507508# Compute penalties for each edge509for i, j in mergerGraph.edges():510penalty_ij = 0511for u in subgraph_dictionary[i]:512for neighbor_u in nx.all_neighbors(sampleGraph2, u):513if neighbor_u in subgraph_dictionary[j]:514if str(sampleGraph2.nodes[u]['color']) != str(sampleGraph2.nodes[neighbor_u]['color']):515penalty_ij += 1516else:517penalty_ij += -1518mergerGraph[i][j]['penalty'] = penalty_ij519520521# Graph the penalties of each edge522edge_labels = nx.get_edge_attributes(mergerGraph, 'penalty')523524525# Run QAOA on the merger subgraph to identify which subgraphs526# if any should change colors527528layer_count_merger = 1 # set arbitrarily529parameter_count_merger: int = 2 * layer_count_merger530531# Specify the optimizer and its initial parameters. Make it repeatable.532cudaq.set_random_seed(101)533optimizer_merger = cudaq.optimizers.COBYLA()534np.random.seed(101)535optimizer_merger.initial_parameters = np.random.uniform(-np.pi, np.pi,536parameter_count_merger)537optimizer_merger.max_iterations=150538539merger_nodes = list(mergerGraph.nodes())540qubit_count = len(merger_nodes)541merger_edge_src = []542merger_edge_tgt = []543for u, v in nx.edges(mergerGraph):544# We can use the index() command to read out the qubits associated with the vertex u and v.545merger_edge_src.append(merger_nodes.index(u))546merger_edge_tgt.append(merger_nodes.index(v))547548# Pass the kernel, spin operator, and optimizer to `cudaq.vqe`.549optimal_expectation, optimal_parameters = cudaq.vqe(550kernel=kernel_qaoa,551spin_operator=mHamiltonian(mergerGraph),552argument_mapper=lambda parameter_vector: (qubit_count, layer_count, merger_edge_src, merger_edge_tgt, parameter_vector),553optimizer=optimizer_merger,554parameter_count=parameter_count_merger,555shots = 10000)556557# Print the optimized value and its parameters558print("Optimal value = ", optimal_expectation)559print("Optimal parameters = ", optimal_parameters)560561# Sample the circuit using the optimized parameters562sample_number=15000563counts = cudaq.sample(kernel_qaoa, qubit_count, layer_count, merger_edge_src, merger_edge_tgt, optimal_parameters, shots_count=5000)564print(f"most_probable = {counts.most_probable()}")565566# Merger results567mergerResultsString=counts.most_probable()568569## Record a new coloring of the sampleGraph2 according to the merger results570# with a node vertex attribute 'new_color'571flipGraphColors={}572573mergerNodes = sorted(list(nx.nodes(mergerGraph)))574for u in mergerNodes:575indexu = mergerNodes.index(u)576flipGraphColors[u]=int(mergerResultsString[indexu])577578for key in subgraph_dictionary:579if flipGraphColors[key]==1:580for u in subgraph_dictionary[key].nodes():581sampleGraph2.nodes[u]['new_color'] = 1 - sampleGraph2.nodes[u]['color']582else:583for u in subgraph_dictionary[key].nodes():584sampleGraph2.nodes[u]['new_color'] = sampleGraph2.nodes[u]['color']585586# Compute the new cut of the larger graph based on the new colors587max_cut = 0588max_cut_edges = []589for u, v in sampleGraph2.edges():590if str(sampleGraph2.nodes[u]['new_color']) != str(sampleGraph2.nodes[v]['new_color']):591max_cut+=1592max_cut_edges.append((u,v))593print('The max cut value approximated from the Divide and Conquer QAOA is',max_cut)594595596