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)
2025-02-14T12:51:37.012812 image/svg+xml Matplotlib v3.10.0, 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_b05477b83a214cba9075813ad3f94f8e mixing cluster_3ca95053700244648cb8ca42607113f6 cost 42c13a8fbd2d4a3c948987fc7256808a 0 791f07baa3004062855d4f40a572c997 H 42c13a8fbd2d4a3c948987fc7256808a--791f07baa3004062855d4f40a572c997 b8b431dd5a6d463dbdd8c3fd0130b151 1 679945d220c148769e6c66ee08c94e2b 791f07baa3004062855d4f40a572c997--679945d220c148769e6c66ee08c94e2b 63f81d9d08334ae9a0ee9ed96eae20f5 679945d220c148769e6c66ee08c94e2b--63f81d9d08334ae9a0ee9ed96eae20f5 cf66e9bb008d498abb341d227275a35d 63f81d9d08334ae9a0ee9ed96eae20f5--cf66e9bb008d498abb341d227275a35d fe722919b73b47778bd239bc7a28533e cf66e9bb008d498abb341d227275a35d--fe722919b73b47778bd239bc7a28533e cfe21f19e6f841d0a85313286e47aa06 fe722919b73b47778bd239bc7a28533e--cfe21f19e6f841d0a85313286e47aa06 55b79931925747a1b1f891794452135c cfe21f19e6f841d0a85313286e47aa06--55b79931925747a1b1f891794452135c 1af77930f7f14ed58053a74a0206688d 55b79931925747a1b1f891794452135c--1af77930f7f14ed58053a74a0206688d 9c84c4fa276c48629ca95a0eaa8fe4f9 1af77930f7f14ed58053a74a0206688d--9c84c4fa276c48629ca95a0eaa8fe4f9 e3e5c7c2abcb4bc5957dab0fb6768bb8 9c84c4fa276c48629ca95a0eaa8fe4f9--e3e5c7c2abcb4bc5957dab0fb6768bb8 024029f2eccd4997a84f7c1e3ed3f3c4 e3e5c7c2abcb4bc5957dab0fb6768bb8--024029f2eccd4997a84f7c1e3ed3f3c4 fb0aa07ebdfe40e792333475a1866c94 024029f2eccd4997a84f7c1e3ed3f3c4--fb0aa07ebdfe40e792333475a1866c94 c52fe8fbc6f24229972e2936cc4f98f6 fb0aa07ebdfe40e792333475a1866c94--c52fe8fbc6f24229972e2936cc4f98f6 553b274260b54951883e5908edf52a4b c52fe8fbc6f24229972e2936cc4f98f6--553b274260b54951883e5908edf52a4b 4a32fc0c37604af2bff2b81ee8b4719a 553b274260b54951883e5908edf52a4b--4a32fc0c37604af2bff2b81ee8b4719a df31ec0464f74d1792080f748f2c1429 4a32fc0c37604af2bff2b81ee8b4719a--df31ec0464f74d1792080f748f2c1429 caabdccdb0d343f38115922aa06ccc7f df31ec0464f74d1792080f748f2c1429--caabdccdb0d343f38115922aa06ccc7f 16ef369beacb4c498cbe7a909cfd9961 caabdccdb0d343f38115922aa06ccc7f--16ef369beacb4c498cbe7a909cfd9961 12d30cd503a247f187271ed2e9216b31 16ef369beacb4c498cbe7a909cfd9961--12d30cd503a247f187271ed2e9216b31 560dee8f3b4d4b79ae0b54ad11881cc7 RX(b0) 12d30cd503a247f187271ed2e9216b31--560dee8f3b4d4b79ae0b54ad11881cc7 12e926502eb14ad1b4fe5c1ec6c78cbd 560dee8f3b4d4b79ae0b54ad11881cc7--12e926502eb14ad1b4fe5c1ec6c78cbd 655fb622856d4325a5872f742b61774a a792b02bfae84f9883602a0b486955c5 H b8b431dd5a6d463dbdd8c3fd0130b151--a792b02bfae84f9883602a0b486955c5 376dcd7406b348d69cc303986ce591f1 2 a9055ff6db83432c9aad2414ec3825a5 X a792b02bfae84f9883602a0b486955c5--a9055ff6db83432c9aad2414ec3825a5 a9055ff6db83432c9aad2414ec3825a5--679945d220c148769e6c66ee08c94e2b b525429a9574463c9f7f8bbe72154129 RZ(g0) a9055ff6db83432c9aad2414ec3825a5--b525429a9574463c9f7f8bbe72154129 c52e5cfb649144b686b4bdf3e2b45e5c X b525429a9574463c9f7f8bbe72154129--c52e5cfb649144b686b4bdf3e2b45e5c c52e5cfb649144b686b4bdf3e2b45e5c--cf66e9bb008d498abb341d227275a35d 387fbb4152354787b7e2a393ef12071d c52e5cfb649144b686b4bdf3e2b45e5c--387fbb4152354787b7e2a393ef12071d 574039e90106482ab2190ea9a677b99f 387fbb4152354787b7e2a393ef12071d--574039e90106482ab2190ea9a677b99f 6ac83ab0fc81419d87a324f19fda59db 574039e90106482ab2190ea9a677b99f--6ac83ab0fc81419d87a324f19fda59db 29a80ef089674a81b2473d526ec34b2c 6ac83ab0fc81419d87a324f19fda59db--29a80ef089674a81b2473d526ec34b2c 7ae4ce48df2e4d52bef7c53b5f622067 29a80ef089674a81b2473d526ec34b2c--7ae4ce48df2e4d52bef7c53b5f622067 e60d82a14abd465aa473e13fdba9e4d4 7ae4ce48df2e4d52bef7c53b5f622067--e60d82a14abd465aa473e13fdba9e4d4 008af498869d4d1883661470408bd8a9 e60d82a14abd465aa473e13fdba9e4d4--008af498869d4d1883661470408bd8a9 fea7a971a22f4af7a89e902027e5f780 008af498869d4d1883661470408bd8a9--fea7a971a22f4af7a89e902027e5f780 8903da40163d40059d9ade1a4c4cfaeb fea7a971a22f4af7a89e902027e5f780--8903da40163d40059d9ade1a4c4cfaeb 96957d62623f43c099a14e602343c3c2 8903da40163d40059d9ade1a4c4cfaeb--96957d62623f43c099a14e602343c3c2 ddfb147a89124bea878bc9550bc6e95f 96957d62623f43c099a14e602343c3c2--ddfb147a89124bea878bc9550bc6e95f 044fb52ea1824fde9e7a905acc4d4482 ddfb147a89124bea878bc9550bc6e95f--044fb52ea1824fde9e7a905acc4d4482 168626c224de4722bb0ec65ab24776ac 044fb52ea1824fde9e7a905acc4d4482--168626c224de4722bb0ec65ab24776ac ae8c2629446f46c1a609d9ea964b46ee 168626c224de4722bb0ec65ab24776ac--ae8c2629446f46c1a609d9ea964b46ee 9b5fb4ef069e4272a0d058c66eb2d321 ae8c2629446f46c1a609d9ea964b46ee--9b5fb4ef069e4272a0d058c66eb2d321 4e014fe8020c41ba9210f1a9017f1bb3 RX(b0) 9b5fb4ef069e4272a0d058c66eb2d321--4e014fe8020c41ba9210f1a9017f1bb3 4e014fe8020c41ba9210f1a9017f1bb3--655fb622856d4325a5872f742b61774a cb09dee78d4843f791ca644b1bde227c ce9c657703b04649b4ec78d780243cac H 376dcd7406b348d69cc303986ce591f1--ce9c657703b04649b4ec78d780243cac e3f0cae9a1e444ff8353db7070f892d8 3 e4f054b791cd4dd2b56cf20b08b9f1d4 ce9c657703b04649b4ec78d780243cac--e4f054b791cd4dd2b56cf20b08b9f1d4 f66148f89afb43c2ae9e313033d263e5 e4f054b791cd4dd2b56cf20b08b9f1d4--f66148f89afb43c2ae9e313033d263e5 8e700c6df2474ece98bf4e4402f9217f f66148f89afb43c2ae9e313033d263e5--8e700c6df2474ece98bf4e4402f9217f 2adc09ff57744d75a5afed7921f52f86 X 8e700c6df2474ece98bf4e4402f9217f--2adc09ff57744d75a5afed7921f52f86 2adc09ff57744d75a5afed7921f52f86--fe722919b73b47778bd239bc7a28533e e4d72046b0534abb8049c18c24ab6a62 RZ(g0) 2adc09ff57744d75a5afed7921f52f86--e4d72046b0534abb8049c18c24ab6a62 034686d95085422d9beb5723f789ed90 X e4d72046b0534abb8049c18c24ab6a62--034686d95085422d9beb5723f789ed90 034686d95085422d9beb5723f789ed90--55b79931925747a1b1f891794452135c 3303568f5ee8415abcf54a1e15e39796 034686d95085422d9beb5723f789ed90--3303568f5ee8415abcf54a1e15e39796 412d7a78595144a6a67000a336eadf40 3303568f5ee8415abcf54a1e15e39796--412d7a78595144a6a67000a336eadf40 c8cf8e42218b458885d73f3ce2b16095 412d7a78595144a6a67000a336eadf40--c8cf8e42218b458885d73f3ce2b16095 5bafbc444a154631aea86f9ebefbeabf X c8cf8e42218b458885d73f3ce2b16095--5bafbc444a154631aea86f9ebefbeabf 5bafbc444a154631aea86f9ebefbeabf--008af498869d4d1883661470408bd8a9 ffeb03ea7f154657b548ee853eb2f138 RZ(g0) 5bafbc444a154631aea86f9ebefbeabf--ffeb03ea7f154657b548ee853eb2f138 cac693ebd20348ecbfb47369da353531 X ffeb03ea7f154657b548ee853eb2f138--cac693ebd20348ecbfb47369da353531 cac693ebd20348ecbfb47369da353531--8903da40163d40059d9ade1a4c4cfaeb 1218a2bf4f7c4f33992122b8d26da26b cac693ebd20348ecbfb47369da353531--1218a2bf4f7c4f33992122b8d26da26b 2e568d6cb50d45acaaf37a74730cd03b 1218a2bf4f7c4f33992122b8d26da26b--2e568d6cb50d45acaaf37a74730cd03b 0c41d9fef4234c90816b891a45699b8c 2e568d6cb50d45acaaf37a74730cd03b--0c41d9fef4234c90816b891a45699b8c 482979a008bb435b8d89e839d5123f8c 0c41d9fef4234c90816b891a45699b8c--482979a008bb435b8d89e839d5123f8c 2811e621f3f4497ca0e48e02b2cb45be 482979a008bb435b8d89e839d5123f8c--2811e621f3f4497ca0e48e02b2cb45be 6f88cb1e2f414e1e85907ce13104fe75 2811e621f3f4497ca0e48e02b2cb45be--6f88cb1e2f414e1e85907ce13104fe75 aec96a856b5c4295838f1b497555a0f8 RX(b0) 6f88cb1e2f414e1e85907ce13104fe75--aec96a856b5c4295838f1b497555a0f8 aec96a856b5c4295838f1b497555a0f8--cb09dee78d4843f791ca644b1bde227c c8311f8acc84471b8eeea0eaf24d10e9 bf3101441d93483085c2426655cc75e5 H e3f0cae9a1e444ff8353db7070f892d8--bf3101441d93483085c2426655cc75e5 04531380938b49b989b70578b7167d16 bf3101441d93483085c2426655cc75e5--04531380938b49b989b70578b7167d16 bc3b2c052b3b477f8bff74d30ec47cfa 04531380938b49b989b70578b7167d16--bc3b2c052b3b477f8bff74d30ec47cfa cfdda8bebdb84d5692f544727fe13a25 bc3b2c052b3b477f8bff74d30ec47cfa--cfdda8bebdb84d5692f544727fe13a25 78fc478a3ed34e69b9802c8510f6934c cfdda8bebdb84d5692f544727fe13a25--78fc478a3ed34e69b9802c8510f6934c 203f8e9a4626432b8d2ddc5b866b98ae 78fc478a3ed34e69b9802c8510f6934c--203f8e9a4626432b8d2ddc5b866b98ae 5abf2e7013bf472086f394c8fadff99c 203f8e9a4626432b8d2ddc5b866b98ae--5abf2e7013bf472086f394c8fadff99c 83e43d2a0e564cb3b256c37fa9fe7c59 X 5abf2e7013bf472086f394c8fadff99c--83e43d2a0e564cb3b256c37fa9fe7c59 83e43d2a0e564cb3b256c37fa9fe7c59--1af77930f7f14ed58053a74a0206688d c195d14c2b2c41999596ea7e260be5ba RZ(g0) 83e43d2a0e564cb3b256c37fa9fe7c59--c195d14c2b2c41999596ea7e260be5ba 5be547ed0aca4fb194040cbfee455661 X c195d14c2b2c41999596ea7e260be5ba--5be547ed0aca4fb194040cbfee455661 5be547ed0aca4fb194040cbfee455661--e3e5c7c2abcb4bc5957dab0fb6768bb8 3fd371f2eb3948c1b1d95cd2e1bba79e 5be547ed0aca4fb194040cbfee455661--3fd371f2eb3948c1b1d95cd2e1bba79e 6b71b58f27f94e46a3bd1242fe0e0120 3fd371f2eb3948c1b1d95cd2e1bba79e--6b71b58f27f94e46a3bd1242fe0e0120 28e676cb9a7449f99d8df627bc2b8402 6b71b58f27f94e46a3bd1242fe0e0120--28e676cb9a7449f99d8df627bc2b8402 56a4a50ed3714bdd9d972aa2bccb8fcb X 28e676cb9a7449f99d8df627bc2b8402--56a4a50ed3714bdd9d972aa2bccb8fcb 56a4a50ed3714bdd9d972aa2bccb8fcb--96957d62623f43c099a14e602343c3c2 2fa85fbeeff6446aafd54071079457f3 RZ(g0) 56a4a50ed3714bdd9d972aa2bccb8fcb--2fa85fbeeff6446aafd54071079457f3 d1566275453643d0b64ab5ace2ad4d6c X 2fa85fbeeff6446aafd54071079457f3--d1566275453643d0b64ab5ace2ad4d6c d1566275453643d0b64ab5ace2ad4d6c--044fb52ea1824fde9e7a905acc4d4482 b229b85d266b4919a6990d2958123938 X d1566275453643d0b64ab5ace2ad4d6c--b229b85d266b4919a6990d2958123938 b229b85d266b4919a6990d2958123938--482979a008bb435b8d89e839d5123f8c e63f1771488448589def8789460d5b49 RZ(g0) b229b85d266b4919a6990d2958123938--e63f1771488448589def8789460d5b49 5bfb714032bc4e5db2f7aaca0ffe7045 X e63f1771488448589def8789460d5b49--5bfb714032bc4e5db2f7aaca0ffe7045 5bfb714032bc4e5db2f7aaca0ffe7045--6f88cb1e2f414e1e85907ce13104fe75 adbaffa58ea648e18aa7e69d66c056d7 RX(b0) 5bfb714032bc4e5db2f7aaca0ffe7045--adbaffa58ea648e18aa7e69d66c056d7 adbaffa58ea648e18aa7e69d66c056d7--c8311f8acc84471b8eeea0eaf24d10e9

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 2025-02-14T12:51:41.331971 image/svg+xml Matplotlib v3.10.0, https://matplotlib.org/

References


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