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-05-03T08:23:55.604501 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_066d2dbbfa3049a389ca6cfe523e6841 mixing cluster_f38669f2f8354e26bfbace287dff8e99 cost 3b184f879bc043a4bc16421ab53fffe1 0 0ca54da98f6a47698d04a9f7f6bf773c H 3b184f879bc043a4bc16421ab53fffe1--0ca54da98f6a47698d04a9f7f6bf773c 3050fb0ad1b64c27a6281eecc0786f32 1 4a9db82aea8e4dbfa1025889ede615ed 0ca54da98f6a47698d04a9f7f6bf773c--4a9db82aea8e4dbfa1025889ede615ed f1f72bd242364a2681ebc174e92c5a37 4a9db82aea8e4dbfa1025889ede615ed--f1f72bd242364a2681ebc174e92c5a37 4685247249d44339899ab813f4b6679a f1f72bd242364a2681ebc174e92c5a37--4685247249d44339899ab813f4b6679a 70cf2299ee894944af9eb860f03fae36 4685247249d44339899ab813f4b6679a--70cf2299ee894944af9eb860f03fae36 57c3c498e4684f6ab010799b71c81967 70cf2299ee894944af9eb860f03fae36--57c3c498e4684f6ab010799b71c81967 0f015359fcf84ed9ad840c13c1fbc095 57c3c498e4684f6ab010799b71c81967--0f015359fcf84ed9ad840c13c1fbc095 35c38232f0fb4f39a3a782318f1390fd 0f015359fcf84ed9ad840c13c1fbc095--35c38232f0fb4f39a3a782318f1390fd 7e271cc8b3b0442d8b3ac5e917549e75 35c38232f0fb4f39a3a782318f1390fd--7e271cc8b3b0442d8b3ac5e917549e75 0bcf6b573967482ba082a591b7548de9 7e271cc8b3b0442d8b3ac5e917549e75--0bcf6b573967482ba082a591b7548de9 8d92d009f0d24fb4bdb80e8e667bbb83 0bcf6b573967482ba082a591b7548de9--8d92d009f0d24fb4bdb80e8e667bbb83 7046e66a4d0f4728b90dad6df27ee9a8 8d92d009f0d24fb4bdb80e8e667bbb83--7046e66a4d0f4728b90dad6df27ee9a8 3684b40e74944f22a4cf12e9ae184e01 7046e66a4d0f4728b90dad6df27ee9a8--3684b40e74944f22a4cf12e9ae184e01 c788c6c3a4bd47489da8d45f33065cf3 3684b40e74944f22a4cf12e9ae184e01--c788c6c3a4bd47489da8d45f33065cf3 2a3ca44e7d8a46079deb113c3b4ae7ec c788c6c3a4bd47489da8d45f33065cf3--2a3ca44e7d8a46079deb113c3b4ae7ec b37120e073b74d99936d0ae0e228c915 2a3ca44e7d8a46079deb113c3b4ae7ec--b37120e073b74d99936d0ae0e228c915 80ee7c75e66c43599d9209e6e7874e00 b37120e073b74d99936d0ae0e228c915--80ee7c75e66c43599d9209e6e7874e00 20e0ce0c3e72431f8fbfd3db2161b9ad 80ee7c75e66c43599d9209e6e7874e00--20e0ce0c3e72431f8fbfd3db2161b9ad 5bf2e4c99ed04cdd8c586b6ce817ceb6 20e0ce0c3e72431f8fbfd3db2161b9ad--5bf2e4c99ed04cdd8c586b6ce817ceb6 cb25a42c16e74a609cdbefe8dd38d6b8 RX(b0) 5bf2e4c99ed04cdd8c586b6ce817ceb6--cb25a42c16e74a609cdbefe8dd38d6b8 cafff2c9046f42efbec6f3d9755a19d8 cb25a42c16e74a609cdbefe8dd38d6b8--cafff2c9046f42efbec6f3d9755a19d8 3e80111ce45c45db9c5d5018a288c377 9c6fcfa54d324a89a10598ed56f64013 H 3050fb0ad1b64c27a6281eecc0786f32--9c6fcfa54d324a89a10598ed56f64013 7e160b7965544915809cf115c17c44a4 2 1b4c014975174e208243175803102004 X 9c6fcfa54d324a89a10598ed56f64013--1b4c014975174e208243175803102004 1b4c014975174e208243175803102004--4a9db82aea8e4dbfa1025889ede615ed f8068fa9b3794ad88e06c3d709f15f5d RZ(g0) 1b4c014975174e208243175803102004--f8068fa9b3794ad88e06c3d709f15f5d 7cf0b7da34cc4726a9b48043c7e5777d X f8068fa9b3794ad88e06c3d709f15f5d--7cf0b7da34cc4726a9b48043c7e5777d 7cf0b7da34cc4726a9b48043c7e5777d--4685247249d44339899ab813f4b6679a d2893f6c6c854ff39972f8a49954424c 7cf0b7da34cc4726a9b48043c7e5777d--d2893f6c6c854ff39972f8a49954424c 0eb372b50b824abba9444d9a36d41186 d2893f6c6c854ff39972f8a49954424c--0eb372b50b824abba9444d9a36d41186 7920cd4272894bab8ff0263b238c4c86 0eb372b50b824abba9444d9a36d41186--7920cd4272894bab8ff0263b238c4c86 92f16739b2ea4ec9948d3d3b18f05469 7920cd4272894bab8ff0263b238c4c86--92f16739b2ea4ec9948d3d3b18f05469 dcb269569af54a848ab2c2da1138d2e6 92f16739b2ea4ec9948d3d3b18f05469--dcb269569af54a848ab2c2da1138d2e6 8d33add1470844e488cb2260222d29ff dcb269569af54a848ab2c2da1138d2e6--8d33add1470844e488cb2260222d29ff c96fb9a8a8df470f92e8f3f72297fac0 8d33add1470844e488cb2260222d29ff--c96fb9a8a8df470f92e8f3f72297fac0 0c5aa9ea1e434a33b0b89ea482d96393 c96fb9a8a8df470f92e8f3f72297fac0--0c5aa9ea1e434a33b0b89ea482d96393 5283652e4bf74e04b81380b359f42f54 0c5aa9ea1e434a33b0b89ea482d96393--5283652e4bf74e04b81380b359f42f54 ad8c41a22bce42deb653c47aba21df40 5283652e4bf74e04b81380b359f42f54--ad8c41a22bce42deb653c47aba21df40 63b0eb849b9f469a94d2b1934bfaa2b9 ad8c41a22bce42deb653c47aba21df40--63b0eb849b9f469a94d2b1934bfaa2b9 3c211287391b40cfb46bd5ebb8a3a326 63b0eb849b9f469a94d2b1934bfaa2b9--3c211287391b40cfb46bd5ebb8a3a326 96067a9a6ab04eb5a7f42f119f61f1ef 3c211287391b40cfb46bd5ebb8a3a326--96067a9a6ab04eb5a7f42f119f61f1ef 70c88b29556a489ba6fc977b864531cf 96067a9a6ab04eb5a7f42f119f61f1ef--70c88b29556a489ba6fc977b864531cf 033841d7cecb49008af9feb1cc30ef8e 70c88b29556a489ba6fc977b864531cf--033841d7cecb49008af9feb1cc30ef8e dee9785a3402439595777f1c95409251 RX(b0) 033841d7cecb49008af9feb1cc30ef8e--dee9785a3402439595777f1c95409251 dee9785a3402439595777f1c95409251--3e80111ce45c45db9c5d5018a288c377 43b81825820e4a19876a5e3d1c2609d3 c951b57cda1145e0a4d9fc4d639f2a45 H 7e160b7965544915809cf115c17c44a4--c951b57cda1145e0a4d9fc4d639f2a45 97bd0558b69c45c3b112a4e49b3c5cba 3 f9b79980b4c7401089d1469375dcda8e c951b57cda1145e0a4d9fc4d639f2a45--f9b79980b4c7401089d1469375dcda8e 216f8d80c49f40adbdb76db7ef941541 f9b79980b4c7401089d1469375dcda8e--216f8d80c49f40adbdb76db7ef941541 bfe711b4f7924183ad8d0d47edadc4bc 216f8d80c49f40adbdb76db7ef941541--bfe711b4f7924183ad8d0d47edadc4bc db3b43285d0d49579752596068e5ad87 X bfe711b4f7924183ad8d0d47edadc4bc--db3b43285d0d49579752596068e5ad87 db3b43285d0d49579752596068e5ad87--70cf2299ee894944af9eb860f03fae36 553dc78ae6934fdd832ce5ba029a5f7e RZ(g0) db3b43285d0d49579752596068e5ad87--553dc78ae6934fdd832ce5ba029a5f7e 820bb61cfafc4dfca500dbb5179dbbb3 X 553dc78ae6934fdd832ce5ba029a5f7e--820bb61cfafc4dfca500dbb5179dbbb3 820bb61cfafc4dfca500dbb5179dbbb3--0f015359fcf84ed9ad840c13c1fbc095 8efa475e0085497f968b220920b216fa 820bb61cfafc4dfca500dbb5179dbbb3--8efa475e0085497f968b220920b216fa 82382778e53d4fc2ac749cfbff19df7f 8efa475e0085497f968b220920b216fa--82382778e53d4fc2ac749cfbff19df7f 5606f2eb61c3420b9609b29dafc91d4c 82382778e53d4fc2ac749cfbff19df7f--5606f2eb61c3420b9609b29dafc91d4c 73a2edfb9d314c39b472b05608963ec3 X 5606f2eb61c3420b9609b29dafc91d4c--73a2edfb9d314c39b472b05608963ec3 73a2edfb9d314c39b472b05608963ec3--c96fb9a8a8df470f92e8f3f72297fac0 965e79ca90904b1796d8b0bcd9daaff1 RZ(g0) 73a2edfb9d314c39b472b05608963ec3--965e79ca90904b1796d8b0bcd9daaff1 2b227f16db44409dbc22427d5ce2df73 X 965e79ca90904b1796d8b0bcd9daaff1--2b227f16db44409dbc22427d5ce2df73 2b227f16db44409dbc22427d5ce2df73--5283652e4bf74e04b81380b359f42f54 2123667951114d5bb633b5ab3036c04e 2b227f16db44409dbc22427d5ce2df73--2123667951114d5bb633b5ab3036c04e b43d1a89348f4a4184cbf466d66f85da 2123667951114d5bb633b5ab3036c04e--b43d1a89348f4a4184cbf466d66f85da 20a33835afa14118b2ef43a20bd4a360 b43d1a89348f4a4184cbf466d66f85da--20a33835afa14118b2ef43a20bd4a360 10a51a19c9184440bb4591edcfaad13f 20a33835afa14118b2ef43a20bd4a360--10a51a19c9184440bb4591edcfaad13f 67a49f00662749f9b462b35d4d8ecd80 10a51a19c9184440bb4591edcfaad13f--67a49f00662749f9b462b35d4d8ecd80 55a0ba0bb2364e6b8dcd62bcfb9db30c 67a49f00662749f9b462b35d4d8ecd80--55a0ba0bb2364e6b8dcd62bcfb9db30c b45f9a8b4e304616a8aba95cbea778ea RX(b0) 55a0ba0bb2364e6b8dcd62bcfb9db30c--b45f9a8b4e304616a8aba95cbea778ea b45f9a8b4e304616a8aba95cbea778ea--43b81825820e4a19876a5e3d1c2609d3 a7597383bc8245f7be842d403964c0d8 2ba2bc02bad44079aa50c0098fbe2ac4 H 97bd0558b69c45c3b112a4e49b3c5cba--2ba2bc02bad44079aa50c0098fbe2ac4 69ab0c8d094b48eb8c891cb5e4790ea6 2ba2bc02bad44079aa50c0098fbe2ac4--69ab0c8d094b48eb8c891cb5e4790ea6 00735593ad3e464d8a41b7705268649d 69ab0c8d094b48eb8c891cb5e4790ea6--00735593ad3e464d8a41b7705268649d 5e1397ff1135490cba5b7248d4b09329 00735593ad3e464d8a41b7705268649d--5e1397ff1135490cba5b7248d4b09329 9e3ef1524da24dcd84153cc1334f7363 5e1397ff1135490cba5b7248d4b09329--9e3ef1524da24dcd84153cc1334f7363 92f7334729884f3281fa04f319cfe2d8 9e3ef1524da24dcd84153cc1334f7363--92f7334729884f3281fa04f319cfe2d8 92ea54381d0e4f46b41aaa13ae89a2d4 92f7334729884f3281fa04f319cfe2d8--92ea54381d0e4f46b41aaa13ae89a2d4 ce3b5c36334a464da37480e48640079f X 92ea54381d0e4f46b41aaa13ae89a2d4--ce3b5c36334a464da37480e48640079f ce3b5c36334a464da37480e48640079f--35c38232f0fb4f39a3a782318f1390fd a738777899f04944abbd2b02b4237cbd RZ(g0) ce3b5c36334a464da37480e48640079f--a738777899f04944abbd2b02b4237cbd 7cf54a6152564540a201320f86053722 X a738777899f04944abbd2b02b4237cbd--7cf54a6152564540a201320f86053722 7cf54a6152564540a201320f86053722--0bcf6b573967482ba082a591b7548de9 ac5b49aba7ea4ebcab6b9ed456455e81 7cf54a6152564540a201320f86053722--ac5b49aba7ea4ebcab6b9ed456455e81 41ad20198ea14bb0822f439b7969b1d0 ac5b49aba7ea4ebcab6b9ed456455e81--41ad20198ea14bb0822f439b7969b1d0 139782d3c0634014b7bd0499b66dce9e 41ad20198ea14bb0822f439b7969b1d0--139782d3c0634014b7bd0499b66dce9e b46767ef7ddf4aeca05c120ed4166d2a X 139782d3c0634014b7bd0499b66dce9e--b46767ef7ddf4aeca05c120ed4166d2a b46767ef7ddf4aeca05c120ed4166d2a--ad8c41a22bce42deb653c47aba21df40 0d2eeb67328a4fcd954732ccae9bef11 RZ(g0) b46767ef7ddf4aeca05c120ed4166d2a--0d2eeb67328a4fcd954732ccae9bef11 fb9ed21277724054ba4d5c52b773116f X 0d2eeb67328a4fcd954732ccae9bef11--fb9ed21277724054ba4d5c52b773116f fb9ed21277724054ba4d5c52b773116f--3c211287391b40cfb46bd5ebb8a3a326 6494660b548945bebb88753a7cc6be28 X fb9ed21277724054ba4d5c52b773116f--6494660b548945bebb88753a7cc6be28 6494660b548945bebb88753a7cc6be28--10a51a19c9184440bb4591edcfaad13f 54e349cbfabf469d852a4afe3a21861b RZ(g0) 6494660b548945bebb88753a7cc6be28--54e349cbfabf469d852a4afe3a21861b d3658092808d44b0a68e9b47f5ac31cb X 54e349cbfabf469d852a4afe3a21861b--d3658092808d44b0a68e9b47f5ac31cb d3658092808d44b0a68e9b47f5ac31cb--55a0ba0bb2364e6b8dcd62bcfb9db30c e03bc68e558c4f04a02b3074231a8854 RX(b0) d3658092808d44b0a68e9b47f5ac31cb--e03bc68e558c4f04a02b3074231a8854 e03bc68e558c4f04a02b3074231a8854--a7597383bc8245f7be842d403964c0d8

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-05-03T08:23:59.914619 image/svg+xml Matplotlib v3.7.5, https://matplotlib.org/

References


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