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-06-16T15:43:46.989823 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_7c257b7f9bb14f4980d68003133633f3 mixing cluster_189aeb75067d452ba0bd56059a1814c7 cost 72ebd70492d0426084b3b2fe46e9cf4a 0 5e8de9c86b6b4c2a958cdbf35525bbc6 H 72ebd70492d0426084b3b2fe46e9cf4a--5e8de9c86b6b4c2a958cdbf35525bbc6 eedbef7b9cdd47cd8b217d617fa31609 1 1770a173a9524ae1afe25384d94b7d42 5e8de9c86b6b4c2a958cdbf35525bbc6--1770a173a9524ae1afe25384d94b7d42 b8fa11d98f7f4a3d8adb1ec110d6ca98 1770a173a9524ae1afe25384d94b7d42--b8fa11d98f7f4a3d8adb1ec110d6ca98 94cd114166bc4764806954d2dc5e80cc b8fa11d98f7f4a3d8adb1ec110d6ca98--94cd114166bc4764806954d2dc5e80cc 511350309f9e4d73aa877d5e85a0327d 94cd114166bc4764806954d2dc5e80cc--511350309f9e4d73aa877d5e85a0327d 402da6b048bb4c42a690bb7879a5791a 511350309f9e4d73aa877d5e85a0327d--402da6b048bb4c42a690bb7879a5791a f2d9bff552e14a98b4db091850ca928b 402da6b048bb4c42a690bb7879a5791a--f2d9bff552e14a98b4db091850ca928b 8f1ca15e2fee4d1fba70d8d43ab3a335 f2d9bff552e14a98b4db091850ca928b--8f1ca15e2fee4d1fba70d8d43ab3a335 1cb7b164e8c7413b87a686155175fdfc 8f1ca15e2fee4d1fba70d8d43ab3a335--1cb7b164e8c7413b87a686155175fdfc f6d4d41464e940e798d013644fdb0709 1cb7b164e8c7413b87a686155175fdfc--f6d4d41464e940e798d013644fdb0709 09bb73e3ead9481982d786b90097fe65 f6d4d41464e940e798d013644fdb0709--09bb73e3ead9481982d786b90097fe65 029d828c7ab4455792ac879658914746 09bb73e3ead9481982d786b90097fe65--029d828c7ab4455792ac879658914746 00527bc0606348d0bdc64971b3bed35a 029d828c7ab4455792ac879658914746--00527bc0606348d0bdc64971b3bed35a 8825157e2f76403fafae750487f36bcd 00527bc0606348d0bdc64971b3bed35a--8825157e2f76403fafae750487f36bcd 786c1bae086644c7aa81d56be3b40127 8825157e2f76403fafae750487f36bcd--786c1bae086644c7aa81d56be3b40127 f802a7b5c4b54b0796daf5945dda779f 786c1bae086644c7aa81d56be3b40127--f802a7b5c4b54b0796daf5945dda779f dbf2f96fe9124725b66dd1853257c090 f802a7b5c4b54b0796daf5945dda779f--dbf2f96fe9124725b66dd1853257c090 81108e9b1e914b98a138d5a5ba7bd378 dbf2f96fe9124725b66dd1853257c090--81108e9b1e914b98a138d5a5ba7bd378 c7f63ac2d42048678d6c4c11d798f716 81108e9b1e914b98a138d5a5ba7bd378--c7f63ac2d42048678d6c4c11d798f716 f36d246f952c4b9297f6bc1477b23e19 RX(b0) c7f63ac2d42048678d6c4c11d798f716--f36d246f952c4b9297f6bc1477b23e19 dc56b64b39724d088d8fc5ae142449f2 f36d246f952c4b9297f6bc1477b23e19--dc56b64b39724d088d8fc5ae142449f2 91fc958db0cb448c9ca4287893dfc173 190a5df0562b41b18ca251f7289d586c H eedbef7b9cdd47cd8b217d617fa31609--190a5df0562b41b18ca251f7289d586c 5756ea5cad3f4befbfee70079312b07d 2 a5fed71589db4491a7707fd8c82feea4 X 190a5df0562b41b18ca251f7289d586c--a5fed71589db4491a7707fd8c82feea4 a5fed71589db4491a7707fd8c82feea4--1770a173a9524ae1afe25384d94b7d42 3a2ea4b38d52405f8b5b3d399619e097 RZ(g0) a5fed71589db4491a7707fd8c82feea4--3a2ea4b38d52405f8b5b3d399619e097 82b1848ac539415d815d0ebd42f579c4 X 3a2ea4b38d52405f8b5b3d399619e097--82b1848ac539415d815d0ebd42f579c4 82b1848ac539415d815d0ebd42f579c4--94cd114166bc4764806954d2dc5e80cc 9dbfd944a2a143e5890eb38d2e8d95ed 82b1848ac539415d815d0ebd42f579c4--9dbfd944a2a143e5890eb38d2e8d95ed 1bdf8e69f2434e8383a35fca6472e4c6 9dbfd944a2a143e5890eb38d2e8d95ed--1bdf8e69f2434e8383a35fca6472e4c6 c156454312dc42f9873abe367e85921c 1bdf8e69f2434e8383a35fca6472e4c6--c156454312dc42f9873abe367e85921c ab7ce90ef45c4274a10a499e75e04722 c156454312dc42f9873abe367e85921c--ab7ce90ef45c4274a10a499e75e04722 e1fc87c031b64cd48609465cbadebf10 ab7ce90ef45c4274a10a499e75e04722--e1fc87c031b64cd48609465cbadebf10 a65836b7eedb4549b5ceb2edc7c4dce3 e1fc87c031b64cd48609465cbadebf10--a65836b7eedb4549b5ceb2edc7c4dce3 2076f5150be54591acceb6136671bc06 a65836b7eedb4549b5ceb2edc7c4dce3--2076f5150be54591acceb6136671bc06 10997a905ee14c5ba1054bff1f1bb014 2076f5150be54591acceb6136671bc06--10997a905ee14c5ba1054bff1f1bb014 13499b41511c461bbb84fa164d82fcda 10997a905ee14c5ba1054bff1f1bb014--13499b41511c461bbb84fa164d82fcda 1bc81bbef74a4cd6a627db113c9ceb48 13499b41511c461bbb84fa164d82fcda--1bc81bbef74a4cd6a627db113c9ceb48 91d05d768fa44470878e14f492fb3771 1bc81bbef74a4cd6a627db113c9ceb48--91d05d768fa44470878e14f492fb3771 4a55afe903c4489d85a3415b75802d7c 91d05d768fa44470878e14f492fb3771--4a55afe903c4489d85a3415b75802d7c ebda3be49559448e8323e27d246aa93d 4a55afe903c4489d85a3415b75802d7c--ebda3be49559448e8323e27d246aa93d 209c51a4026b4fe4b0aa5b57114f29ff ebda3be49559448e8323e27d246aa93d--209c51a4026b4fe4b0aa5b57114f29ff 2c3b66aa48fa497cba341b82e99f82df 209c51a4026b4fe4b0aa5b57114f29ff--2c3b66aa48fa497cba341b82e99f82df 9dd7e31f53e340ba87f775c6415db135 RX(b0) 2c3b66aa48fa497cba341b82e99f82df--9dd7e31f53e340ba87f775c6415db135 9dd7e31f53e340ba87f775c6415db135--91fc958db0cb448c9ca4287893dfc173 b32a1c1041524c85ada8612c05f9ca82 a5d302619d914347a13a5190fc4268d5 H 5756ea5cad3f4befbfee70079312b07d--a5d302619d914347a13a5190fc4268d5 8463b10cbd9648c080e69a85e470afe5 3 4125743ad9fc4b429a6ba0ec35877aef a5d302619d914347a13a5190fc4268d5--4125743ad9fc4b429a6ba0ec35877aef 6b71d29dfc5a4d2eba1b923a0e6cf7c6 4125743ad9fc4b429a6ba0ec35877aef--6b71d29dfc5a4d2eba1b923a0e6cf7c6 95796b6b2ccd47cdb0d0c0aa24f8e531 6b71d29dfc5a4d2eba1b923a0e6cf7c6--95796b6b2ccd47cdb0d0c0aa24f8e531 ecf3047c297f454bb3d2e1fefcd0c0b3 X 95796b6b2ccd47cdb0d0c0aa24f8e531--ecf3047c297f454bb3d2e1fefcd0c0b3 ecf3047c297f454bb3d2e1fefcd0c0b3--511350309f9e4d73aa877d5e85a0327d 665902870adb4bf2bd547e6fb71418b9 RZ(g0) ecf3047c297f454bb3d2e1fefcd0c0b3--665902870adb4bf2bd547e6fb71418b9 93c9742f1b3f4e118cf117accad63098 X 665902870adb4bf2bd547e6fb71418b9--93c9742f1b3f4e118cf117accad63098 93c9742f1b3f4e118cf117accad63098--f2d9bff552e14a98b4db091850ca928b 1ad09aec73674c218e477f251a8a8fa1 93c9742f1b3f4e118cf117accad63098--1ad09aec73674c218e477f251a8a8fa1 3c1d6a0552fa4e61be55983fd67dcd1c 1ad09aec73674c218e477f251a8a8fa1--3c1d6a0552fa4e61be55983fd67dcd1c 208e0fbbd2d24d4e8c2386f194242f69 3c1d6a0552fa4e61be55983fd67dcd1c--208e0fbbd2d24d4e8c2386f194242f69 499a65e5114649a5a61897fd8640d5c8 X 208e0fbbd2d24d4e8c2386f194242f69--499a65e5114649a5a61897fd8640d5c8 499a65e5114649a5a61897fd8640d5c8--2076f5150be54591acceb6136671bc06 794ec70cf57742078b64f83f05a8a3af RZ(g0) 499a65e5114649a5a61897fd8640d5c8--794ec70cf57742078b64f83f05a8a3af c346973323fe408990511aad0751c611 X 794ec70cf57742078b64f83f05a8a3af--c346973323fe408990511aad0751c611 c346973323fe408990511aad0751c611--13499b41511c461bbb84fa164d82fcda 8f1fec17172442209598a9e0c5c4f629 c346973323fe408990511aad0751c611--8f1fec17172442209598a9e0c5c4f629 30bc17afe0504ab1a43f89636f89757c 8f1fec17172442209598a9e0c5c4f629--30bc17afe0504ab1a43f89636f89757c a1eff5fce6a54f95a0350bdc666b1632 30bc17afe0504ab1a43f89636f89757c--a1eff5fce6a54f95a0350bdc666b1632 ca77eccf0655462f92cfe073fd26e4c4 a1eff5fce6a54f95a0350bdc666b1632--ca77eccf0655462f92cfe073fd26e4c4 0cc450f5fb2d405ba2d2c765b7bf6899 ca77eccf0655462f92cfe073fd26e4c4--0cc450f5fb2d405ba2d2c765b7bf6899 433fd5a212424336b0c9b83202e88563 0cc450f5fb2d405ba2d2c765b7bf6899--433fd5a212424336b0c9b83202e88563 bab5f58488a1424ab1616c663e532e01 RX(b0) 433fd5a212424336b0c9b83202e88563--bab5f58488a1424ab1616c663e532e01 bab5f58488a1424ab1616c663e532e01--b32a1c1041524c85ada8612c05f9ca82 5189a910ed3a411bbb8baa9f5e7b910e d32721797e084e1884160900fee25065 H 8463b10cbd9648c080e69a85e470afe5--d32721797e084e1884160900fee25065 b5682ad9421c4b74bacb9bd3bfea5ab0 d32721797e084e1884160900fee25065--b5682ad9421c4b74bacb9bd3bfea5ab0 339babbad19c484fa785a12e45dcf16d b5682ad9421c4b74bacb9bd3bfea5ab0--339babbad19c484fa785a12e45dcf16d 92304f04ac1546e3b66e36b9910c24c7 339babbad19c484fa785a12e45dcf16d--92304f04ac1546e3b66e36b9910c24c7 2333a9033906402fa7c24ea73132466c 92304f04ac1546e3b66e36b9910c24c7--2333a9033906402fa7c24ea73132466c 5a4dbf16c26b466fb5ea9ca7d6f06385 2333a9033906402fa7c24ea73132466c--5a4dbf16c26b466fb5ea9ca7d6f06385 ad2084605e8444e08884b1f4221025fb 5a4dbf16c26b466fb5ea9ca7d6f06385--ad2084605e8444e08884b1f4221025fb a488d3735e9940b981efea0359e8e17f X ad2084605e8444e08884b1f4221025fb--a488d3735e9940b981efea0359e8e17f a488d3735e9940b981efea0359e8e17f--8f1ca15e2fee4d1fba70d8d43ab3a335 4278238e8baf4f8e847351e824846955 RZ(g0) a488d3735e9940b981efea0359e8e17f--4278238e8baf4f8e847351e824846955 f7558fead134425d81e16493db4b6dd5 X 4278238e8baf4f8e847351e824846955--f7558fead134425d81e16493db4b6dd5 f7558fead134425d81e16493db4b6dd5--f6d4d41464e940e798d013644fdb0709 2c8b4425235440f0be603a372d209540 f7558fead134425d81e16493db4b6dd5--2c8b4425235440f0be603a372d209540 8907948f5079416d89f8d5fd8f3a036c 2c8b4425235440f0be603a372d209540--8907948f5079416d89f8d5fd8f3a036c e39de3cc1da84120bd9b29915d6d967b 8907948f5079416d89f8d5fd8f3a036c--e39de3cc1da84120bd9b29915d6d967b fd7840b7e0a34aad9e95b5af25f130db X e39de3cc1da84120bd9b29915d6d967b--fd7840b7e0a34aad9e95b5af25f130db fd7840b7e0a34aad9e95b5af25f130db--1bc81bbef74a4cd6a627db113c9ceb48 1d3867f53e264943bfd1e56647970f06 RZ(g0) fd7840b7e0a34aad9e95b5af25f130db--1d3867f53e264943bfd1e56647970f06 53ec409cacee4530852459cbc66dc980 X 1d3867f53e264943bfd1e56647970f06--53ec409cacee4530852459cbc66dc980 53ec409cacee4530852459cbc66dc980--4a55afe903c4489d85a3415b75802d7c 59e6fda58d054f1e9ea2c067cd43c019 X 53ec409cacee4530852459cbc66dc980--59e6fda58d054f1e9ea2c067cd43c019 59e6fda58d054f1e9ea2c067cd43c019--ca77eccf0655462f92cfe073fd26e4c4 003df0657f5d44ed8f2f061f6d4f9f3c RZ(g0) 59e6fda58d054f1e9ea2c067cd43c019--003df0657f5d44ed8f2f061f6d4f9f3c df22043b71b849c79e636eb9012f765f X 003df0657f5d44ed8f2f061f6d4f9f3c--df22043b71b849c79e636eb9012f765f df22043b71b849c79e636eb9012f765f--433fd5a212424336b0c9b83202e88563 d78cb15edf804e2ba312491c8fb0bdf2 RX(b0) df22043b71b849c79e636eb9012f765f--d78cb15edf804e2ba312491c8fb0bdf2 d78cb15edf804e2ba312491c8fb0bdf2--5189a910ed3a411bbb8baa9f5e7b910e

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.7470706807026373
MaxCut cost at iteration 20: 3.8378810288930207
MaxCut cost at iteration 30: 3.9424197899236146
MaxCut cost at iteration 40: 3.998125625576603
MaxCut cost at iteration 50: 3.9964705285082114
MaxCut cost at iteration 60: 3.9991374608876598
MaxCut cost at iteration 70: 3.999467854291969
MaxCut cost at iteration 80: 3.999872558672824
MaxCut cost at iteration 90: 3.999947583412106
MaxCut cost at iteration 100: 3.9999793311641065

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-06-16T15:43:51.117350 image/svg+xml Matplotlib v3.7.5, https://matplotlib.org/

References


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