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-09-20T08:35:17.798208 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_9a047520f86b4922a646b3cc1a34c04a mixing cluster_55f39f9de1d4434dab79b9fde38ccc72 cost aba71dc5a1c345f3be365308c3953ba8 0 80f77a372132450f8e5077710b8f8296 H aba71dc5a1c345f3be365308c3953ba8--80f77a372132450f8e5077710b8f8296 5cbd44834f7a4291b592129a26bbb83f 1 7575c508567f4bbf8795545217b6610f 80f77a372132450f8e5077710b8f8296--7575c508567f4bbf8795545217b6610f c364d561f0c84ff0a4ee9a8b2da7757c 7575c508567f4bbf8795545217b6610f--c364d561f0c84ff0a4ee9a8b2da7757c 333e3115b29f4882bb3023773f7aadf4 c364d561f0c84ff0a4ee9a8b2da7757c--333e3115b29f4882bb3023773f7aadf4 86d3c949fc644b4e9f868c4d772ecdd1 333e3115b29f4882bb3023773f7aadf4--86d3c949fc644b4e9f868c4d772ecdd1 a6adcc52018845e9bd4be182654ad155 86d3c949fc644b4e9f868c4d772ecdd1--a6adcc52018845e9bd4be182654ad155 0133092d5e814ea085df4412c0dbb532 a6adcc52018845e9bd4be182654ad155--0133092d5e814ea085df4412c0dbb532 8947bb61384e4c4c800b55a43485738d 0133092d5e814ea085df4412c0dbb532--8947bb61384e4c4c800b55a43485738d 3980ae44901e4e82ac547f51515005c1 8947bb61384e4c4c800b55a43485738d--3980ae44901e4e82ac547f51515005c1 a4af0a9de93b4c739dd0f5ea1f84c5bc 3980ae44901e4e82ac547f51515005c1--a4af0a9de93b4c739dd0f5ea1f84c5bc baa45786ed3c4d90a4b3cad7dda9fcd7 a4af0a9de93b4c739dd0f5ea1f84c5bc--baa45786ed3c4d90a4b3cad7dda9fcd7 1b03c873d25040bab34c6c88d49d8e0b baa45786ed3c4d90a4b3cad7dda9fcd7--1b03c873d25040bab34c6c88d49d8e0b 6a389b64c05b439aadc816e2ee6ab225 1b03c873d25040bab34c6c88d49d8e0b--6a389b64c05b439aadc816e2ee6ab225 34e378ae20754907aba9b99f36c96e57 6a389b64c05b439aadc816e2ee6ab225--34e378ae20754907aba9b99f36c96e57 f00af454504640d6834f9043eec33880 34e378ae20754907aba9b99f36c96e57--f00af454504640d6834f9043eec33880 950355f462bd44e395ea3d1a6bf4d370 f00af454504640d6834f9043eec33880--950355f462bd44e395ea3d1a6bf4d370 4e2094362c43462caa3d0744da3ee1aa 950355f462bd44e395ea3d1a6bf4d370--4e2094362c43462caa3d0744da3ee1aa 98d5c8c30f2f40328005684c1302856f 4e2094362c43462caa3d0744da3ee1aa--98d5c8c30f2f40328005684c1302856f dc135df147be4ca1bebec4232f686121 98d5c8c30f2f40328005684c1302856f--dc135df147be4ca1bebec4232f686121 47d7905d1abe44048984674583d87cee RX(b0) dc135df147be4ca1bebec4232f686121--47d7905d1abe44048984674583d87cee df8d661c43864514a4f9dd8ea82aa51a 47d7905d1abe44048984674583d87cee--df8d661c43864514a4f9dd8ea82aa51a bae7246548424e958192e689fec3836c e06308b6a4ad4c41972c4a4371ea9232 H 5cbd44834f7a4291b592129a26bbb83f--e06308b6a4ad4c41972c4a4371ea9232 223b8fbc96e64876aeafacaf32cd86cc 2 75a29b3408c04cf5ba1e45136e03f0f5 X e06308b6a4ad4c41972c4a4371ea9232--75a29b3408c04cf5ba1e45136e03f0f5 75a29b3408c04cf5ba1e45136e03f0f5--7575c508567f4bbf8795545217b6610f 6dd4502ecd374a7bbd00557e72c54c96 RZ(g0) 75a29b3408c04cf5ba1e45136e03f0f5--6dd4502ecd374a7bbd00557e72c54c96 0a0b2cc6f38e44e0b2a814522cf542c6 X 6dd4502ecd374a7bbd00557e72c54c96--0a0b2cc6f38e44e0b2a814522cf542c6 0a0b2cc6f38e44e0b2a814522cf542c6--333e3115b29f4882bb3023773f7aadf4 761ea6678a744894846c9e8b4bd2d568 0a0b2cc6f38e44e0b2a814522cf542c6--761ea6678a744894846c9e8b4bd2d568 ddbdca3802f543278bdc4859063aa8a6 761ea6678a744894846c9e8b4bd2d568--ddbdca3802f543278bdc4859063aa8a6 ec91224a7ded420194ce5db4425c28ac ddbdca3802f543278bdc4859063aa8a6--ec91224a7ded420194ce5db4425c28ac 1a849ff9a3ad447bb63f25a0d5d67edb ec91224a7ded420194ce5db4425c28ac--1a849ff9a3ad447bb63f25a0d5d67edb d568548644c747d8bcf8dd3ec8d2ba90 1a849ff9a3ad447bb63f25a0d5d67edb--d568548644c747d8bcf8dd3ec8d2ba90 361c49114adf40aaa680a6cec3f046a0 d568548644c747d8bcf8dd3ec8d2ba90--361c49114adf40aaa680a6cec3f046a0 2e8243722249462696c3c586d20e0da5 361c49114adf40aaa680a6cec3f046a0--2e8243722249462696c3c586d20e0da5 1fdf35fbc71540e1a555ab86e9f3689d 2e8243722249462696c3c586d20e0da5--1fdf35fbc71540e1a555ab86e9f3689d ef7571479f0c4798aaa776f7a7d08461 1fdf35fbc71540e1a555ab86e9f3689d--ef7571479f0c4798aaa776f7a7d08461 2cea9bb8d6bb4ec8a3b747715a8be685 ef7571479f0c4798aaa776f7a7d08461--2cea9bb8d6bb4ec8a3b747715a8be685 830c8f5797024f84af571f95b66cd350 2cea9bb8d6bb4ec8a3b747715a8be685--830c8f5797024f84af571f95b66cd350 1e96ab373ffe4bf59bedaaac646855a1 830c8f5797024f84af571f95b66cd350--1e96ab373ffe4bf59bedaaac646855a1 d9fde22c0b874352a7bc28d4d973ee0e 1e96ab373ffe4bf59bedaaac646855a1--d9fde22c0b874352a7bc28d4d973ee0e fd68d1ebb7ca4f63a2df240a5914dcc9 d9fde22c0b874352a7bc28d4d973ee0e--fd68d1ebb7ca4f63a2df240a5914dcc9 1aaefcd25b8143dabb3f5685e8b48354 fd68d1ebb7ca4f63a2df240a5914dcc9--1aaefcd25b8143dabb3f5685e8b48354 a554282dc3604db78b0108854250119c RX(b0) 1aaefcd25b8143dabb3f5685e8b48354--a554282dc3604db78b0108854250119c a554282dc3604db78b0108854250119c--bae7246548424e958192e689fec3836c 9d602e4bef2849e48f4d405d67889259 d1d24cb068204e699117f122de691608 H 223b8fbc96e64876aeafacaf32cd86cc--d1d24cb068204e699117f122de691608 0e7d035105584f32b57c28bc6df914d3 3 0fe5abddf91d44018769039168a6a049 d1d24cb068204e699117f122de691608--0fe5abddf91d44018769039168a6a049 b4a3136752ce46aca3427f743ad11e9a 0fe5abddf91d44018769039168a6a049--b4a3136752ce46aca3427f743ad11e9a 8c5f184c319b467c997a578fc648a734 b4a3136752ce46aca3427f743ad11e9a--8c5f184c319b467c997a578fc648a734 e510f1057514466b8144c62f995e446f X 8c5f184c319b467c997a578fc648a734--e510f1057514466b8144c62f995e446f e510f1057514466b8144c62f995e446f--86d3c949fc644b4e9f868c4d772ecdd1 83298ed2d3994ff382624961576c5bce RZ(g0) e510f1057514466b8144c62f995e446f--83298ed2d3994ff382624961576c5bce aa087829446440748c154656419d036c X 83298ed2d3994ff382624961576c5bce--aa087829446440748c154656419d036c aa087829446440748c154656419d036c--0133092d5e814ea085df4412c0dbb532 e0936ef87bbe4c00ad3ac7772287959d aa087829446440748c154656419d036c--e0936ef87bbe4c00ad3ac7772287959d e6198640dfaf4e5fb499772d644c2055 e0936ef87bbe4c00ad3ac7772287959d--e6198640dfaf4e5fb499772d644c2055 0ffc59efee034dae97e375cf9fb507f4 e6198640dfaf4e5fb499772d644c2055--0ffc59efee034dae97e375cf9fb507f4 d8c7665da6e443d6a1fab0a0898b5349 X 0ffc59efee034dae97e375cf9fb507f4--d8c7665da6e443d6a1fab0a0898b5349 d8c7665da6e443d6a1fab0a0898b5349--2e8243722249462696c3c586d20e0da5 b57db81686424333801e1d39ec81ede9 RZ(g0) d8c7665da6e443d6a1fab0a0898b5349--b57db81686424333801e1d39ec81ede9 74891d8265d044a2a0f5fd895df178c7 X b57db81686424333801e1d39ec81ede9--74891d8265d044a2a0f5fd895df178c7 74891d8265d044a2a0f5fd895df178c7--ef7571479f0c4798aaa776f7a7d08461 5b6a6f25a62249bf91a7ed678bc85f5f 74891d8265d044a2a0f5fd895df178c7--5b6a6f25a62249bf91a7ed678bc85f5f 11ec92dd5dd04768be59e8187dfd1887 5b6a6f25a62249bf91a7ed678bc85f5f--11ec92dd5dd04768be59e8187dfd1887 52d77b994da04b86bbd433423f11d41b 11ec92dd5dd04768be59e8187dfd1887--52d77b994da04b86bbd433423f11d41b 539bae843d8549bebdcd08a6cf8e4834 52d77b994da04b86bbd433423f11d41b--539bae843d8549bebdcd08a6cf8e4834 04ed054f77414675b8dd027a294f4c92 539bae843d8549bebdcd08a6cf8e4834--04ed054f77414675b8dd027a294f4c92 3d14c75a752e481b80c9174e90175217 04ed054f77414675b8dd027a294f4c92--3d14c75a752e481b80c9174e90175217 525d55c30ea94893b15e38e8b18d9776 RX(b0) 3d14c75a752e481b80c9174e90175217--525d55c30ea94893b15e38e8b18d9776 525d55c30ea94893b15e38e8b18d9776--9d602e4bef2849e48f4d405d67889259 08f259011521493191c7bc3d5155bcb7 7d7fe58fda06417b8b55c7ff3dcabfca H 0e7d035105584f32b57c28bc6df914d3--7d7fe58fda06417b8b55c7ff3dcabfca 1c15e2266d2a48faa3399ee97ce1e793 7d7fe58fda06417b8b55c7ff3dcabfca--1c15e2266d2a48faa3399ee97ce1e793 5d1f2703f32f43898361021fd7e637c8 1c15e2266d2a48faa3399ee97ce1e793--5d1f2703f32f43898361021fd7e637c8 1f115c58637749649b04b4897f900433 5d1f2703f32f43898361021fd7e637c8--1f115c58637749649b04b4897f900433 11fba74746dc44578ca604455a73c0eb 1f115c58637749649b04b4897f900433--11fba74746dc44578ca604455a73c0eb 0e4784fff2964e1083fdbdf2a116fd5d 11fba74746dc44578ca604455a73c0eb--0e4784fff2964e1083fdbdf2a116fd5d 1ae70b50ed634c14a7ab859d4c4309f6 0e4784fff2964e1083fdbdf2a116fd5d--1ae70b50ed634c14a7ab859d4c4309f6 bb5da3739e3d4cee96781fc419531940 X 1ae70b50ed634c14a7ab859d4c4309f6--bb5da3739e3d4cee96781fc419531940 bb5da3739e3d4cee96781fc419531940--8947bb61384e4c4c800b55a43485738d f6441e3d995e43b3863a3420b84718c9 RZ(g0) bb5da3739e3d4cee96781fc419531940--f6441e3d995e43b3863a3420b84718c9 1eced17ab4f748b2b62bd854e88a1adb X f6441e3d995e43b3863a3420b84718c9--1eced17ab4f748b2b62bd854e88a1adb 1eced17ab4f748b2b62bd854e88a1adb--a4af0a9de93b4c739dd0f5ea1f84c5bc 9075d055a0a545b7b3cb6dc27729a9d7 1eced17ab4f748b2b62bd854e88a1adb--9075d055a0a545b7b3cb6dc27729a9d7 28ddb6d4e3bd45898b8cdd0ee72810b8 9075d055a0a545b7b3cb6dc27729a9d7--28ddb6d4e3bd45898b8cdd0ee72810b8 ed810d028d1047d28a8ddce31331bc67 28ddb6d4e3bd45898b8cdd0ee72810b8--ed810d028d1047d28a8ddce31331bc67 e4cc168b3e0e418a93b1f7198358075c X ed810d028d1047d28a8ddce31331bc67--e4cc168b3e0e418a93b1f7198358075c e4cc168b3e0e418a93b1f7198358075c--2cea9bb8d6bb4ec8a3b747715a8be685 a8911956a2844aea8c146e950e4d0daa RZ(g0) e4cc168b3e0e418a93b1f7198358075c--a8911956a2844aea8c146e950e4d0daa 56f0bc0b7c0649c982fbd5f26e41ec8c X a8911956a2844aea8c146e950e4d0daa--56f0bc0b7c0649c982fbd5f26e41ec8c 56f0bc0b7c0649c982fbd5f26e41ec8c--1e96ab373ffe4bf59bedaaac646855a1 7b29886695cf4699bc4f1c3e095e9b85 X 56f0bc0b7c0649c982fbd5f26e41ec8c--7b29886695cf4699bc4f1c3e095e9b85 7b29886695cf4699bc4f1c3e095e9b85--539bae843d8549bebdcd08a6cf8e4834 62e43d3cc190470a980c87f43ea965b1 RZ(g0) 7b29886695cf4699bc4f1c3e095e9b85--62e43d3cc190470a980c87f43ea965b1 846c6ba8cd2f4b2093074c708273e826 X 62e43d3cc190470a980c87f43ea965b1--846c6ba8cd2f4b2093074c708273e826 846c6ba8cd2f4b2093074c708273e826--3d14c75a752e481b80c9174e90175217 cc1a1e6f882046759312e2e5e6905c88 RX(b0) 846c6ba8cd2f4b2093074c708273e826--cc1a1e6f882046759312e2e5e6905c88 cc1a1e6f882046759312e2e5e6905c88--08f259011521493191c7bc3d5155bcb7

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-09-20T08:35:22.447697 image/svg+xml Matplotlib v3.7.5, https://matplotlib.org/

References


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