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-07-10T14:30:22.821380 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_4ea9b5178a31435aa7424fc17f67ed21 mixing cluster_4b274a91201e40e1b5b1426249b54b2c cost c50ee5674aff400fb35ec5aebd482ef3 0 c356147f89b34a52a811917dfcc164b4 H c50ee5674aff400fb35ec5aebd482ef3--c356147f89b34a52a811917dfcc164b4 7276e0be2f1b4cf1acacd980fd22db99 1 774191e80964479e81f821249821127d c356147f89b34a52a811917dfcc164b4--774191e80964479e81f821249821127d 40258c775aa94528bd64bc1576c857b9 774191e80964479e81f821249821127d--40258c775aa94528bd64bc1576c857b9 22669a3f7ed343e6bf76858e2cbde9c6 40258c775aa94528bd64bc1576c857b9--22669a3f7ed343e6bf76858e2cbde9c6 884f6fafe25241ba82ac713f73301e89 22669a3f7ed343e6bf76858e2cbde9c6--884f6fafe25241ba82ac713f73301e89 5a763aa11feb483aa64cdd68787a6a19 884f6fafe25241ba82ac713f73301e89--5a763aa11feb483aa64cdd68787a6a19 cf86b6629b6a4180bd19946e4e112648 5a763aa11feb483aa64cdd68787a6a19--cf86b6629b6a4180bd19946e4e112648 082e2a4762434139983289b00e34c1c9 cf86b6629b6a4180bd19946e4e112648--082e2a4762434139983289b00e34c1c9 3611bd874e914706bc3341c69780ae6e 082e2a4762434139983289b00e34c1c9--3611bd874e914706bc3341c69780ae6e 063b8a33112541479da0715d28567650 3611bd874e914706bc3341c69780ae6e--063b8a33112541479da0715d28567650 aa4e7ef34a4e4686a36d870c2605789d 063b8a33112541479da0715d28567650--aa4e7ef34a4e4686a36d870c2605789d fda97db2cdef4f76a20838251db023a6 aa4e7ef34a4e4686a36d870c2605789d--fda97db2cdef4f76a20838251db023a6 bc069dc6932848c5b8bb12b0074da061 fda97db2cdef4f76a20838251db023a6--bc069dc6932848c5b8bb12b0074da061 2a3090b6b1544437bbc3452290864a04 bc069dc6932848c5b8bb12b0074da061--2a3090b6b1544437bbc3452290864a04 22cc29a49b194485b79ded462b3d6d3f 2a3090b6b1544437bbc3452290864a04--22cc29a49b194485b79ded462b3d6d3f 4096cc8c877c4e8f9c354dfcfc576fce 22cc29a49b194485b79ded462b3d6d3f--4096cc8c877c4e8f9c354dfcfc576fce 5520918f10354abc992bee1c6437edc4 4096cc8c877c4e8f9c354dfcfc576fce--5520918f10354abc992bee1c6437edc4 d5a6af02bc8d4118b9cc25fe8529728b 5520918f10354abc992bee1c6437edc4--d5a6af02bc8d4118b9cc25fe8529728b 6d4f7c36d18d4a6791645b7fd6164ce7 d5a6af02bc8d4118b9cc25fe8529728b--6d4f7c36d18d4a6791645b7fd6164ce7 a9c9e47245874881a151ca7cdbccef41 RX(b0) 6d4f7c36d18d4a6791645b7fd6164ce7--a9c9e47245874881a151ca7cdbccef41 a6d6e1f0226b49a79e1c08d285ebf4c7 a9c9e47245874881a151ca7cdbccef41--a6d6e1f0226b49a79e1c08d285ebf4c7 dfdf3146309f4afab6f0b79e35ddef78 489d91af660c4550993223cea73fde08 H 7276e0be2f1b4cf1acacd980fd22db99--489d91af660c4550993223cea73fde08 84f5d0361f6b4c91846f5bda4d206f87 2 7cf4d3b302e143e88c93a9a85a0c0c45 X 489d91af660c4550993223cea73fde08--7cf4d3b302e143e88c93a9a85a0c0c45 7cf4d3b302e143e88c93a9a85a0c0c45--774191e80964479e81f821249821127d cd1f4e196a604c7fa33aadc015b43446 RZ(g0) 7cf4d3b302e143e88c93a9a85a0c0c45--cd1f4e196a604c7fa33aadc015b43446 21603d5fc3ed4ed9983c3e9ae07fe391 X cd1f4e196a604c7fa33aadc015b43446--21603d5fc3ed4ed9983c3e9ae07fe391 21603d5fc3ed4ed9983c3e9ae07fe391--22669a3f7ed343e6bf76858e2cbde9c6 a30f4ffada2341beaabd408b519cce90 21603d5fc3ed4ed9983c3e9ae07fe391--a30f4ffada2341beaabd408b519cce90 f654ba4d76e1455781a6586517de9bad a30f4ffada2341beaabd408b519cce90--f654ba4d76e1455781a6586517de9bad 3a739305212f404ca8a84fc434266f12 f654ba4d76e1455781a6586517de9bad--3a739305212f404ca8a84fc434266f12 ef3a9d22fc9f4fba90b48dc99fd9fbd6 3a739305212f404ca8a84fc434266f12--ef3a9d22fc9f4fba90b48dc99fd9fbd6 e3e375c3f0b04712ad89e2c5a22b02fc ef3a9d22fc9f4fba90b48dc99fd9fbd6--e3e375c3f0b04712ad89e2c5a22b02fc 3882a74ab61c41c48fc9f2540324e755 e3e375c3f0b04712ad89e2c5a22b02fc--3882a74ab61c41c48fc9f2540324e755 dd680b39959343cd8de01415f778e337 3882a74ab61c41c48fc9f2540324e755--dd680b39959343cd8de01415f778e337 df9f53df482948ab9166e3a73c44ee0c dd680b39959343cd8de01415f778e337--df9f53df482948ab9166e3a73c44ee0c f2a393dd304f4091874de94c7bae36fb df9f53df482948ab9166e3a73c44ee0c--f2a393dd304f4091874de94c7bae36fb b822479fdbe942fcb498592400c37e72 f2a393dd304f4091874de94c7bae36fb--b822479fdbe942fcb498592400c37e72 27741fad6e61488a9525f28add353fe2 b822479fdbe942fcb498592400c37e72--27741fad6e61488a9525f28add353fe2 187b835a3a2b4bd2b41c4ff518a4ccf9 27741fad6e61488a9525f28add353fe2--187b835a3a2b4bd2b41c4ff518a4ccf9 2577ad0797a54648834ddfdeb89dba3a 187b835a3a2b4bd2b41c4ff518a4ccf9--2577ad0797a54648834ddfdeb89dba3a a415859f26fc4e8fa4aca4722f053446 2577ad0797a54648834ddfdeb89dba3a--a415859f26fc4e8fa4aca4722f053446 7fdee9f7d8f04a6f812fba5327caf970 a415859f26fc4e8fa4aca4722f053446--7fdee9f7d8f04a6f812fba5327caf970 6d3a3b941a184589a44491c6105d653d RX(b0) 7fdee9f7d8f04a6f812fba5327caf970--6d3a3b941a184589a44491c6105d653d 6d3a3b941a184589a44491c6105d653d--dfdf3146309f4afab6f0b79e35ddef78 7754ca7954da400fbde9f0ee095e67b4 066a04ba8c904c2482e739fbf47d5f72 H 84f5d0361f6b4c91846f5bda4d206f87--066a04ba8c904c2482e739fbf47d5f72 caa3caba0fab4ed68882751a2b868841 3 4c647be8da774c88a075548afe61a10e 066a04ba8c904c2482e739fbf47d5f72--4c647be8da774c88a075548afe61a10e cf0ab2c75a244f148c422b1f46d5a522 4c647be8da774c88a075548afe61a10e--cf0ab2c75a244f148c422b1f46d5a522 ecec24be1784442c982e2ac2accdaaf0 cf0ab2c75a244f148c422b1f46d5a522--ecec24be1784442c982e2ac2accdaaf0 732db420c7bb4ad29503d69a25a139f0 X ecec24be1784442c982e2ac2accdaaf0--732db420c7bb4ad29503d69a25a139f0 732db420c7bb4ad29503d69a25a139f0--884f6fafe25241ba82ac713f73301e89 2de5c81af23b470a9844967589b35715 RZ(g0) 732db420c7bb4ad29503d69a25a139f0--2de5c81af23b470a9844967589b35715 de612ddb7a9e4c54aded32be0ba755e7 X 2de5c81af23b470a9844967589b35715--de612ddb7a9e4c54aded32be0ba755e7 de612ddb7a9e4c54aded32be0ba755e7--cf86b6629b6a4180bd19946e4e112648 a5a5ebff8bce4d3690849be339760b19 de612ddb7a9e4c54aded32be0ba755e7--a5a5ebff8bce4d3690849be339760b19 4dbf289de23e4568b6a51eec266c6412 a5a5ebff8bce4d3690849be339760b19--4dbf289de23e4568b6a51eec266c6412 574a9b80e90f4891a21dcdd35a9fbcda 4dbf289de23e4568b6a51eec266c6412--574a9b80e90f4891a21dcdd35a9fbcda 403d6bb3476a447ca9d507396adf6fd1 X 574a9b80e90f4891a21dcdd35a9fbcda--403d6bb3476a447ca9d507396adf6fd1 403d6bb3476a447ca9d507396adf6fd1--dd680b39959343cd8de01415f778e337 9fee9844bb2f490ca78111ef2a8d751f RZ(g0) 403d6bb3476a447ca9d507396adf6fd1--9fee9844bb2f490ca78111ef2a8d751f 2168d5e95b174bce811a4fdbab93c02c X 9fee9844bb2f490ca78111ef2a8d751f--2168d5e95b174bce811a4fdbab93c02c 2168d5e95b174bce811a4fdbab93c02c--f2a393dd304f4091874de94c7bae36fb 8d3fceaf61b34d63b71aacf1b5971963 2168d5e95b174bce811a4fdbab93c02c--8d3fceaf61b34d63b71aacf1b5971963 d51128fae8ca4791a23d51655579c5f5 8d3fceaf61b34d63b71aacf1b5971963--d51128fae8ca4791a23d51655579c5f5 9165dd7a5a1e438faf86696242409010 d51128fae8ca4791a23d51655579c5f5--9165dd7a5a1e438faf86696242409010 f1edd4a8268144989659b31b77c0362c 9165dd7a5a1e438faf86696242409010--f1edd4a8268144989659b31b77c0362c 1789e98a0b0f4e1c945baa11cd041abb f1edd4a8268144989659b31b77c0362c--1789e98a0b0f4e1c945baa11cd041abb 0c1f41e6f6854888b931be8ff26762b9 1789e98a0b0f4e1c945baa11cd041abb--0c1f41e6f6854888b931be8ff26762b9 59167eef87264d09812009af5be2037e RX(b0) 0c1f41e6f6854888b931be8ff26762b9--59167eef87264d09812009af5be2037e 59167eef87264d09812009af5be2037e--7754ca7954da400fbde9f0ee095e67b4 290e23312f03492eb67b13389094095c 6a217509c5e44420bd553020fc0ae9f1 H caa3caba0fab4ed68882751a2b868841--6a217509c5e44420bd553020fc0ae9f1 22d76431c30e45c983885aad56ee61b8 6a217509c5e44420bd553020fc0ae9f1--22d76431c30e45c983885aad56ee61b8 70cb370146ee4eea9c17f639e1b7a8cb 22d76431c30e45c983885aad56ee61b8--70cb370146ee4eea9c17f639e1b7a8cb 00d94416513449e8b7b6fae165413329 70cb370146ee4eea9c17f639e1b7a8cb--00d94416513449e8b7b6fae165413329 98e9a9e16bb24db8a90706d319e572cb 00d94416513449e8b7b6fae165413329--98e9a9e16bb24db8a90706d319e572cb 4d4a10d222ec425a8060d50665a0a246 98e9a9e16bb24db8a90706d319e572cb--4d4a10d222ec425a8060d50665a0a246 ead9e26c122542468a4967240ca0c557 4d4a10d222ec425a8060d50665a0a246--ead9e26c122542468a4967240ca0c557 8bf22cfc0f0e492ba60d66d22b8390a5 X ead9e26c122542468a4967240ca0c557--8bf22cfc0f0e492ba60d66d22b8390a5 8bf22cfc0f0e492ba60d66d22b8390a5--082e2a4762434139983289b00e34c1c9 5033066154024ee0b80de43b1df015d5 RZ(g0) 8bf22cfc0f0e492ba60d66d22b8390a5--5033066154024ee0b80de43b1df015d5 8e6ae38a41c145feb8c91f22814b326a X 5033066154024ee0b80de43b1df015d5--8e6ae38a41c145feb8c91f22814b326a 8e6ae38a41c145feb8c91f22814b326a--063b8a33112541479da0715d28567650 34fd986c4a4f46448c42efa310888ab6 8e6ae38a41c145feb8c91f22814b326a--34fd986c4a4f46448c42efa310888ab6 6939a4d282ce41718d8402a62dccd534 34fd986c4a4f46448c42efa310888ab6--6939a4d282ce41718d8402a62dccd534 846ae8877a894e0398f04a155f0b6951 6939a4d282ce41718d8402a62dccd534--846ae8877a894e0398f04a155f0b6951 60654b3818e442a590555000a777d09f X 846ae8877a894e0398f04a155f0b6951--60654b3818e442a590555000a777d09f 60654b3818e442a590555000a777d09f--b822479fdbe942fcb498592400c37e72 b2ee6acda0c9436ba3770011c00d83e4 RZ(g0) 60654b3818e442a590555000a777d09f--b2ee6acda0c9436ba3770011c00d83e4 e6b2c743248449faa80aff08d28fb26b X b2ee6acda0c9436ba3770011c00d83e4--e6b2c743248449faa80aff08d28fb26b e6b2c743248449faa80aff08d28fb26b--187b835a3a2b4bd2b41c4ff518a4ccf9 bf347782931248b38ec1430a029ee8e9 X e6b2c743248449faa80aff08d28fb26b--bf347782931248b38ec1430a029ee8e9 bf347782931248b38ec1430a029ee8e9--f1edd4a8268144989659b31b77c0362c 28f4632fbd8d4cd094405bbaf2df0dca RZ(g0) bf347782931248b38ec1430a029ee8e9--28f4632fbd8d4cd094405bbaf2df0dca 4041823fec8847c7af2442e97a328866 X 28f4632fbd8d4cd094405bbaf2df0dca--4041823fec8847c7af2442e97a328866 4041823fec8847c7af2442e97a328866--0c1f41e6f6854888b931be8ff26762b9 f7c09dac9c4144799525d019eabbf9b1 RX(b0) 4041823fec8847c7af2442e97a328866--f7c09dac9c4144799525d019eabbf9b1 f7c09dac9c4144799525d019eabbf9b1--290e23312f03492eb67b13389094095c

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-07-10T14:30:27.177140 image/svg+xml Matplotlib v3.7.5, https://matplotlib.org/

References


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