This tutorial shows how to solve the maximum cut (MaxCut) combinatorial
optimization problem on a graph using the Quantum Approximate Optimization
Algorithm (QAOA), first introduced by Farhi et al. in 2014 1.
Given an arbitrary graph, the MaxCut problem consists in finding a
graph cut (external) which partitions
the nodes into two disjoint sets, such that the number of edges in the
cut is maximized. This is a very common combinatorial optimization problem known to be computationally hard (NP-hard).
The graph used for this tutorial is an unweighted graph randomly generated using the networkx library with
a certain probability p of having an edge between two arbitrary nodes (known as Erdős–Rényi (external) graph).
import numpy as np
import networkx as nx
import matplotlib.pyplot as plt
import random
# ensure reproducibility
seed =42
np.random.seed(seed)
random.seed(seed)
# Create random graph
n_nodes =4
edge_prob =0.8
graph = nx.gnp_random_graph(n_nodes, edge_prob)
nx.draw(graph)
The goal of the MaxCut algorithm is to maximize the following cost function:
C(p)=α∑mCα(p)
where p is a given cut of the graph, α is an index over the edges and Cα(p) is written
such that if the nodes connected by the α edge are in the same set, it returns 0, otherwise it returns 1.
We will represent a cut p as a bitstring of length N, where N is the number of nodes, and where the bit in position i shows to which partition node i belongs. We assign value 0 to one of the partitions defined by the cut and 1 to the other. Since this choice is arbitrary, every cut is represented by two bitstrings, e.g. "0011" and "1100" are equivalent.
Since in this tutorial we are only dealing with small graphs, we can find the maximum cut by brute force to make sure QAOA works as intended.
# Function to calculate the cost associated with a cut
The Max-Cut problem can be solved by using the QAOA algorithm. QAOA belongs to the class of Variational Quantum Algorithms (VQAs), which means that its quantum circuit contains a certain number of parametrized quantum gates that need to be optimized with a classical optimizer.
The QAOA circuit is composed of two operators:
The cost operator \(U_c\): a circuit generated by the cost Hamiltonian which
encodes the cost function described above into a quantum circuit. The solution to the optimization problem is encoded in the ground state of the cost Hamiltonian \(H_c\).
The cost operator is simply the evolution of the cost Hamiltonian parametrized by a variational parameter \(\gamma\) so that \(U_c = e^{i\gamma H_c}.\)
The mixing operator \(U_b\): a simple set of single-qubit rotations with adjustable
angles which are tuned during the classical optimization loop to minimize the cost
The cost Hamiltonian of the MaxCut problem can be written as:
Hc=21⟨i,j⟩∑(1−ZiZj)
where ⟨i,j⟩ represents the edge between nodes i and j. The solution of the MaxCut problem is encoded in the ground state of the above Hamiltonian.
The QAOA quantum circuit consists of a number of layers, each layer containing a cost and a mixing operator.
Below, the QAOA quantum circuit is defined using
qadence operations.
First, a layer of Hadamard gates is applied to all qubits to prepare the initial state ∣+⟩⊗n.
The cost operator of each layer can be built "manually", implementing the eiZZγ terms with CNOTs and a RZ(2γ) rotation, or it can also be automatically decomposed
into digital single and two-qubits operations via the .digital_decomposition() method.
The decomposition is exact since the Hamiltonian generator is diagonal.
from qadence import tag, kron, chain, RX, RZ, Z, H, CNOT, I, add
from qadence import HamEvo, QuantumCircuit, Parameter
n_qubits = graph.number_of_nodes()
n_edges = graph.number_of_edges()
n_layers =6
# Generate the cost Hamiltonian
zz_ops =add(Z(edge[0])@Z(edge[1])for edge in graph.edges)
cost_ham =0.5* (n_edges *kron(I(i)for i inrange(n_qubits)) - zz_ops)
print(f"MaxCut cost at iteration {i+1}: {-loss.item()}")
Initial loss: -2.1782381363858794
MaxCut cost at iteration 10: 3.7470706807026417
MaxCut cost at iteration 20: 3.8378810288930216
MaxCut cost at iteration 30: 3.9424197899236133
MaxCut cost at iteration 40: 3.9981256255766002
MaxCut cost at iteration 50: 3.996470528508214
MaxCut cost at iteration 60: 3.9991374608876606
MaxCut cost at iteration 70: 3.9994678542919555
MaxCut cost at iteration 80: 3.999872558672829
MaxCut cost at iteration 90: 3.9999475834121063
MaxCut cost at iteration 100: 3.9999793311641003
Qadence offers some convenience functions to implement this training loop with advanced
logging and metrics track features. You can refer to this tutorial for more details.
Given the trained quantum model, one needs to sample the resulting quantum state to
recover the bitstring with the highest probability which corresponds to the maximum
cut of the graph.
samples = model.sample(n_shots=100)[0]
most_frequent =max(samples,key=samples.get)
print(f"Most frequently sampled bitstring corresponding to the maximum cut: {most_frequent}")
# let's now draw the cut obtained with the QAOA procedure