Skip to content

Solving MaxCut with QAOA

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 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 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)
2024-08-22T12:51:54.781051 image/svg+xml Matplotlib v3.7.5, https://matplotlib.org/

The goal of the MaxCut algorithm is to maximize the following cost function:

\[\mathcal{C}(p) = \sum_{\alpha}^m \mathcal{C}_{\alpha}(p)\]

where \(p\) is a given cut of the graph, \(\alpha\) is an index over the edges and \(\mathcal{C}_{\alpha}(p)\) is written such that if the nodes connected by the \(\alpha\) 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
def calculate_cost(cut: str, graph: nx.graph) -> float:
    """Returns the cost of a given cut (represented by a bitstring)"""
    cost = 0
    for edge in graph.edges():
        (i, j) = edge
        if cut[i] != cut[j]:
            cost += 1
    return cost


# Function to get a binary representation of an int
get_binary = lambda x, n: format(x, "b").zfill(n)

# List of all possible cuts
all_possible_cuts = [bin(k)[2:].rjust(n_nodes, "0") for k in range(2**n_nodes)]

# List with the costs associated to each cut
all_costs = [calculate_cost(cut, graph) for cut in all_possible_cuts]

# Get the maximum cost
maxcost = max(all_costs)

# Get all cuts that correspond to the maximum cost
maxcuts = [get_binary(i, n_nodes) for i, j in enumerate(all_costs) if j == maxcost]
print(f"The maximum cut is represented by the bitstrings {maxcuts}, with a cost of {maxcost}")
The maximum cut is represented by the bitstrings ['0011', '0101', '0110', '1001', '1010', '1100'], with a cost of 4

The QAOA quantum circuit

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:

\[H_c = \frac12 \sum_{\langle i,j\rangle} (\mathbb{1} - Z_iZ_j)\]

where \(\langle i,j\rangle\) 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 \(|+\rangle ^{\otimes n}\). The cost operator of each layer can be built "manually", implementing the \(e^{iZZ\gamma}\) terms with CNOTs and a \(\rm{RZ}(2\gamma)\) 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 in range(n_qubits)) - zz_ops)


# QAOA circuit
def build_qaoa_circuit(n_qubits, n_layers, graph):
    layers = []
    # Layer of Hadamards
    initial_layer = kron(H(i) for i in range(n_qubits))
    layers.append(initial_layer)
    for layer in range(n_layers):

        # cost layer with digital decomposition
        # cost_layer = HamEvo(cost_ham, f"g{layer}").digital_decomposition(approximation="basic")
        cost_layer = []
        for edge in graph.edges():
            (q0, q1) = edge
            zz_term = chain(
                CNOT(q0, q1),
                RZ(q1, Parameter(f"g{layer}")),
                CNOT(q0, q1),
            )
            cost_layer.append(zz_term)
        cost_layer = chain(*cost_layer)
        cost_layer = tag(cost_layer, "cost")

        # mixing layer with single qubit rotations
        mixing_layer = kron(RX(i, f"b{layer}") for i in range(n_qubits))
        mixing_layer = tag(mixing_layer, "mixing")

        # putting all together in a single ChainBlock
        layers.append(chain(cost_layer, mixing_layer))

    final_b = chain(*layers)
    return QuantumCircuit(n_qubits, final_b)


circuit = build_qaoa_circuit(n_qubits, n_layers, graph)

