Path: blob/main/qaoa-for-max-cut/Example-02-step-2-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(keys_on_qpu[q],'=subgraph problems to be computed on processor',q)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)109print("{} sent by processor {}".format(subgraph_to_qpu, rank))110else:111assigned_subgraph_dictionary = subgraph_to_qpu112else:113# Receive the subgraph data114assigned_subgraph_dictionary= comm.recv(source=0, tag=0)115print("Processor {} received {} from processor {}".format(rank,assigned_subgraph_dictionary, 0))116117118119#######################################################120# Step 2121#######################################################122123# Define a function to generate the Hamiltonian for a max cut problem using the graph G124125def hamiltonian_max_cut(sources : List[int], targets : List[int]):126"""Hamiltonian for finding the max cut for the graph with edges defined by the pairs generated by source and target edges127128Parameters129----------130sources: List[int]131list of the source vertices for edges in the graph132targets: List[int]133list of the target vertices for the edges in the graph134135Returns136-------137cudaq.SpinOperator138Hamiltonian for finding the max cut of the graph defined by the given edges139"""140hamiltonian = 0141# Since our vertices may not be a list from 0 to n, or may not even be integers,142143for i in range(len(sources)):144# Add a term to the Hamiltonian for the edge (u,v)145qubitu = sources[i]146qubitv = targets[i]147hamiltonian += 0.5*(spin.z(qubitu)*spin.z(qubitv)-spin.i(qubitu)*spin.i(qubitv))148149return hamiltonian150151# Problem Kernel152153@cudaq.kernel154def qaoaProblem(qubit_0 : cudaq.qubit, qubit_1 : cudaq.qubit, alpha : float):155"""Build the QAOA gate sequence between two qubits that represent an edge of the graph156Parameters157----------158qubit_0: cudaq.qubit159Qubit representing the first vertex of an edge160qubit_1: cudaq.qubit161Qubit representing the second vertex of an edge162alpha: float163Free variable164165"""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 variable180181"""182rx(2.0*beta, qubit_0)183184185# We now define the kernel_qaoa function which will be the QAOA circuit for our graph186# Since the QAOA circuit for max cut depends on the structure of the graph,187# we'll feed in global concrete variable values into the kernel_qaoa function for the qubit_count, layer_count, edges_src, edges_tgt.188# The types for these variables are restricted to Quake Values (e.g. qubit, int, List[int], ...)189# The thetas plaeholder will be our free parameters (the alphas and betas in the circuit diagrams depicted above)190@cudaq.kernel191def kernel_qaoa(qubit_count :int, layer_count: int, edges_src: List[int], edges_tgt: List[int], thetas : List[float]):192"""Build the QAOA circuit for max cut of the graph with given edges and nodes193Parameters194----------195qubit_count: int196Number of qubits in the circuit, which is the same as the number of nodes in our graph197layer_count : int198Number of layers in the QAOA kernel199edges_src: List[int]200List of the first (source) node listed in each edge of the graph, when the edges of the graph are listed as pairs of nodes201edges_tgt: List[int]202List of the second (target) node listed in each edge of the graph, when the edges of the graph are listed as pairs of nodes203thetas: List[float]204Free variables to be optimized205206"""207# Let's allocate the qubits208qreg = cudaq.qvector(qubit_count)209210# And then place the qubits in superposition211h(qreg)212213# Each layer has two components: the problem kernel and the mixer214for i in range(layer_count):215# Add the problem kernel to each layer216for edge in range(len(edges_src)):217qubitu = edges_src[edge]218qubitv = edges_tgt[edge]219qaoaProblem(qreg[qubitu], qreg[qubitv], thetas[i])220# Add the mixer kernel to each layer221for j in range(qubit_count):222qaoaMixer(qreg[j],thetas[i+layer_count])223224def find_optimal_parameters(G, layer_count, seed):225"""Function for finding the optimal parameters of QAOA for the max cut of a graph226Parameters227----------228G: networkX graph229Problem graph whose max cut we aim to find230layer_count : int231Number of layers in the QAOA circuit232seed : int233Random seed for reproducibility of results234235Returns236-------237list[float]238Optimal parameters for the QAOA applied to the given graph G239"""240parameter_count: int = 2 * layer_count241242# Problem parameters243nodes = sorted(list(nx.nodes(G)))244qubit_src = []245qubit_tgt = []246for u, v in nx.edges(G):247# We can use the index() command to read out the qubits associated with the vertex u and v.248qubit_src.append(nodes.index(u))249qubit_tgt.append(nodes.index(v))250# The number of qubits we'll need is the same as the number of vertices in our graph251qubit_count : int = len(nodes)252# Each layer of the QAOA kernel contains 2 parameters253parameter_count : int = 2*layer_count254255# Specify the optimizer and its initial parameters.256optimizer = cudaq.optimizers.COBYLA()257np.random.seed(seed)258optimizer.initial_parameters = np.random.uniform(-np.pi, np.pi,259parameter_count)260261# Pass the kernel, spin operator, and optimizer to `cudaq.vqe`.262optimal_expectation, optimal_parameters = cudaq.vqe(263kernel=kernel_qaoa,264spin_operator=hamiltonian_max_cut(qubit_src, qubit_tgt),265argument_mapper=lambda parameter_vector: (qubit_count, layer_count, qubit_src, qubit_tgt, parameter_vector),266optimizer=optimizer,267parameter_count=parameter_count)268269return optimal_parameters270def qaoa_for_graph(G, layer_count, shots, seed):271"""Function for finding the max cut of a graph using QAOA272273Parameters274----------275G: networkX graph276Problem graph whose max cut we aim to find277layer_count : int278Number of layers in the QAOA circuit279shots : int280Number of shots in the sampling subroutine281seed : int282Random seed for reproducibility of results283284Returns285-------286str287Binary string representing the max cut coloring of the vertinces of the graph288"""289290291parameter_count: int = 2 * layer_count292293# Problem parameters294nodes = sorted(list(nx.nodes(G)))295qubit_src = []296qubit_tgt = []297for u, v in nx.edges(G):298# We can use the index() command to read out the qubits associated with the vertex u and v.299qubit_src.append(nodes.index(u))300qubit_tgt.append(nodes.index(v))301# The number of qubits we'll need is the same as the number of vertices in our graph302qubit_count : int = len(nodes)303# Each layer of the QAOA kernel contains 2 parameters304parameter_count : int = 2*layer_count305306optimal_parameters = find_optimal_parameters(G, layer_count, seed)307308# Print the optimized parameters309print("Optimal parameters = ", optimal_parameters)310311# Sample the circuit312counts = cudaq.sample(kernel_qaoa, qubit_count, layer_count, qubit_src, qubit_tgt, optimal_parameters, shots_count=shots)313print('most_probable outcome = ',counts.most_probable())314results = str(counts.most_probable())315return results316317318############################################################################319# On GPU with rank r, compute the subgraph solutions for the320# subgraphs in assigned_subgraph_dictionary that live on GPU r321############################################################################322layer_count =1323results = {}324new_seed_for_each_graph = rank # to give each subgraph solution different initial parameters325for key in assigned_subgraph_dictionary:326G = assigned_subgraph_dictionary[key]327results[key] = qaoa_for_graph(G, layer_count, shots = 10000, seed=654321+new_seed_for_each_graph)328new_seed_for_each_graph+=1329print('The max cut QAOA coloring for the subgraph',key,'is',results[key])330print('The results dictionary variable on GPU',rank,'is',results)331332############################################################################333# Exercise to copy over the subgraph solutions from the individual GPUs334# back to GPU 0.335#############################################################################336# Let's introduce another MPI function that will be useful to337# iterate over all the GPUs.338size = comm.Get_size()339340# Copy the results over to QPU 0 for consolidation341if rank!=0:342comm.send(results, dest=0, tag=0)343print("{} sent by processor {}".format(results, rank))344else:345for j in range(1,size,1):346colors = comm.recv(source=j, tag=0)347print("Received {} from processor {}".format(colors, j))348for key in colors:349results[key]=colors[key]350print("The results dictionary on GPU 0 =", results)351352353354