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-01-23T14:19:42.219553 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_2d93c98cbef547cf815b2e8c0961e308 mixing cluster_e8a2be8223164328b6253adb7719077d cost 1c44e3759e3440d3a32c4ce909edba68 0 c9cb3c2b810f422cafabfb040cf9bb31 H 1c44e3759e3440d3a32c4ce909edba68--c9cb3c2b810f422cafabfb040cf9bb31 ca2693f4938145f19605a08be372d8e9 1 ed43f1bea1814528931f9321fa160c80 c9cb3c2b810f422cafabfb040cf9bb31--ed43f1bea1814528931f9321fa160c80 8cc359395c27416987580b06881e89df ed43f1bea1814528931f9321fa160c80--8cc359395c27416987580b06881e89df c5c042b14f6c4e5f8647c5b9452ba913 8cc359395c27416987580b06881e89df--c5c042b14f6c4e5f8647c5b9452ba913 8cceb73aa28b45f098b08826c21a8eb8 c5c042b14f6c4e5f8647c5b9452ba913--8cceb73aa28b45f098b08826c21a8eb8 a4ec7fd480f34f73afad207a7d170fff 8cceb73aa28b45f098b08826c21a8eb8--a4ec7fd480f34f73afad207a7d170fff 91cbe510ee2d459fbbb9435bed9c9bc0 a4ec7fd480f34f73afad207a7d170fff--91cbe510ee2d459fbbb9435bed9c9bc0 86694bba4bbd473cafa5052ab7cad6df 91cbe510ee2d459fbbb9435bed9c9bc0--86694bba4bbd473cafa5052ab7cad6df 1db18a71402346df8ea8709d096883aa 86694bba4bbd473cafa5052ab7cad6df--1db18a71402346df8ea8709d096883aa 56ec5bb023df46fa8ca119faf67b2969 1db18a71402346df8ea8709d096883aa--56ec5bb023df46fa8ca119faf67b2969 c4ab0794dd084ab28eefb740e9605e08 56ec5bb023df46fa8ca119faf67b2969--c4ab0794dd084ab28eefb740e9605e08 00bd08d966d748a29745ee63b215a73c c4ab0794dd084ab28eefb740e9605e08--00bd08d966d748a29745ee63b215a73c fcbd21cab87f4cc387e50b68a9f8c383 00bd08d966d748a29745ee63b215a73c--fcbd21cab87f4cc387e50b68a9f8c383 ce73ffaeb0f14bcd8d5ae6459bbc9ecc fcbd21cab87f4cc387e50b68a9f8c383--ce73ffaeb0f14bcd8d5ae6459bbc9ecc 869f168029e4490590f4df1a1d2828d4 ce73ffaeb0f14bcd8d5ae6459bbc9ecc--869f168029e4490590f4df1a1d2828d4 a68efd2f2ea34d758c04eab70577c4f1 869f168029e4490590f4df1a1d2828d4--a68efd2f2ea34d758c04eab70577c4f1 c3b795db4bdb44d497f69c4600934434 a68efd2f2ea34d758c04eab70577c4f1--c3b795db4bdb44d497f69c4600934434 6745d01bf74f4c28afa50d51ceace728 c3b795db4bdb44d497f69c4600934434--6745d01bf74f4c28afa50d51ceace728 d82d666265b54234821de26179fb46f7 6745d01bf74f4c28afa50d51ceace728--d82d666265b54234821de26179fb46f7 1fa1a151cded4c19a4801976fc477890 RX(b0) d82d666265b54234821de26179fb46f7--1fa1a151cded4c19a4801976fc477890 266611d191ee4726962b3488b86bc4fd 1fa1a151cded4c19a4801976fc477890--266611d191ee4726962b3488b86bc4fd 0fd4fd785c9d4e9fa84f2e35a038ce51 d54800435d25400aa97d782f488b5ebf H ca2693f4938145f19605a08be372d8e9--d54800435d25400aa97d782f488b5ebf 9c1cd3cf9dc14918ae0e3d1c4a7723ac 2 90383ab3ca954d7894ecfe56e2f876d6 X d54800435d25400aa97d782f488b5ebf--90383ab3ca954d7894ecfe56e2f876d6 90383ab3ca954d7894ecfe56e2f876d6--ed43f1bea1814528931f9321fa160c80 0da889983c024ef4997c6279f8afb999 RZ(g0) 90383ab3ca954d7894ecfe56e2f876d6--0da889983c024ef4997c6279f8afb999 f8d6e159783e468484640c928f2deece X 0da889983c024ef4997c6279f8afb999--f8d6e159783e468484640c928f2deece f8d6e159783e468484640c928f2deece--c5c042b14f6c4e5f8647c5b9452ba913 082da5036e1b49cf821eea4160434f4e f8d6e159783e468484640c928f2deece--082da5036e1b49cf821eea4160434f4e b51d7f4fc89841f6b3f45c57aade6011 082da5036e1b49cf821eea4160434f4e--b51d7f4fc89841f6b3f45c57aade6011 86b8add4aeb54e5b948c03e2d199386f b51d7f4fc89841f6b3f45c57aade6011--86b8add4aeb54e5b948c03e2d199386f fe175574465a4303850675db537a8420 86b8add4aeb54e5b948c03e2d199386f--fe175574465a4303850675db537a8420 ecda296c4e2c4300a450090623eeb605 fe175574465a4303850675db537a8420--ecda296c4e2c4300a450090623eeb605 3ad3122a7ffe4d1592c75efc70e65b17 ecda296c4e2c4300a450090623eeb605--3ad3122a7ffe4d1592c75efc70e65b17 3905c4773a0b4f18a45e0091d52101b8 3ad3122a7ffe4d1592c75efc70e65b17--3905c4773a0b4f18a45e0091d52101b8 da94405a30634384892680baaa3e36f0 3905c4773a0b4f18a45e0091d52101b8--da94405a30634384892680baaa3e36f0 df09e9ae69ac406b9186ee2921f1700f da94405a30634384892680baaa3e36f0--df09e9ae69ac406b9186ee2921f1700f 257d0e6087e04fad8dc11a04a862d18d df09e9ae69ac406b9186ee2921f1700f--257d0e6087e04fad8dc11a04a862d18d b64a0a167ab04394b2922102975556ea 257d0e6087e04fad8dc11a04a862d18d--b64a0a167ab04394b2922102975556ea d03a80e65ab149599ae8255ca3b09b15 b64a0a167ab04394b2922102975556ea--d03a80e65ab149599ae8255ca3b09b15 c3cd083ef2fe496fa6f5de5b5948641c d03a80e65ab149599ae8255ca3b09b15--c3cd083ef2fe496fa6f5de5b5948641c a77a551eddc04da88684f93cd654f843 c3cd083ef2fe496fa6f5de5b5948641c--a77a551eddc04da88684f93cd654f843 87e5aa883bb9470e8ccbfe7fb7755ab6 a77a551eddc04da88684f93cd654f843--87e5aa883bb9470e8ccbfe7fb7755ab6 58246cca25584e54ab6a002edc22b96a RX(b0) 87e5aa883bb9470e8ccbfe7fb7755ab6--58246cca25584e54ab6a002edc22b96a 58246cca25584e54ab6a002edc22b96a--0fd4fd785c9d4e9fa84f2e35a038ce51 91c21dc291c14e15817147fd168c1642 c2286063eed4458a89a041a466a20bc9 H 9c1cd3cf9dc14918ae0e3d1c4a7723ac--c2286063eed4458a89a041a466a20bc9 63af7725ffee4cbfb434b701f9f1b341 3 1894bb8b6989474fbc778adb401e8505 c2286063eed4458a89a041a466a20bc9--1894bb8b6989474fbc778adb401e8505 3e8a7f35fd804af5a273083740d0bb72 1894bb8b6989474fbc778adb401e8505--3e8a7f35fd804af5a273083740d0bb72 420b2afd1107474eb43bf0d65fae34e8 3e8a7f35fd804af5a273083740d0bb72--420b2afd1107474eb43bf0d65fae34e8 0f576e4dd03d4bb2b17a32630a4dd00b X 420b2afd1107474eb43bf0d65fae34e8--0f576e4dd03d4bb2b17a32630a4dd00b 0f576e4dd03d4bb2b17a32630a4dd00b--8cceb73aa28b45f098b08826c21a8eb8 024ff04a9cdb43bba6064860e608bdfd RZ(g0) 0f576e4dd03d4bb2b17a32630a4dd00b--024ff04a9cdb43bba6064860e608bdfd 973b00b04ae7475cbea3701326eb3093 X 024ff04a9cdb43bba6064860e608bdfd--973b00b04ae7475cbea3701326eb3093 973b00b04ae7475cbea3701326eb3093--91cbe510ee2d459fbbb9435bed9c9bc0 eba1f01ae72b4bc2889691ab7104c824 973b00b04ae7475cbea3701326eb3093--eba1f01ae72b4bc2889691ab7104c824 9856283f360a4659b9cd862a10dd3a11 eba1f01ae72b4bc2889691ab7104c824--9856283f360a4659b9cd862a10dd3a11 809a5c631c53408e8a97d24616e01cb3 9856283f360a4659b9cd862a10dd3a11--809a5c631c53408e8a97d24616e01cb3 40922b0ba30746d2b9a1b3f6bcc3a213 X 809a5c631c53408e8a97d24616e01cb3--40922b0ba30746d2b9a1b3f6bcc3a213 40922b0ba30746d2b9a1b3f6bcc3a213--3905c4773a0b4f18a45e0091d52101b8 553896ab5a954f2bac29c1f005b48ae5 RZ(g0) 40922b0ba30746d2b9a1b3f6bcc3a213--553896ab5a954f2bac29c1f005b48ae5 c017f53429504ec4a06ec7dbf7c8e547 X 553896ab5a954f2bac29c1f005b48ae5--c017f53429504ec4a06ec7dbf7c8e547 c017f53429504ec4a06ec7dbf7c8e547--df09e9ae69ac406b9186ee2921f1700f 78cfd12dd0ce408096791bd25e90be02 c017f53429504ec4a06ec7dbf7c8e547--78cfd12dd0ce408096791bd25e90be02 7425e805c916440faa3b8a107fc4947b 78cfd12dd0ce408096791bd25e90be02--7425e805c916440faa3b8a107fc4947b d88a32e003d74c6e9f6064da094ee544 7425e805c916440faa3b8a107fc4947b--d88a32e003d74c6e9f6064da094ee544 a5c8d74848d541d1a2cbdfe88cb90cb5 d88a32e003d74c6e9f6064da094ee544--a5c8d74848d541d1a2cbdfe88cb90cb5 fd5af36c72b24b9186822c0794dfd4ca a5c8d74848d541d1a2cbdfe88cb90cb5--fd5af36c72b24b9186822c0794dfd4ca 077c86442a004602b08b2bcdaa459aa4 fd5af36c72b24b9186822c0794dfd4ca--077c86442a004602b08b2bcdaa459aa4 ae7101fea2e14e3196db6c453de119e1 RX(b0) 077c86442a004602b08b2bcdaa459aa4--ae7101fea2e14e3196db6c453de119e1 ae7101fea2e14e3196db6c453de119e1--91c21dc291c14e15817147fd168c1642 a30610468357419199d7ff9c6ef7c5e1 e724928b3fa14ca19ec8967421ead21e H 63af7725ffee4cbfb434b701f9f1b341--e724928b3fa14ca19ec8967421ead21e 5d2684cf69854ed7885f0ef7da221f8d e724928b3fa14ca19ec8967421ead21e--5d2684cf69854ed7885f0ef7da221f8d b8f0334d4fc64d83956e94a8baa9c04b 5d2684cf69854ed7885f0ef7da221f8d--b8f0334d4fc64d83956e94a8baa9c04b 2441b9f0491d481595811e86c1393e83 b8f0334d4fc64d83956e94a8baa9c04b--2441b9f0491d481595811e86c1393e83 aa84389f32d944eea36c0d8466089f76 2441b9f0491d481595811e86c1393e83--aa84389f32d944eea36c0d8466089f76 5b3f32b19ae54210ae29821505ace34d aa84389f32d944eea36c0d8466089f76--5b3f32b19ae54210ae29821505ace34d 03fa1d3f54b4461b877d92b5c825f807 5b3f32b19ae54210ae29821505ace34d--03fa1d3f54b4461b877d92b5c825f807 2a154d52bc68482f84b312dcb238cf6e X 03fa1d3f54b4461b877d92b5c825f807--2a154d52bc68482f84b312dcb238cf6e 2a154d52bc68482f84b312dcb238cf6e--86694bba4bbd473cafa5052ab7cad6df 87844781b157410f922f56a2177fe0f4 RZ(g0) 2a154d52bc68482f84b312dcb238cf6e--87844781b157410f922f56a2177fe0f4 9d0a17a67b70447b8bab5addf79dd376 X 87844781b157410f922f56a2177fe0f4--9d0a17a67b70447b8bab5addf79dd376 9d0a17a67b70447b8bab5addf79dd376--56ec5bb023df46fa8ca119faf67b2969 96e71e267b9041b3b905342def20f40e 9d0a17a67b70447b8bab5addf79dd376--96e71e267b9041b3b905342def20f40e 3869984b2998487caa57e10f953c6523 96e71e267b9041b3b905342def20f40e--3869984b2998487caa57e10f953c6523 ea53060c45d44b0ead0f8636be9c3e6d 3869984b2998487caa57e10f953c6523--ea53060c45d44b0ead0f8636be9c3e6d 85731467baa2446e8214fe78e3bec109 X ea53060c45d44b0ead0f8636be9c3e6d--85731467baa2446e8214fe78e3bec109 85731467baa2446e8214fe78e3bec109--257d0e6087e04fad8dc11a04a862d18d 2d03adffe8af49ccbd3dbcc584253372 RZ(g0) 85731467baa2446e8214fe78e3bec109--2d03adffe8af49ccbd3dbcc584253372 38e1a6ced212473d8a1066adfa1e9172 X 2d03adffe8af49ccbd3dbcc584253372--38e1a6ced212473d8a1066adfa1e9172 38e1a6ced212473d8a1066adfa1e9172--d03a80e65ab149599ae8255ca3b09b15 cc613f975a704c46981859639d9941bf X 38e1a6ced212473d8a1066adfa1e9172--cc613f975a704c46981859639d9941bf cc613f975a704c46981859639d9941bf--a5c8d74848d541d1a2cbdfe88cb90cb5 3e923a30a0514cc2835e3e949d0647fe RZ(g0) cc613f975a704c46981859639d9941bf--3e923a30a0514cc2835e3e949d0647fe daf1a97b03bb42b1b2badf5e4be4de20 X 3e923a30a0514cc2835e3e949d0647fe--daf1a97b03bb42b1b2badf5e4be4de20 daf1a97b03bb42b1b2badf5e4be4de20--077c86442a004602b08b2bcdaa459aa4 ae164cae38bf4f61b5df2981823a8c63 RX(b0) daf1a97b03bb42b1b2badf5e4be4de20--ae164cae38bf4f61b5df2981823a8c63 ae164cae38bf4f61b5df2981823a8c63--a30610468357419199d7ff9c6ef7c5e1

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-01-23T14:19:45.916271 image/svg+xml Matplotlib v3.10.0, https://matplotlib.org/

References


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