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-12-05T14:32:22.104469 image/svg+xml Matplotlib v3.9.3, 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_4477ef2ab94249c6b1a41950a3524ffc mixing cluster_59e0f1cc203c4c76834d561cfbc76e85 cost 71c507543e3d475da10d1a303ab72c2f 0 98ba19ed52654755beb0310eeb3cc213 H 71c507543e3d475da10d1a303ab72c2f--98ba19ed52654755beb0310eeb3cc213 a94eeaa82fb94e2d87bbfd0ec8bceb14 1 ad636b67b3194e30800c72d9764e0a31 98ba19ed52654755beb0310eeb3cc213--ad636b67b3194e30800c72d9764e0a31 200bedd13ec34a43816a6fe064884a5f ad636b67b3194e30800c72d9764e0a31--200bedd13ec34a43816a6fe064884a5f 4b69111b68ad4e30b1f2b89827b4b2b7 200bedd13ec34a43816a6fe064884a5f--4b69111b68ad4e30b1f2b89827b4b2b7 662868f1553a495699ac1b14d7a0d528 4b69111b68ad4e30b1f2b89827b4b2b7--662868f1553a495699ac1b14d7a0d528 7081620bd25f40b5969cb45374a9e68a 662868f1553a495699ac1b14d7a0d528--7081620bd25f40b5969cb45374a9e68a 6a8eaad4528f47a28884de0b800adf61 7081620bd25f40b5969cb45374a9e68a--6a8eaad4528f47a28884de0b800adf61 2bf538458c58456abb6b25d907674d53 6a8eaad4528f47a28884de0b800adf61--2bf538458c58456abb6b25d907674d53 a86b9bba1f5646e7abe36847f8a46bc6 2bf538458c58456abb6b25d907674d53--a86b9bba1f5646e7abe36847f8a46bc6 e6a68babfc354d3e8d9319fa89c2b8ff a86b9bba1f5646e7abe36847f8a46bc6--e6a68babfc354d3e8d9319fa89c2b8ff c6607f13945b4e97b1a461f4dd19ca46 e6a68babfc354d3e8d9319fa89c2b8ff--c6607f13945b4e97b1a461f4dd19ca46 c7e11ad9987b4f84b0399f8221d0e50a c6607f13945b4e97b1a461f4dd19ca46--c7e11ad9987b4f84b0399f8221d0e50a 8335afa86e124db1ad30bc169a31c127 c7e11ad9987b4f84b0399f8221d0e50a--8335afa86e124db1ad30bc169a31c127 8d827089e9464472a79c8c3c96e12c40 8335afa86e124db1ad30bc169a31c127--8d827089e9464472a79c8c3c96e12c40 ab26b19ed95642faa13c1d63f1c266c4 8d827089e9464472a79c8c3c96e12c40--ab26b19ed95642faa13c1d63f1c266c4 7855580b115f412ea29a91542ba64f7c ab26b19ed95642faa13c1d63f1c266c4--7855580b115f412ea29a91542ba64f7c 41b66ef471c04337a52977d25ad948e7 7855580b115f412ea29a91542ba64f7c--41b66ef471c04337a52977d25ad948e7 904b528679ed47339d9c662c7d923140 41b66ef471c04337a52977d25ad948e7--904b528679ed47339d9c662c7d923140 097d6bbfd57f46e28fae31debff9e7e3 904b528679ed47339d9c662c7d923140--097d6bbfd57f46e28fae31debff9e7e3 42c7b44320d6446d874360ccc8f0539a RX(b0) 097d6bbfd57f46e28fae31debff9e7e3--42c7b44320d6446d874360ccc8f0539a 0c1d600025354f6aa28eadf7e1f8e89a 42c7b44320d6446d874360ccc8f0539a--0c1d600025354f6aa28eadf7e1f8e89a 2ee93cb3432f40c693c2ce45aa678968 ee1e1761fdca472f8e1256799147b33d H a94eeaa82fb94e2d87bbfd0ec8bceb14--ee1e1761fdca472f8e1256799147b33d 4ca4abde60be4972b2ccf186c24973cb 2 3ea38e4ac21d48629638eb7adc08739d X ee1e1761fdca472f8e1256799147b33d--3ea38e4ac21d48629638eb7adc08739d 3ea38e4ac21d48629638eb7adc08739d--ad636b67b3194e30800c72d9764e0a31 923cfe7ddde945769f83f9e9e589a56c RZ(g0) 3ea38e4ac21d48629638eb7adc08739d--923cfe7ddde945769f83f9e9e589a56c 0071e9ed0a08407ba1250ed076fac682 X 923cfe7ddde945769f83f9e9e589a56c--0071e9ed0a08407ba1250ed076fac682 0071e9ed0a08407ba1250ed076fac682--4b69111b68ad4e30b1f2b89827b4b2b7 88ae4791353543faa92a02475d0ea873 0071e9ed0a08407ba1250ed076fac682--88ae4791353543faa92a02475d0ea873 a390d5339cb1497284493f79082fccb5 88ae4791353543faa92a02475d0ea873--a390d5339cb1497284493f79082fccb5 bd6d6f7695ca48df87ceca0d27aab128 a390d5339cb1497284493f79082fccb5--bd6d6f7695ca48df87ceca0d27aab128 182b861dbcc640f8bccbefc8ab610134 bd6d6f7695ca48df87ceca0d27aab128--182b861dbcc640f8bccbefc8ab610134 2aaf3d84134449fbbccd29f74df5b820 182b861dbcc640f8bccbefc8ab610134--2aaf3d84134449fbbccd29f74df5b820 64f2bf6cf3f347fd8072e1f083eb870f 2aaf3d84134449fbbccd29f74df5b820--64f2bf6cf3f347fd8072e1f083eb870f 12c03cc6a03345f0a06b97d4a60523ba 64f2bf6cf3f347fd8072e1f083eb870f--12c03cc6a03345f0a06b97d4a60523ba 78c24287ac3e4b6bbe8517bba5fa315a 12c03cc6a03345f0a06b97d4a60523ba--78c24287ac3e4b6bbe8517bba5fa315a 61f5151599754edd893752e3e4e72983 78c24287ac3e4b6bbe8517bba5fa315a--61f5151599754edd893752e3e4e72983 cc2fd8b0c8504ee5b5731917fbf7e2ae 61f5151599754edd893752e3e4e72983--cc2fd8b0c8504ee5b5731917fbf7e2ae 01a6b058ee04425fa887ef7469bc6b1d cc2fd8b0c8504ee5b5731917fbf7e2ae--01a6b058ee04425fa887ef7469bc6b1d 5c1d432059f14e138620357a252ea4d6 01a6b058ee04425fa887ef7469bc6b1d--5c1d432059f14e138620357a252ea4d6 5a81fbdc088645a5ab516996330173e1 5c1d432059f14e138620357a252ea4d6--5a81fbdc088645a5ab516996330173e1 31a6ac2d244245ca9aaa05e14132f529 5a81fbdc088645a5ab516996330173e1--31a6ac2d244245ca9aaa05e14132f529 d6012046fb9343008a50e33404c92880 31a6ac2d244245ca9aaa05e14132f529--d6012046fb9343008a50e33404c92880 0cc477225d3e4954a655ba69788d8251 RX(b0) d6012046fb9343008a50e33404c92880--0cc477225d3e4954a655ba69788d8251 0cc477225d3e4954a655ba69788d8251--2ee93cb3432f40c693c2ce45aa678968 cc1b9b1777654edea42125aa4b21e855 06a25eecef6f4b65bda485fdd7d323d2 H 4ca4abde60be4972b2ccf186c24973cb--06a25eecef6f4b65bda485fdd7d323d2 8acf3ad068134b298adc5448b4c0261b 3 26458160725f4fa6b1c355dc8ce99ac5 06a25eecef6f4b65bda485fdd7d323d2--26458160725f4fa6b1c355dc8ce99ac5 cc2c81a994e6487d94482eea17d4ae6c 26458160725f4fa6b1c355dc8ce99ac5--cc2c81a994e6487d94482eea17d4ae6c c91ab6aadece4a7498ea87769e6ca65b cc2c81a994e6487d94482eea17d4ae6c--c91ab6aadece4a7498ea87769e6ca65b 6f43e59aaaf945f89c644438c7373ec3 X c91ab6aadece4a7498ea87769e6ca65b--6f43e59aaaf945f89c644438c7373ec3 6f43e59aaaf945f89c644438c7373ec3--662868f1553a495699ac1b14d7a0d528 973d2fdcd6954a69b2b79770208b5b06 RZ(g0) 6f43e59aaaf945f89c644438c7373ec3--973d2fdcd6954a69b2b79770208b5b06 1b4a4fe00ba248e98223299817a65a9c X 973d2fdcd6954a69b2b79770208b5b06--1b4a4fe00ba248e98223299817a65a9c 1b4a4fe00ba248e98223299817a65a9c--6a8eaad4528f47a28884de0b800adf61 585d88c0e37848c184cc73308ce5bbd2 1b4a4fe00ba248e98223299817a65a9c--585d88c0e37848c184cc73308ce5bbd2 8694f59ac3e1441984cfbab25d6a43f3 585d88c0e37848c184cc73308ce5bbd2--8694f59ac3e1441984cfbab25d6a43f3 4867690b94bd41bdb5c55025644663e5 8694f59ac3e1441984cfbab25d6a43f3--4867690b94bd41bdb5c55025644663e5 21310d3f08974c32acabee58f690a80e X 4867690b94bd41bdb5c55025644663e5--21310d3f08974c32acabee58f690a80e 21310d3f08974c32acabee58f690a80e--12c03cc6a03345f0a06b97d4a60523ba 4c5de938705648d1b7549ae5f52c3dd0 RZ(g0) 21310d3f08974c32acabee58f690a80e--4c5de938705648d1b7549ae5f52c3dd0 6937f07eaa764b3083dfa9d9d015555b X 4c5de938705648d1b7549ae5f52c3dd0--6937f07eaa764b3083dfa9d9d015555b 6937f07eaa764b3083dfa9d9d015555b--61f5151599754edd893752e3e4e72983 686b1e0c00ee4de08d862e286f58ed27 6937f07eaa764b3083dfa9d9d015555b--686b1e0c00ee4de08d862e286f58ed27 e5d202a7767045ef888653ee70469299 686b1e0c00ee4de08d862e286f58ed27--e5d202a7767045ef888653ee70469299 28c21281caf543da8272860871bbd84d e5d202a7767045ef888653ee70469299--28c21281caf543da8272860871bbd84d 575b3c24ea4247c3b44854d4abd239cd 28c21281caf543da8272860871bbd84d--575b3c24ea4247c3b44854d4abd239cd c920e1fbecdf4471893c63397188c2e0 575b3c24ea4247c3b44854d4abd239cd--c920e1fbecdf4471893c63397188c2e0 d75fed1998ef419fa04b4b472fde925b c920e1fbecdf4471893c63397188c2e0--d75fed1998ef419fa04b4b472fde925b 31f7f88a6197452280eac98d1af337a7 RX(b0) d75fed1998ef419fa04b4b472fde925b--31f7f88a6197452280eac98d1af337a7 31f7f88a6197452280eac98d1af337a7--cc1b9b1777654edea42125aa4b21e855 fb98055e34f1423c9f64ec3dea172553 4d3a8eaf3a5844d0b6fc59ca37d0e05c H 8acf3ad068134b298adc5448b4c0261b--4d3a8eaf3a5844d0b6fc59ca37d0e05c c967328994ed42f8aebd8fe06d4e47b9 4d3a8eaf3a5844d0b6fc59ca37d0e05c--c967328994ed42f8aebd8fe06d4e47b9 32b99b575dca4828bb7cc0971fa9d108 c967328994ed42f8aebd8fe06d4e47b9--32b99b575dca4828bb7cc0971fa9d108 aa61fac26853483c9a3f2db13a8ca527 32b99b575dca4828bb7cc0971fa9d108--aa61fac26853483c9a3f2db13a8ca527 6329647f7ded4ccdadf47027713dea08 aa61fac26853483c9a3f2db13a8ca527--6329647f7ded4ccdadf47027713dea08 e553ca96e9fe43f8a537bd89567edc60 6329647f7ded4ccdadf47027713dea08--e553ca96e9fe43f8a537bd89567edc60 b25149e045244f708450c82be2a8bd29 e553ca96e9fe43f8a537bd89567edc60--b25149e045244f708450c82be2a8bd29 c578830d53554c0c80d504348cea7004 X b25149e045244f708450c82be2a8bd29--c578830d53554c0c80d504348cea7004 c578830d53554c0c80d504348cea7004--2bf538458c58456abb6b25d907674d53 76a6cd53d0764d668db2681ccd49bc23 RZ(g0) c578830d53554c0c80d504348cea7004--76a6cd53d0764d668db2681ccd49bc23 5ee3fea7e7324d6fbfc70e8061dea9bc X 76a6cd53d0764d668db2681ccd49bc23--5ee3fea7e7324d6fbfc70e8061dea9bc 5ee3fea7e7324d6fbfc70e8061dea9bc--e6a68babfc354d3e8d9319fa89c2b8ff 64371e4a17eb48c8995f9acdbd1daebf 5ee3fea7e7324d6fbfc70e8061dea9bc--64371e4a17eb48c8995f9acdbd1daebf 39d4cdff3b77476e9e5e9a1bfe93fce3 64371e4a17eb48c8995f9acdbd1daebf--39d4cdff3b77476e9e5e9a1bfe93fce3 b85abfcecec04591a4411fc13ef99171 39d4cdff3b77476e9e5e9a1bfe93fce3--b85abfcecec04591a4411fc13ef99171 9e2510df0e53491d88e6555abf2bb0f8 X b85abfcecec04591a4411fc13ef99171--9e2510df0e53491d88e6555abf2bb0f8 9e2510df0e53491d88e6555abf2bb0f8--cc2fd8b0c8504ee5b5731917fbf7e2ae 2f9fe9caeebf474999d3420e80df38fe RZ(g0) 9e2510df0e53491d88e6555abf2bb0f8--2f9fe9caeebf474999d3420e80df38fe 56291c6e3f934194aa68e802ca86688c X 2f9fe9caeebf474999d3420e80df38fe--56291c6e3f934194aa68e802ca86688c 56291c6e3f934194aa68e802ca86688c--5c1d432059f14e138620357a252ea4d6 e52829ff928144cb9a5c9ba034dcf42d X 56291c6e3f934194aa68e802ca86688c--e52829ff928144cb9a5c9ba034dcf42d e52829ff928144cb9a5c9ba034dcf42d--575b3c24ea4247c3b44854d4abd239cd 05de1dc935784db487af07373bdbc24e RZ(g0) e52829ff928144cb9a5c9ba034dcf42d--05de1dc935784db487af07373bdbc24e 4398553943e24d99ba5c6a5c956f08bd X 05de1dc935784db487af07373bdbc24e--4398553943e24d99ba5c6a5c956f08bd 4398553943e24d99ba5c6a5c956f08bd--d75fed1998ef419fa04b4b472fde925b acfec628a03846639de3df2d35f2b672 RX(b0) 4398553943e24d99ba5c6a5c956f08bd--acfec628a03846639de3df2d35f2b672 acfec628a03846639de3df2d35f2b672--fb98055e34f1423c9f64ec3dea172553

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-12-05T14:32:26.679793 image/svg+xml Matplotlib v3.9.3, https://matplotlib.org/

References


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