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-04-11T13:28:00.389114 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_f2ef13eab8b04e74851691cc62633e2f mixing cluster_1f8bf6774c214a2b98d95a5f675b37bd cost c8e96fcd300a427b9317b6252c8e7ef2 0 16991532d84a4887b2c32fb2026681b0 H c8e96fcd300a427b9317b6252c8e7ef2--16991532d84a4887b2c32fb2026681b0 40f7ed243d9c42edb184fdb53e720810 1 74ebcef133564b87aec3e254b6e7ee69 16991532d84a4887b2c32fb2026681b0--74ebcef133564b87aec3e254b6e7ee69 bf2ce0d240a64e078bf08e8eb0dd8d76 74ebcef133564b87aec3e254b6e7ee69--bf2ce0d240a64e078bf08e8eb0dd8d76 3a4d4ceb2b11489a9af8edffd484fbbd bf2ce0d240a64e078bf08e8eb0dd8d76--3a4d4ceb2b11489a9af8edffd484fbbd 414e070c2f574669aec8cf441ab3b330 3a4d4ceb2b11489a9af8edffd484fbbd--414e070c2f574669aec8cf441ab3b330 5b6d4cd23ee345fcaad2171518cfd8df 414e070c2f574669aec8cf441ab3b330--5b6d4cd23ee345fcaad2171518cfd8df e8cf966c39ae4713abba3b13bafb0295 5b6d4cd23ee345fcaad2171518cfd8df--e8cf966c39ae4713abba3b13bafb0295 5a78b66a31d24f75a9186a0550b040b8 e8cf966c39ae4713abba3b13bafb0295--5a78b66a31d24f75a9186a0550b040b8 392711614d614a1c9741a588eaa02ca0 5a78b66a31d24f75a9186a0550b040b8--392711614d614a1c9741a588eaa02ca0 14df7f9f0c7f4924b145f2629eb15848 392711614d614a1c9741a588eaa02ca0--14df7f9f0c7f4924b145f2629eb15848 ea3d446782424b2799de000cdb6bc550 14df7f9f0c7f4924b145f2629eb15848--ea3d446782424b2799de000cdb6bc550 d1026926011c482eabdb57106e8ca50b ea3d446782424b2799de000cdb6bc550--d1026926011c482eabdb57106e8ca50b 173565b312e74e8387f2b8174e8648cb d1026926011c482eabdb57106e8ca50b--173565b312e74e8387f2b8174e8648cb c7d4e0110cb343c0838af056406bf392 173565b312e74e8387f2b8174e8648cb--c7d4e0110cb343c0838af056406bf392 573209626505461787c0a239e941644e c7d4e0110cb343c0838af056406bf392--573209626505461787c0a239e941644e 875a0a81245d48a28cc04eecf0d36b99 573209626505461787c0a239e941644e--875a0a81245d48a28cc04eecf0d36b99 902d0a4874aa433bb86c1fb7ee067289 875a0a81245d48a28cc04eecf0d36b99--902d0a4874aa433bb86c1fb7ee067289 260031a0803948da9eb0706e9fd61ee8 902d0a4874aa433bb86c1fb7ee067289--260031a0803948da9eb0706e9fd61ee8 9a3cceb5c1884a7dafc69f7b19056297 260031a0803948da9eb0706e9fd61ee8--9a3cceb5c1884a7dafc69f7b19056297 36bddf76e0e4428c981a1d9080fe3be0 RX(b0) 9a3cceb5c1884a7dafc69f7b19056297--36bddf76e0e4428c981a1d9080fe3be0 4bf6e4a2d9e646a999a911d66eccc1f3 36bddf76e0e4428c981a1d9080fe3be0--4bf6e4a2d9e646a999a911d66eccc1f3 8173394ce4d24b469961c972439f3474 44e5ec3170ee4344a29d7ccb2c835331 H 40f7ed243d9c42edb184fdb53e720810--44e5ec3170ee4344a29d7ccb2c835331 6c4bc0583f55461a9236470f182aaedf 2 294eecf6621448678af7a6353773d2c0 X 44e5ec3170ee4344a29d7ccb2c835331--294eecf6621448678af7a6353773d2c0 294eecf6621448678af7a6353773d2c0--74ebcef133564b87aec3e254b6e7ee69 fef72bd2f5b246be900bb900bc801756 RZ(g0) 294eecf6621448678af7a6353773d2c0--fef72bd2f5b246be900bb900bc801756 fdd0d0cfef454216971da3bd98daec0c X fef72bd2f5b246be900bb900bc801756--fdd0d0cfef454216971da3bd98daec0c fdd0d0cfef454216971da3bd98daec0c--3a4d4ceb2b11489a9af8edffd484fbbd 42ba653628b440678ca022f2d9864ddb fdd0d0cfef454216971da3bd98daec0c--42ba653628b440678ca022f2d9864ddb c02de411c2974db2aa3c96b27f49b4db 42ba653628b440678ca022f2d9864ddb--c02de411c2974db2aa3c96b27f49b4db 24495a5df48e46cda5d8a4779e1fc2c8 c02de411c2974db2aa3c96b27f49b4db--24495a5df48e46cda5d8a4779e1fc2c8 21de0784e9534c9b8e82dc5fb300a319 24495a5df48e46cda5d8a4779e1fc2c8--21de0784e9534c9b8e82dc5fb300a319 0af9bb47f6fe4e5a80fbd2809eec47fc 21de0784e9534c9b8e82dc5fb300a319--0af9bb47f6fe4e5a80fbd2809eec47fc d447ef26e9ca4cd08dfe1743b2f254e1 0af9bb47f6fe4e5a80fbd2809eec47fc--d447ef26e9ca4cd08dfe1743b2f254e1 906a9b6dcb2f4b6dbc7c08e4451ba65e d447ef26e9ca4cd08dfe1743b2f254e1--906a9b6dcb2f4b6dbc7c08e4451ba65e 7c6e2a8aab114afb8afeac59870953cf 906a9b6dcb2f4b6dbc7c08e4451ba65e--7c6e2a8aab114afb8afeac59870953cf d107fc102c26454db6de422332bf84e1 7c6e2a8aab114afb8afeac59870953cf--d107fc102c26454db6de422332bf84e1 c0d8ebc2c48a4d379a3768a00dc15854 d107fc102c26454db6de422332bf84e1--c0d8ebc2c48a4d379a3768a00dc15854 7dc3a73190924971be558bba55668736 c0d8ebc2c48a4d379a3768a00dc15854--7dc3a73190924971be558bba55668736 10865003c0914339b65ae190b8b09e02 7dc3a73190924971be558bba55668736--10865003c0914339b65ae190b8b09e02 776a0756c278400a809d3935ae3500c0 10865003c0914339b65ae190b8b09e02--776a0756c278400a809d3935ae3500c0 690cf4d8a71b4b2d92563404c14d4c04 776a0756c278400a809d3935ae3500c0--690cf4d8a71b4b2d92563404c14d4c04 2eafb420e5074ceaa0a01480f14efa78 690cf4d8a71b4b2d92563404c14d4c04--2eafb420e5074ceaa0a01480f14efa78 4e7ffba0a9154c92b8a2517a76a9ab66 RX(b0) 2eafb420e5074ceaa0a01480f14efa78--4e7ffba0a9154c92b8a2517a76a9ab66 4e7ffba0a9154c92b8a2517a76a9ab66--8173394ce4d24b469961c972439f3474 ae6885f2b6b047ceb243d10a23deee3e 779db40570c94e6184a2c539dd6ff69e H 6c4bc0583f55461a9236470f182aaedf--779db40570c94e6184a2c539dd6ff69e bef9775bc51d4a93a336bbdd099e4ebd 3 65c4fab260414050927d4bd29dff806e 779db40570c94e6184a2c539dd6ff69e--65c4fab260414050927d4bd29dff806e 9970df1a1d6244319019af9bb041cdba 65c4fab260414050927d4bd29dff806e--9970df1a1d6244319019af9bb041cdba 20dfd3039fd34472894d02a83ef74879 9970df1a1d6244319019af9bb041cdba--20dfd3039fd34472894d02a83ef74879 4248c3a74f8041f4bf361760c8b4b193 X 20dfd3039fd34472894d02a83ef74879--4248c3a74f8041f4bf361760c8b4b193 4248c3a74f8041f4bf361760c8b4b193--414e070c2f574669aec8cf441ab3b330 111451a3ef794f66aca5d5f032528ae3 RZ(g0) 4248c3a74f8041f4bf361760c8b4b193--111451a3ef794f66aca5d5f032528ae3 228bad5fef3d44619444ebff81976e0e X 111451a3ef794f66aca5d5f032528ae3--228bad5fef3d44619444ebff81976e0e 228bad5fef3d44619444ebff81976e0e--e8cf966c39ae4713abba3b13bafb0295 8fe3a73e73624d00b175268ff9819f96 228bad5fef3d44619444ebff81976e0e--8fe3a73e73624d00b175268ff9819f96 4c9b7385801f4550982d7a32d18a4193 8fe3a73e73624d00b175268ff9819f96--4c9b7385801f4550982d7a32d18a4193 40d65465506743a2a9523651403135e3 4c9b7385801f4550982d7a32d18a4193--40d65465506743a2a9523651403135e3 e9974faf06d7401889c8b1cfa733c77d X 40d65465506743a2a9523651403135e3--e9974faf06d7401889c8b1cfa733c77d e9974faf06d7401889c8b1cfa733c77d--906a9b6dcb2f4b6dbc7c08e4451ba65e d79210058a6a4397ba05961bf54dfc9c RZ(g0) e9974faf06d7401889c8b1cfa733c77d--d79210058a6a4397ba05961bf54dfc9c fd69564a091d4264ba6e54bcb69b5f23 X d79210058a6a4397ba05961bf54dfc9c--fd69564a091d4264ba6e54bcb69b5f23 fd69564a091d4264ba6e54bcb69b5f23--d107fc102c26454db6de422332bf84e1 495afde5e7bf42488c54e3ad08cc939e fd69564a091d4264ba6e54bcb69b5f23--495afde5e7bf42488c54e3ad08cc939e 9d33a4b3b9ab41eab315ac1abf316483 495afde5e7bf42488c54e3ad08cc939e--9d33a4b3b9ab41eab315ac1abf316483 fba8dab581ac4b3bbd173d2432dabbaa 9d33a4b3b9ab41eab315ac1abf316483--fba8dab581ac4b3bbd173d2432dabbaa 9510b0d8e3854705a74b1162b554a846 fba8dab581ac4b3bbd173d2432dabbaa--9510b0d8e3854705a74b1162b554a846 1e9a2ac50eaf4d11a610bc2412d49417 9510b0d8e3854705a74b1162b554a846--1e9a2ac50eaf4d11a610bc2412d49417 f07cbd97215d473aa2f59f3e668b52dd 1e9a2ac50eaf4d11a610bc2412d49417--f07cbd97215d473aa2f59f3e668b52dd e7efc024127c40d3b8fbdcd01dcedce9 RX(b0) f07cbd97215d473aa2f59f3e668b52dd--e7efc024127c40d3b8fbdcd01dcedce9 e7efc024127c40d3b8fbdcd01dcedce9--ae6885f2b6b047ceb243d10a23deee3e ef5b059b0a2a4690a79399528fd840b2 1ebf49e4b16e4612970f39fb4ed27ddf H bef9775bc51d4a93a336bbdd099e4ebd--1ebf49e4b16e4612970f39fb4ed27ddf f3bbf54f0d5a4e108a82d177cb06a223 1ebf49e4b16e4612970f39fb4ed27ddf--f3bbf54f0d5a4e108a82d177cb06a223 2a5146bf9f10470786ec710b569e1495 f3bbf54f0d5a4e108a82d177cb06a223--2a5146bf9f10470786ec710b569e1495 462ba13eae1443f7b7b0e711df7dc18a 2a5146bf9f10470786ec710b569e1495--462ba13eae1443f7b7b0e711df7dc18a f232e2131fbc42c79b49adc0b8ff768c 462ba13eae1443f7b7b0e711df7dc18a--f232e2131fbc42c79b49adc0b8ff768c b9a8508a5108450098cdb3a596b1dbc4 f232e2131fbc42c79b49adc0b8ff768c--b9a8508a5108450098cdb3a596b1dbc4 ed93af01e57f4ed8bae8c5aa45643f51 b9a8508a5108450098cdb3a596b1dbc4--ed93af01e57f4ed8bae8c5aa45643f51 0bdd9a956732475392aff13577ff4f60 X ed93af01e57f4ed8bae8c5aa45643f51--0bdd9a956732475392aff13577ff4f60 0bdd9a956732475392aff13577ff4f60--5a78b66a31d24f75a9186a0550b040b8 285ff7a557374450a1156ad0f8df921c RZ(g0) 0bdd9a956732475392aff13577ff4f60--285ff7a557374450a1156ad0f8df921c 3bfba1296fb44e74add6b64aa32095ac X 285ff7a557374450a1156ad0f8df921c--3bfba1296fb44e74add6b64aa32095ac 3bfba1296fb44e74add6b64aa32095ac--14df7f9f0c7f4924b145f2629eb15848 df93a5889f1c4b8182cd8e6ef484f390 3bfba1296fb44e74add6b64aa32095ac--df93a5889f1c4b8182cd8e6ef484f390 f1d84772c1d046a890b659a0d91f8302 df93a5889f1c4b8182cd8e6ef484f390--f1d84772c1d046a890b659a0d91f8302 ee23b6142f574068a4076c8c277feb00 f1d84772c1d046a890b659a0d91f8302--ee23b6142f574068a4076c8c277feb00 79618ff19a1d4f509fc59f10763c4f4a X ee23b6142f574068a4076c8c277feb00--79618ff19a1d4f509fc59f10763c4f4a 79618ff19a1d4f509fc59f10763c4f4a--c0d8ebc2c48a4d379a3768a00dc15854 81531558f6ee4af1a7a26fad19b60cbb RZ(g0) 79618ff19a1d4f509fc59f10763c4f4a--81531558f6ee4af1a7a26fad19b60cbb 0364a87070f44b1caacabea572194a38 X 81531558f6ee4af1a7a26fad19b60cbb--0364a87070f44b1caacabea572194a38 0364a87070f44b1caacabea572194a38--10865003c0914339b65ae190b8b09e02 24519e13771e4ba6a2f48aca751b950c X 0364a87070f44b1caacabea572194a38--24519e13771e4ba6a2f48aca751b950c 24519e13771e4ba6a2f48aca751b950c--9510b0d8e3854705a74b1162b554a846 359761cfda504a16a99d1b03ee483449 RZ(g0) 24519e13771e4ba6a2f48aca751b950c--359761cfda504a16a99d1b03ee483449 09562766ffcd4c5386472c91142b204e X 359761cfda504a16a99d1b03ee483449--09562766ffcd4c5386472c91142b204e 09562766ffcd4c5386472c91142b204e--f07cbd97215d473aa2f59f3e668b52dd fbfd1d0bf22844fea84fc23548a2e771 RX(b0) 09562766ffcd4c5386472c91142b204e--fbfd1d0bf22844fea84fc23548a2e771 fbfd1d0bf22844fea84fc23548a2e771--ef5b059b0a2a4690a79399528fd840b2

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-04-11T13:28:04.544058 image/svg+xml Matplotlib v3.7.5, https://matplotlib.org/

References


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