# Print a single layer of the circuit
%3 cluster_df0da9e488e046cb8f84c276c68f79f0 mixing cluster_b9458132dc484d53ab46a5cc3aebc10a cost d6040a46021c435b8a0c3d014003b92e 0 68637c335b2844f686023e07ec40eeb3 H d6040a46021c435b8a0c3d014003b92e--68637c335b2844f686023e07ec40eeb3 be79d8e7b2b9429dbbb7def5215c9701 1 cb32ce4e6bc247d9a0eb6309c5c0c0e1 68637c335b2844f686023e07ec40eeb3--cb32ce4e6bc247d9a0eb6309c5c0c0e1 f27ce2ee71bd44038d1ee7b203b4c6f0 cb32ce4e6bc247d9a0eb6309c5c0c0e1--f27ce2ee71bd44038d1ee7b203b4c6f0 a4a264a817784858bfd3bdbe151a38a7 f27ce2ee71bd44038d1ee7b203b4c6f0--a4a264a817784858bfd3bdbe151a38a7 d47406f62f23486081ec95a7d3755b7c a4a264a817784858bfd3bdbe151a38a7--d47406f62f23486081ec95a7d3755b7c acd5cd641b414a38a7a08aec1121d97f d47406f62f23486081ec95a7d3755b7c--acd5cd641b414a38a7a08aec1121d97f 95383381d58e4ca9a9e6a5a6682d1f7f acd5cd641b414a38a7a08aec1121d97f--95383381d58e4ca9a9e6a5a6682d1f7f b2950db65c304d599e86795e0d9d5f15 95383381d58e4ca9a9e6a5a6682d1f7f--b2950db65c304d599e86795e0d9d5f15 70577792c9784006b16012606f275698 b2950db65c304d599e86795e0d9d5f15--70577792c9784006b16012606f275698 b196a20ad2f649bb812e87d4abf7db96 70577792c9784006b16012606f275698--b196a20ad2f649bb812e87d4abf7db96 b6ad04d5fd074df88904ae6a0c755280 b196a20ad2f649bb812e87d4abf7db96--b6ad04d5fd074df88904ae6a0c755280 0fa0fd71b70142d2b56becc6d9653ccb b6ad04d5fd074df88904ae6a0c755280--0fa0fd71b70142d2b56becc6d9653ccb e3a13705538a4e44a9b7872e7622820a 0fa0fd71b70142d2b56becc6d9653ccb--e3a13705538a4e44a9b7872e7622820a 8c67ec64dcb846b29c76d9d98552f357 e3a13705538a4e44a9b7872e7622820a--8c67ec64dcb846b29c76d9d98552f357 48f38229e7f84cbf9fb1e6515b708504 8c67ec64dcb846b29c76d9d98552f357--48f38229e7f84cbf9fb1e6515b708504 fe651662792c4512afc964ab972b8c3d 48f38229e7f84cbf9fb1e6515b708504--fe651662792c4512afc964ab972b8c3d 3d800ad1b57e4925a6c9e0e67618696f fe651662792c4512afc964ab972b8c3d--3d800ad1b57e4925a6c9e0e67618696f 3a0e6fd0cda743089c6f785a8ee7c556 3d800ad1b57e4925a6c9e0e67618696f--3a0e6fd0cda743089c6f785a8ee7c556 b054829b635841fea749e123353dc54c 3a0e6fd0cda743089c6f785a8ee7c556--b054829b635841fea749e123353dc54c 8c881494cadd4749adf71c3173c68123 RX(b0) b054829b635841fea749e123353dc54c--8c881494cadd4749adf71c3173c68123 75cc304583644c439f3b9347845375b4 8c881494cadd4749adf71c3173c68123--75cc304583644c439f3b9347845375b4 c55df32c56b446e8b2a08bf0802b60d3 c5907d0ad4d14c998bb0737c96e5e943 H be79d8e7b2b9429dbbb7def5215c9701--c5907d0ad4d14c998bb0737c96e5e943 45eca8718d1a42b28324400c3d72450b 2 b9c8bcaf6daf4e5c8bc27a4823d67e28 X c5907d0ad4d14c998bb0737c96e5e943--b9c8bcaf6daf4e5c8bc27a4823d67e28 b9c8bcaf6daf4e5c8bc27a4823d67e28--cb32ce4e6bc247d9a0eb6309c5c0c0e1 59d4573e378a413abf390e7b19893d31 RZ(g0) b9c8bcaf6daf4e5c8bc27a4823d67e28--59d4573e378a413abf390e7b19893d31 6463f13986d9437fa1ba1a01f2ef5c05 X 59d4573e378a413abf390e7b19893d31--6463f13986d9437fa1ba1a01f2ef5c05 6463f13986d9437fa1ba1a01f2ef5c05--a4a264a817784858bfd3bdbe151a38a7 d4fc611924014d82b644cef200278c1d 6463f13986d9437fa1ba1a01f2ef5c05--d4fc611924014d82b644cef200278c1d e4630b01510d4caeb4a30b7a5981c7c0 d4fc611924014d82b644cef200278c1d--e4630b01510d4caeb4a30b7a5981c7c0 9bbdc804e87a415ba66d11b3625183a4 e4630b01510d4caeb4a30b7a5981c7c0--9bbdc804e87a415ba66d11b3625183a4 fbfb8cdae8e845da9ce226aae259ac97 9bbdc804e87a415ba66d11b3625183a4--fbfb8cdae8e845da9ce226aae259ac97 99a038e2bce94e63b76736eb48948f22 fbfb8cdae8e845da9ce226aae259ac97--99a038e2bce94e63b76736eb48948f22 2e96b3f0364e465690f2a6be70aa190d 99a038e2bce94e63b76736eb48948f22--2e96b3f0364e465690f2a6be70aa190d b907b245e8554225b648d07edffe0d71 2e96b3f0364e465690f2a6be70aa190d--b907b245e8554225b648d07edffe0d71 f5b1f0680bb8416888701079ec4c6126 b907b245e8554225b648d07edffe0d71--f5b1f0680bb8416888701079ec4c6126 879248cb30cd4458ad7809fbaaa2139a f5b1f0680bb8416888701079ec4c6126--879248cb30cd4458ad7809fbaaa2139a a75852daf6db44eea9f38c27ea7dca42 879248cb30cd4458ad7809fbaaa2139a--a75852daf6db44eea9f38c27ea7dca42 05b02aa7bd0b45148fbe6355cb7ec198 a75852daf6db44eea9f38c27ea7dca42--05b02aa7bd0b45148fbe6355cb7ec198 1db4c7c20b4c425288004fd81d90b559 05b02aa7bd0b45148fbe6355cb7ec198--1db4c7c20b4c425288004fd81d90b559 5918a9ada16a4431a1a1d542a3a1d5db 1db4c7c20b4c425288004fd81d90b559--5918a9ada16a4431a1a1d542a3a1d5db 7eb15d7796754848851f629cdf97c9a2 5918a9ada16a4431a1a1d542a3a1d5db--7eb15d7796754848851f629cdf97c9a2 04efa88ed01f4403b240c164a39f6bec 7eb15d7796754848851f629cdf97c9a2--04efa88ed01f4403b240c164a39f6bec a8170dcd635546b6ba5a453307e09789 RX(b0) 04efa88ed01f4403b240c164a39f6bec--a8170dcd635546b6ba5a453307e09789 a8170dcd635546b6ba5a453307e09789--c55df32c56b446e8b2a08bf0802b60d3 3842c1d1a3584f4da7c675cffe2afa73 6a83a7bac1794adcbd5c87762e2f2631 H 45eca8718d1a42b28324400c3d72450b--6a83a7bac1794adcbd5c87762e2f2631 2668e1b700ad4a1ba03fd6cbd0f1288d 3 9d2294ea3065458d8959e597662e7c6e 6a83a7bac1794adcbd5c87762e2f2631--9d2294ea3065458d8959e597662e7c6e 18218c5ae73f49a0ae757a3308b8b3bb 9d2294ea3065458d8959e597662e7c6e--18218c5ae73f49a0ae757a3308b8b3bb 069f15b35d6149649d8862a3611fa058 18218c5ae73f49a0ae757a3308b8b3bb--069f15b35d6149649d8862a3611fa058 7358d73c534e4078af2fb080627a4e78 X 069f15b35d6149649d8862a3611fa058--7358d73c534e4078af2fb080627a4e78 7358d73c534e4078af2fb080627a4e78--d47406f62f23486081ec95a7d3755b7c 2a1d2989ba91439ca68ced7236c0b3df RZ(g0) 7358d73c534e4078af2fb080627a4e78--2a1d2989ba91439ca68ced7236c0b3df af79c7507e004e79a6f7ba7244b8e216 X 2a1d2989ba91439ca68ced7236c0b3df--af79c7507e004e79a6f7ba7244b8e216 af79c7507e004e79a6f7ba7244b8e216--95383381d58e4ca9a9e6a5a6682d1f7f 79263d6913414a00a8af860a016b348f af79c7507e004e79a6f7ba7244b8e216--79263d6913414a00a8af860a016b348f c3295f2d82b54b87ab097127bc8f1751 79263d6913414a00a8af860a016b348f--c3295f2d82b54b87ab097127bc8f1751 de121b86c7c144f78d9f20fd309b85f9 c3295f2d82b54b87ab097127bc8f1751--de121b86c7c144f78d9f20fd309b85f9 0883d05f8a6f47c9a78396a902349843 X de121b86c7c144f78d9f20fd309b85f9--0883d05f8a6f47c9a78396a902349843 0883d05f8a6f47c9a78396a902349843--b907b245e8554225b648d07edffe0d71 05d245a34a224e8c85e376b6fb5c922b RZ(g0) 0883d05f8a6f47c9a78396a902349843--05d245a34a224e8c85e376b6fb5c922b 03780694db254e28a2005df9b9a6b13a X 05d245a34a224e8c85e376b6fb5c922b--03780694db254e28a2005df9b9a6b13a 03780694db254e28a2005df9b9a6b13a--879248cb30cd4458ad7809fbaaa2139a b2c7df76439e479cbe5652146aba9fb6 03780694db254e28a2005df9b9a6b13a--b2c7df76439e479cbe5652146aba9fb6 cf1ed815ece443339c5ca74df569931d b2c7df76439e479cbe5652146aba9fb6--cf1ed815ece443339c5ca74df569931d 0a10bc24260a44898a1cef4ee4c6646a cf1ed815ece443339c5ca74df569931d--0a10bc24260a44898a1cef4ee4c6646a a2bcec1bd1ae42b7967c431211b52d46 0a10bc24260a44898a1cef4ee4c6646a--a2bcec1bd1ae42b7967c431211b52d46 59b5f5d8be63440e908f167c456b0bb6 a2bcec1bd1ae42b7967c431211b52d46--59b5f5d8be63440e908f167c456b0bb6 7eb8af10a8824564861fad1a91e46ccb 59b5f5d8be63440e908f167c456b0bb6--7eb8af10a8824564861fad1a91e46ccb 05263260ec2e43a2a66e8377672dc057 RX(b0) 7eb8af10a8824564861fad1a91e46ccb--05263260ec2e43a2a66e8377672dc057 05263260ec2e43a2a66e8377672dc057--3842c1d1a3584f4da7c675cffe2afa73 e322b23348584872b565c36195ca1bce d1c6bf04db3a47ccb053e6a7d073f541 H 2668e1b700ad4a1ba03fd6cbd0f1288d--d1c6bf04db3a47ccb053e6a7d073f541 943f0ba9b83448e9831de1169be5493d d1c6bf04db3a47ccb053e6a7d073f541--943f0ba9b83448e9831de1169be5493d 5a61687c2b4b4c4daa6ce00e0baa74ec 943f0ba9b83448e9831de1169be5493d--5a61687c2b4b4c4daa6ce00e0baa74ec 41e84a87725349daa3627fe9fd434158 5a61687c2b4b4c4daa6ce00e0baa74ec--41e84a87725349daa3627fe9fd434158 a615fb2e4c994944b67d27bc7e63e819 41e84a87725349daa3627fe9fd434158--a615fb2e4c994944b67d27bc7e63e819 8330f507a59e4578af61afddf8945961 a615fb2e4c994944b67d27bc7e63e819--8330f507a59e4578af61afddf8945961 2a8da8e71dad4e32b741c925cdb78849 8330f507a59e4578af61afddf8945961--2a8da8e71dad4e32b741c925cdb78849 ff4fa22284d54dc99828179dbab9003a X 2a8da8e71dad4e32b741c925cdb78849--ff4fa22284d54dc99828179dbab9003a ff4fa22284d54dc99828179dbab9003a--b2950db65c304d599e86795e0d9d5f15 691d8eff126940ab99c69eda87aef568 RZ(g0) ff4fa22284d54dc99828179dbab9003a--691d8eff126940ab99c69eda87aef568 c9fb44a190ae4a5582644b99773b66ea X 691d8eff126940ab99c69eda87aef568--c9fb44a190ae4a5582644b99773b66ea c9fb44a190ae4a5582644b99773b66ea--b196a20ad2f649bb812e87d4abf7db96 28b5ecf3139b4965a9f18b631285146a c9fb44a190ae4a5582644b99773b66ea--28b5ecf3139b4965a9f18b631285146a 5496e966c1ab44dcaa52c47ce5c88728 28b5ecf3139b4965a9f18b631285146a--5496e966c1ab44dcaa52c47ce5c88728 010011d8ccad4d13ad82474d142b069d 5496e966c1ab44dcaa52c47ce5c88728--010011d8ccad4d13ad82474d142b069d a2984110cf0d4c91b93abec46f1bed39 X 010011d8ccad4d13ad82474d142b069d--a2984110cf0d4c91b93abec46f1bed39 a2984110cf0d4c91b93abec46f1bed39--a75852daf6db44eea9f38c27ea7dca42 f169cf9bf8f74496af64bb645e9ca374 RZ(g0) a2984110cf0d4c91b93abec46f1bed39--f169cf9bf8f74496af64bb645e9ca374 4fad52ef05ff45c1b91a621d2fdf7e2a X f169cf9bf8f74496af64bb645e9ca374--4fad52ef05ff45c1b91a621d2fdf7e2a 4fad52ef05ff45c1b91a621d2fdf7e2a--1db4c7c20b4c425288004fd81d90b559 c9249554ac3b4a98b4e752dd9b05ac2e X 4fad52ef05ff45c1b91a621d2fdf7e2a--c9249554ac3b4a98b4e752dd9b05ac2e c9249554ac3b4a98b4e752dd9b05ac2e--a2bcec1bd1ae42b7967c431211b52d46 e45a09ea6b9d40b4be731f191cb763e1 RZ(g0) c9249554ac3b4a98b4e752dd9b05ac2e--e45a09ea6b9d40b4be731f191cb763e1 198ff9575e6f4ad1834e65d76cbeab45 X e45a09ea6b9d40b4be731f191cb763e1--198ff9575e6f4ad1834e65d76cbeab45 198ff9575e6f4ad1834e65d76cbeab45--7eb8af10a8824564861fad1a91e46ccb 56dd62686692444ebef173f5aca03ea8 RX(b0) 198ff9575e6f4ad1834e65d76cbeab45--56dd62686692444ebef173f5aca03ea8 56dd62686692444ebef173f5aca03ea8--e322b23348584872b565c36195ca1bce

