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-26T12:34:21.214496 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_fae599b345ef4d54938ec4a5be8993f3 mixing cluster_5f2046d9440243ed94440a3ebc7fc554 cost 95bb6bbde76f45118ab266f0f028f844 0 13665dd202cb41d6bae4523159e9dffd H 95bb6bbde76f45118ab266f0f028f844--13665dd202cb41d6bae4523159e9dffd df985dc277d141dabeac7766b0eebbb5 1 f9857700938d4452abcd462ed3518c5f 13665dd202cb41d6bae4523159e9dffd--f9857700938d4452abcd462ed3518c5f 0028351ea8fa45049c8dc4bbfaee2a37 f9857700938d4452abcd462ed3518c5f--0028351ea8fa45049c8dc4bbfaee2a37 ef8605c0c13143d288c783542b54505d 0028351ea8fa45049c8dc4bbfaee2a37--ef8605c0c13143d288c783542b54505d 2b403437c3154da4ab83f3602a4c3087 ef8605c0c13143d288c783542b54505d--2b403437c3154da4ab83f3602a4c3087 3ad6fe7e5d3541a78db0d3818e4361f7 2b403437c3154da4ab83f3602a4c3087--3ad6fe7e5d3541a78db0d3818e4361f7 fba18e27189a41aa83fd57a5457d372d 3ad6fe7e5d3541a78db0d3818e4361f7--fba18e27189a41aa83fd57a5457d372d a3b60e0e6b9d45649a6a2ec351378985 fba18e27189a41aa83fd57a5457d372d--a3b60e0e6b9d45649a6a2ec351378985 87221a3335ad4f728bd7facf77b46e05 a3b60e0e6b9d45649a6a2ec351378985--87221a3335ad4f728bd7facf77b46e05 b9de5891c3fc40749deea033e7ae6d45 87221a3335ad4f728bd7facf77b46e05--b9de5891c3fc40749deea033e7ae6d45 600eeb35f87b4c07b28e42774233309d b9de5891c3fc40749deea033e7ae6d45--600eeb35f87b4c07b28e42774233309d 4bfb01a53b8343468b7afc8df00ffd80 600eeb35f87b4c07b28e42774233309d--4bfb01a53b8343468b7afc8df00ffd80 01a3ef4f9455448da1ac84c6630653a7 4bfb01a53b8343468b7afc8df00ffd80--01a3ef4f9455448da1ac84c6630653a7 fa0875dc94404a60a363c29c64e05459 01a3ef4f9455448da1ac84c6630653a7--fa0875dc94404a60a363c29c64e05459 a5be0fe275104f8fa21c06204d547ac9 fa0875dc94404a60a363c29c64e05459--a5be0fe275104f8fa21c06204d547ac9 181bacc8132047198f300d682cb3be60 a5be0fe275104f8fa21c06204d547ac9--181bacc8132047198f300d682cb3be60 a6458f5259fb498c8e625b1ee4ed7781 181bacc8132047198f300d682cb3be60--a6458f5259fb498c8e625b1ee4ed7781 a4dafaa4102b4982b4769dea01703205 a6458f5259fb498c8e625b1ee4ed7781--a4dafaa4102b4982b4769dea01703205 44dfd273ba3d4f888e8ce0937b042145 a4dafaa4102b4982b4769dea01703205--44dfd273ba3d4f888e8ce0937b042145 0a74b0f5b51544e493327462c642eac2 RX(b0) 44dfd273ba3d4f888e8ce0937b042145--0a74b0f5b51544e493327462c642eac2 daaecae04d8b4bf0bf88a2c4874b1ed7 0a74b0f5b51544e493327462c642eac2--daaecae04d8b4bf0bf88a2c4874b1ed7 cb94c5f2ce4a4888a9ff8d5f4242c333 ac06b1a6c1de4253aefdfc965e4e49cf H df985dc277d141dabeac7766b0eebbb5--ac06b1a6c1de4253aefdfc965e4e49cf df4d087906364cd88e65c96f5905feb6 2 e0c93338c1214a9b917d754e780422ef X ac06b1a6c1de4253aefdfc965e4e49cf--e0c93338c1214a9b917d754e780422ef e0c93338c1214a9b917d754e780422ef--f9857700938d4452abcd462ed3518c5f 04b4724ba8ac468f80315b7854853a2e RZ(g0) e0c93338c1214a9b917d754e780422ef--04b4724ba8ac468f80315b7854853a2e d72eb67e915f4f7482e729a636711096 X 04b4724ba8ac468f80315b7854853a2e--d72eb67e915f4f7482e729a636711096 d72eb67e915f4f7482e729a636711096--ef8605c0c13143d288c783542b54505d dd22b39a6ba94959bed2e804360ee18e d72eb67e915f4f7482e729a636711096--dd22b39a6ba94959bed2e804360ee18e a8b1e947e722425691a362096c204b7b dd22b39a6ba94959bed2e804360ee18e--a8b1e947e722425691a362096c204b7b 4e03802f6c1947aaae10212dabde6c4d a8b1e947e722425691a362096c204b7b--4e03802f6c1947aaae10212dabde6c4d 309db4e1ac074cc39e144b2ca3732179 4e03802f6c1947aaae10212dabde6c4d--309db4e1ac074cc39e144b2ca3732179 13cfb9dbd6cb4d69b91216c5a188d3e6 309db4e1ac074cc39e144b2ca3732179--13cfb9dbd6cb4d69b91216c5a188d3e6 6d99201b8a434a86a331f172b66b4ec6 13cfb9dbd6cb4d69b91216c5a188d3e6--6d99201b8a434a86a331f172b66b4ec6 0658fcaa912b429eb77279e464b886f8 6d99201b8a434a86a331f172b66b4ec6--0658fcaa912b429eb77279e464b886f8 26aa726601504d71806267b2ed003796 0658fcaa912b429eb77279e464b886f8--26aa726601504d71806267b2ed003796 b8268cfde15046678e914b484aacec2b 26aa726601504d71806267b2ed003796--b8268cfde15046678e914b484aacec2b 88ed374ba97049f38dc6f6548473fe13 b8268cfde15046678e914b484aacec2b--88ed374ba97049f38dc6f6548473fe13 c316ee9650514e7bb613cb875439cece 88ed374ba97049f38dc6f6548473fe13--c316ee9650514e7bb613cb875439cece cb09cb3e363a49a0921a966a4452d72a c316ee9650514e7bb613cb875439cece--cb09cb3e363a49a0921a966a4452d72a a997298efaa6463399cf5a7cecdacab9 cb09cb3e363a49a0921a966a4452d72a--a997298efaa6463399cf5a7cecdacab9 ff4d57e5984241e684be14900cfd970a a997298efaa6463399cf5a7cecdacab9--ff4d57e5984241e684be14900cfd970a 6d0e1ec5b89f4112a28ad46a72f52ebe ff4d57e5984241e684be14900cfd970a--6d0e1ec5b89f4112a28ad46a72f52ebe 64ccd8b6a73242f2ad47e4cc0cdda613 RX(b0) 6d0e1ec5b89f4112a28ad46a72f52ebe--64ccd8b6a73242f2ad47e4cc0cdda613 64ccd8b6a73242f2ad47e4cc0cdda613--cb94c5f2ce4a4888a9ff8d5f4242c333 558ad84819f44f8ba41ab0cb7ded5890 108b1225ec3a4b03a57b71168d9a16c2 H df4d087906364cd88e65c96f5905feb6--108b1225ec3a4b03a57b71168d9a16c2 b309f2a0777e4e1fa3b2083020610889 3 b2a06fe3f496436c85719fd681737ba5 108b1225ec3a4b03a57b71168d9a16c2--b2a06fe3f496436c85719fd681737ba5 9ca47f83dc5a4ed0bd8efe8d6c007cc2 b2a06fe3f496436c85719fd681737ba5--9ca47f83dc5a4ed0bd8efe8d6c007cc2 d9f4b0b3c73649199ac28cf08d851df6 9ca47f83dc5a4ed0bd8efe8d6c007cc2--d9f4b0b3c73649199ac28cf08d851df6 4e65953466654a668df35d872a7204fd X d9f4b0b3c73649199ac28cf08d851df6--4e65953466654a668df35d872a7204fd 4e65953466654a668df35d872a7204fd--2b403437c3154da4ab83f3602a4c3087 7f7a0f318c26480c96d9eabcf6ae6112 RZ(g0) 4e65953466654a668df35d872a7204fd--7f7a0f318c26480c96d9eabcf6ae6112 9191fba95eee40e1870f19b8de4cefef X 7f7a0f318c26480c96d9eabcf6ae6112--9191fba95eee40e1870f19b8de4cefef 9191fba95eee40e1870f19b8de4cefef--fba18e27189a41aa83fd57a5457d372d 9c1a20e7d40f4581b61872f1498259d9 9191fba95eee40e1870f19b8de4cefef--9c1a20e7d40f4581b61872f1498259d9 8dd9c56ab9714dc7bfe4f90ab73f3ac6 9c1a20e7d40f4581b61872f1498259d9--8dd9c56ab9714dc7bfe4f90ab73f3ac6 5ddec2b5dcd44ee994da28970126a606 8dd9c56ab9714dc7bfe4f90ab73f3ac6--5ddec2b5dcd44ee994da28970126a606 99cc9515a3b8456cbebb8cf532e8d3ce X 5ddec2b5dcd44ee994da28970126a606--99cc9515a3b8456cbebb8cf532e8d3ce 99cc9515a3b8456cbebb8cf532e8d3ce--0658fcaa912b429eb77279e464b886f8 69a47eec1e9d43afa418a8a01850280a RZ(g0) 99cc9515a3b8456cbebb8cf532e8d3ce--69a47eec1e9d43afa418a8a01850280a 0dc7e5cdeb5644ffacfdae4a15da191d X 69a47eec1e9d43afa418a8a01850280a--0dc7e5cdeb5644ffacfdae4a15da191d 0dc7e5cdeb5644ffacfdae4a15da191d--b8268cfde15046678e914b484aacec2b ece9d4b83de44419a2f02a3fa2020b89 0dc7e5cdeb5644ffacfdae4a15da191d--ece9d4b83de44419a2f02a3fa2020b89 5f750e45973f44d1b942bcbb75d7cda7 ece9d4b83de44419a2f02a3fa2020b89--5f750e45973f44d1b942bcbb75d7cda7 53e5e60048a94267874ed10a9d9c65a9 5f750e45973f44d1b942bcbb75d7cda7--53e5e60048a94267874ed10a9d9c65a9 192c3d1fa58c44e89be52c6d1864b10c 53e5e60048a94267874ed10a9d9c65a9--192c3d1fa58c44e89be52c6d1864b10c 0ebc2c907e5c4784a18d8c6ce6cba027 192c3d1fa58c44e89be52c6d1864b10c--0ebc2c907e5c4784a18d8c6ce6cba027 ce5e880c041b4fd6ab0ebc3f79a6d384 0ebc2c907e5c4784a18d8c6ce6cba027--ce5e880c041b4fd6ab0ebc3f79a6d384 5060a3a3703849cfb32fc6feac052850 RX(b0) ce5e880c041b4fd6ab0ebc3f79a6d384--5060a3a3703849cfb32fc6feac052850 5060a3a3703849cfb32fc6feac052850--558ad84819f44f8ba41ab0cb7ded5890 091f1f36abb54f17a43f1e5570cb1328 ce2cb98725664817ba5a881b2501e701 H b309f2a0777e4e1fa3b2083020610889--ce2cb98725664817ba5a881b2501e701 1763a51d3d7a4ae9b177fdf7b2e90d4a ce2cb98725664817ba5a881b2501e701--1763a51d3d7a4ae9b177fdf7b2e90d4a d7cb6938f99e4f7e84cf98ceaa026bb6 1763a51d3d7a4ae9b177fdf7b2e90d4a--d7cb6938f99e4f7e84cf98ceaa026bb6 0b5970d9d97c48b5a4e87f4bf210ebed d7cb6938f99e4f7e84cf98ceaa026bb6--0b5970d9d97c48b5a4e87f4bf210ebed af0e4722149d4229806c1c0d021d5a84 0b5970d9d97c48b5a4e87f4bf210ebed--af0e4722149d4229806c1c0d021d5a84 8c69ecd2bf8748cf809b1e509e1da8fd af0e4722149d4229806c1c0d021d5a84--8c69ecd2bf8748cf809b1e509e1da8fd bc21577ffca44b3489708b347f6b7020 8c69ecd2bf8748cf809b1e509e1da8fd--bc21577ffca44b3489708b347f6b7020 7135488ea22c49bd9abd82a51281405e X bc21577ffca44b3489708b347f6b7020--7135488ea22c49bd9abd82a51281405e 7135488ea22c49bd9abd82a51281405e--a3b60e0e6b9d45649a6a2ec351378985 263524ab69414bf4a284771e64572bbc RZ(g0) 7135488ea22c49bd9abd82a51281405e--263524ab69414bf4a284771e64572bbc 0317d95a143f4da58f543f0d3ff33ede X 263524ab69414bf4a284771e64572bbc--0317d95a143f4da58f543f0d3ff33ede 0317d95a143f4da58f543f0d3ff33ede--b9de5891c3fc40749deea033e7ae6d45 b1216953958444fbb0673b4a8592532d 0317d95a143f4da58f543f0d3ff33ede--b1216953958444fbb0673b4a8592532d b8f68927f9ca40bb9762caee45439ab2 b1216953958444fbb0673b4a8592532d--b8f68927f9ca40bb9762caee45439ab2 583084351a8141c292669bd82b41fd65 b8f68927f9ca40bb9762caee45439ab2--583084351a8141c292669bd82b41fd65 7bf1e8fecfb84ac99aea6236b7c559e8 X 583084351a8141c292669bd82b41fd65--7bf1e8fecfb84ac99aea6236b7c559e8 7bf1e8fecfb84ac99aea6236b7c559e8--88ed374ba97049f38dc6f6548473fe13 8ab8e3233c8646009f68d16ea3d6d9cf RZ(g0) 7bf1e8fecfb84ac99aea6236b7c559e8--8ab8e3233c8646009f68d16ea3d6d9cf 4a7238ee094d47cda8a2e051bdf89689 X 8ab8e3233c8646009f68d16ea3d6d9cf--4a7238ee094d47cda8a2e051bdf89689 4a7238ee094d47cda8a2e051bdf89689--cb09cb3e363a49a0921a966a4452d72a 77c6530ef63f4c029b97bc1d679eb4db X 4a7238ee094d47cda8a2e051bdf89689--77c6530ef63f4c029b97bc1d679eb4db 77c6530ef63f4c029b97bc1d679eb4db--192c3d1fa58c44e89be52c6d1864b10c 83a5a594c0084632af60d08d9709106d RZ(g0) 77c6530ef63f4c029b97bc1d679eb4db--83a5a594c0084632af60d08d9709106d e2803431554146ecbba4a9b6b8e0d09f X 83a5a594c0084632af60d08d9709106d--e2803431554146ecbba4a9b6b8e0d09f e2803431554146ecbba4a9b6b8e0d09f--ce5e880c041b4fd6ab0ebc3f79a6d384 ad1353d97ca348baadaca44460af2c1a RX(b0) e2803431554146ecbba4a9b6b8e0d09f--ad1353d97ca348baadaca44460af2c1a ad1353d97ca348baadaca44460af2c1a--091f1f36abb54f17a43f1e5570cb1328

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-26T12:34:25.691431 image/svg+xml Matplotlib v3.7.5, https://matplotlib.org/

References


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