Train the QAOA circuit to solve MaxCut

Given the QAOA circuit above, one can construct the associated Qadence QuantumModel and train it using standard gradient based optimization.

The loss function to be minimized reads:

\[\mathcal{L} =-\langle \psi | H_c| \psi \rangle= -\frac12 \sum_{\langle i,j\rangle} \left(1 - \langle \psi | Z_i Z_j | \psi \rangle \right)\]

where \(|\psi\rangle(\beta, \gamma)\) is the wavefunction obtained by running the QAQA quantum circuit and the sum runs over the edges of the graph \(\langle i,j\rangle\).

import torch
from qadence import QuantumModel

torch.manual_seed(seed)


def loss_function(model: QuantumModel):
    # The loss corresponds to the expectation
    # value of the cost Hamiltonian
    return -1.0 * model.expectation().squeeze()


# initialize the parameters to random values
model = QuantumModel(circuit, observable=cost_ham)
model.reset_vparams(torch.rand(model.num_vparams))
initial_loss = loss_function(model)
print(f"Initial loss: {initial_loss}")

# train the model
n_epochs = 100
lr = 0.1

optimizer = torch.optim.Adam(model.parameters(), lr=lr)

for i in range(n_epochs):
    optimizer.zero_grad()
    loss = loss_function(model)
    loss.backward()
    optimizer.step()
    if (i + 1) % (n_epochs // 10) == 0:
        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.

Results

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
colors = []
labels = {}
for node, b in zip(graph.nodes(), most_frequent):
    colors.append("green") if int(b) == 0 else colors.append("red")
    labels[node] = "A" if int(b) == 0 else "B"

nx.draw_networkx(graph, node_color=colors, with_labels=True, labels=labels)
Most frequently sampled bitstring corresponding to the maximum cut: 1001 2024-08-22T12:51:59.447807 image/svg+xml Matplotlib v3.7.5, https://matplotlib.org/

References


  1. Farhi et al. - A Quantum Approximate Optimization Algorithm