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-21T16:35:15.525555 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_90fe28e979284272b132492d163cd542 mixing cluster_1e00c4a49d7f462c90897aa386ea07e2 cost 70fea891b6574991b9debe2e0e6f9c5e 0 5f304dd72d164209bf820d0142821dab H 70fea891b6574991b9debe2e0e6f9c5e--5f304dd72d164209bf820d0142821dab 03fc6ab0a7cf4779b98b6b7f23ef88c5 1 fff74902e45d42d7886fafefd7a50619 5f304dd72d164209bf820d0142821dab--fff74902e45d42d7886fafefd7a50619 54e69cbb329c43768f27fb4476b1e161 fff74902e45d42d7886fafefd7a50619--54e69cbb329c43768f27fb4476b1e161 7e6d5e6f833a411ca7bd0797e590f162 54e69cbb329c43768f27fb4476b1e161--7e6d5e6f833a411ca7bd0797e590f162 486217e4e29a4c4aa612f6a3ac99e49d 7e6d5e6f833a411ca7bd0797e590f162--486217e4e29a4c4aa612f6a3ac99e49d c05f7b4fa55343ab86dd812598b30949 486217e4e29a4c4aa612f6a3ac99e49d--c05f7b4fa55343ab86dd812598b30949 7552297515774500b486932735c7d028 c05f7b4fa55343ab86dd812598b30949--7552297515774500b486932735c7d028 1a2b60e924fa46cfbd7a47920e247f68 7552297515774500b486932735c7d028--1a2b60e924fa46cfbd7a47920e247f68 a8384a62b4fe4d25bb920c63428cacc2 1a2b60e924fa46cfbd7a47920e247f68--a8384a62b4fe4d25bb920c63428cacc2 ac6755d343cc496f9766913d98e0d321 a8384a62b4fe4d25bb920c63428cacc2--ac6755d343cc496f9766913d98e0d321 33607fadf5c54acf8abfd6d4bfc53579 ac6755d343cc496f9766913d98e0d321--33607fadf5c54acf8abfd6d4bfc53579 828cf7f52ee0463494e2a907c51b0a46 33607fadf5c54acf8abfd6d4bfc53579--828cf7f52ee0463494e2a907c51b0a46 bd139ea609c14dafa5b278b53b0f4cd4 828cf7f52ee0463494e2a907c51b0a46--bd139ea609c14dafa5b278b53b0f4cd4 e902592d56db47aab674f52ecf357551 bd139ea609c14dafa5b278b53b0f4cd4--e902592d56db47aab674f52ecf357551 bbfd1f219281411cbe89bce4d73f2319 e902592d56db47aab674f52ecf357551--bbfd1f219281411cbe89bce4d73f2319 22986b72fce5411e9a16b22b6e57b174 bbfd1f219281411cbe89bce4d73f2319--22986b72fce5411e9a16b22b6e57b174 923747b99fec4c33b1c86fd93dc6bae5 22986b72fce5411e9a16b22b6e57b174--923747b99fec4c33b1c86fd93dc6bae5 dddf5563340246588d346fefdcf4c9ba 923747b99fec4c33b1c86fd93dc6bae5--dddf5563340246588d346fefdcf4c9ba 79c5dd1bef33420cbfae34c4c3118dc3 dddf5563340246588d346fefdcf4c9ba--79c5dd1bef33420cbfae34c4c3118dc3 10812bceede045a6a38631dde3428e81 RX(b0) 79c5dd1bef33420cbfae34c4c3118dc3--10812bceede045a6a38631dde3428e81 e4adf825e54c449fb7cb342c611cb6c8 10812bceede045a6a38631dde3428e81--e4adf825e54c449fb7cb342c611cb6c8 2fe052340739402a9f1d8d1ea1e31369 0d75bf7c0be544efa41daf880440c826 H 03fc6ab0a7cf4779b98b6b7f23ef88c5--0d75bf7c0be544efa41daf880440c826 90c31cb325a04d31855225cc452dfaaa 2 ce1438ecfe80434aa78ffc7579d4ac02 X 0d75bf7c0be544efa41daf880440c826--ce1438ecfe80434aa78ffc7579d4ac02 ce1438ecfe80434aa78ffc7579d4ac02--fff74902e45d42d7886fafefd7a50619 d189ffb19bc8422d9c8f387dccafbb39 RZ(g0) ce1438ecfe80434aa78ffc7579d4ac02--d189ffb19bc8422d9c8f387dccafbb39 cbb169e533774cbeb138402ad35cae0f X d189ffb19bc8422d9c8f387dccafbb39--cbb169e533774cbeb138402ad35cae0f cbb169e533774cbeb138402ad35cae0f--7e6d5e6f833a411ca7bd0797e590f162 dfa7439fad65464aaec002ee54e459d4 cbb169e533774cbeb138402ad35cae0f--dfa7439fad65464aaec002ee54e459d4 592542cb1be3474faa3b12284bfcd69b dfa7439fad65464aaec002ee54e459d4--592542cb1be3474faa3b12284bfcd69b 1c080165f55c445a839de52c7e58912c 592542cb1be3474faa3b12284bfcd69b--1c080165f55c445a839de52c7e58912c 24fd876c03e145e784a171f8ec0231e3 1c080165f55c445a839de52c7e58912c--24fd876c03e145e784a171f8ec0231e3 ea63453c61554b5e9279c1dc5ec7a396 24fd876c03e145e784a171f8ec0231e3--ea63453c61554b5e9279c1dc5ec7a396 b3325ad4b0474d52adac6666c471975f ea63453c61554b5e9279c1dc5ec7a396--b3325ad4b0474d52adac6666c471975f dc4c3be3a5834479aab374ca3d43c3f6 b3325ad4b0474d52adac6666c471975f--dc4c3be3a5834479aab374ca3d43c3f6 9eaf607762ca44bea0ffd47466b4db4a dc4c3be3a5834479aab374ca3d43c3f6--9eaf607762ca44bea0ffd47466b4db4a 4eee4e45045b4596a939e595be05a189 9eaf607762ca44bea0ffd47466b4db4a--4eee4e45045b4596a939e595be05a189 dddfe9aada5e408e8e39756d9adf5819 4eee4e45045b4596a939e595be05a189--dddfe9aada5e408e8e39756d9adf5819 e9e2cdcffb6d448f812a01c06d2cac19 dddfe9aada5e408e8e39756d9adf5819--e9e2cdcffb6d448f812a01c06d2cac19 9a69239e9c7c48ff9c121df75e89b99c e9e2cdcffb6d448f812a01c06d2cac19--9a69239e9c7c48ff9c121df75e89b99c 049098a307794ce7a88a154037fb796a 9a69239e9c7c48ff9c121df75e89b99c--049098a307794ce7a88a154037fb796a 3ba94eef8d3e44cb9fdb1785e6c7e0fd 049098a307794ce7a88a154037fb796a--3ba94eef8d3e44cb9fdb1785e6c7e0fd b277f8a3d4b342c4ba88b7f923a200fc 3ba94eef8d3e44cb9fdb1785e6c7e0fd--b277f8a3d4b342c4ba88b7f923a200fc 146e6690b88f407d9899d5bcc6100168 RX(b0) b277f8a3d4b342c4ba88b7f923a200fc--146e6690b88f407d9899d5bcc6100168 146e6690b88f407d9899d5bcc6100168--2fe052340739402a9f1d8d1ea1e31369 779114ff77f44f86be7b04610819041f 7ed1292a905b45a8bffb818e91dfefa8 H 90c31cb325a04d31855225cc452dfaaa--7ed1292a905b45a8bffb818e91dfefa8 50c663e6474e4c678929909a86e2663f 3 9c8b7254346244b2afa0e064ece9521c 7ed1292a905b45a8bffb818e91dfefa8--9c8b7254346244b2afa0e064ece9521c f732138385c649e48d0e8bf5fa9082d3 9c8b7254346244b2afa0e064ece9521c--f732138385c649e48d0e8bf5fa9082d3 963735efb1d544f7b1cb09b151c6e3e8 f732138385c649e48d0e8bf5fa9082d3--963735efb1d544f7b1cb09b151c6e3e8 e0d8df8b5cc44aafb67dae5d228a034f X 963735efb1d544f7b1cb09b151c6e3e8--e0d8df8b5cc44aafb67dae5d228a034f e0d8df8b5cc44aafb67dae5d228a034f--486217e4e29a4c4aa612f6a3ac99e49d f1c67f34d6034410a8935a887192cbc1 RZ(g0) e0d8df8b5cc44aafb67dae5d228a034f--f1c67f34d6034410a8935a887192cbc1 a5081cbac2ed4d3e86237269abc1ac28 X f1c67f34d6034410a8935a887192cbc1--a5081cbac2ed4d3e86237269abc1ac28 a5081cbac2ed4d3e86237269abc1ac28--7552297515774500b486932735c7d028 2cda4ee7e07f49308ff256ae3fbcdbb0 a5081cbac2ed4d3e86237269abc1ac28--2cda4ee7e07f49308ff256ae3fbcdbb0 0bfd11cc7cd646019b39f70e66922c58 2cda4ee7e07f49308ff256ae3fbcdbb0--0bfd11cc7cd646019b39f70e66922c58 7cb886785cc14cc3a2f8a3e14eb14510 0bfd11cc7cd646019b39f70e66922c58--7cb886785cc14cc3a2f8a3e14eb14510 6e2d58ae833d4b9aa483d7ddf69fa28c X 7cb886785cc14cc3a2f8a3e14eb14510--6e2d58ae833d4b9aa483d7ddf69fa28c 6e2d58ae833d4b9aa483d7ddf69fa28c--dc4c3be3a5834479aab374ca3d43c3f6 d986ae44c1414cb788a571e7f8a5e019 RZ(g0) 6e2d58ae833d4b9aa483d7ddf69fa28c--d986ae44c1414cb788a571e7f8a5e019 f602f60724e84c74a16837d8f566f171 X d986ae44c1414cb788a571e7f8a5e019--f602f60724e84c74a16837d8f566f171 f602f60724e84c74a16837d8f566f171--4eee4e45045b4596a939e595be05a189 77f2e8c335e143fca28af581ee4a3d6f f602f60724e84c74a16837d8f566f171--77f2e8c335e143fca28af581ee4a3d6f ae5d48fc982140ce9029f5f0a65e7aad 77f2e8c335e143fca28af581ee4a3d6f--ae5d48fc982140ce9029f5f0a65e7aad 2c320df7677f41cbb0dc4ab501e96df9 ae5d48fc982140ce9029f5f0a65e7aad--2c320df7677f41cbb0dc4ab501e96df9 e8f27f358ec24002a14d0d6274cfcb67 2c320df7677f41cbb0dc4ab501e96df9--e8f27f358ec24002a14d0d6274cfcb67 9d9e73c4b0c348b18a8d8332d81b230c e8f27f358ec24002a14d0d6274cfcb67--9d9e73c4b0c348b18a8d8332d81b230c 4228069c5bc0463c89d6895add9c8384 9d9e73c4b0c348b18a8d8332d81b230c--4228069c5bc0463c89d6895add9c8384 39431dcc6eba46c691f0636f9c2d460a RX(b0) 4228069c5bc0463c89d6895add9c8384--39431dcc6eba46c691f0636f9c2d460a 39431dcc6eba46c691f0636f9c2d460a--779114ff77f44f86be7b04610819041f 2f25f6dcaa0e4e29b1142dbf1a9ad379 c4e0f2223b7d48fa9fff199e1131ac4c H 50c663e6474e4c678929909a86e2663f--c4e0f2223b7d48fa9fff199e1131ac4c 4eae1a71956d46c6a6f1cb65e18f9119 c4e0f2223b7d48fa9fff199e1131ac4c--4eae1a71956d46c6a6f1cb65e18f9119 d66e2bff9a2f41beb4204ba37574dcaa 4eae1a71956d46c6a6f1cb65e18f9119--d66e2bff9a2f41beb4204ba37574dcaa e2cfb997bd2145288e700f75f936b272 d66e2bff9a2f41beb4204ba37574dcaa--e2cfb997bd2145288e700f75f936b272 aa6c78f5ae954fde85e0d90c625a0877 e2cfb997bd2145288e700f75f936b272--aa6c78f5ae954fde85e0d90c625a0877 dc4924120ef84147a5e5d5a148216e95 aa6c78f5ae954fde85e0d90c625a0877--dc4924120ef84147a5e5d5a148216e95 06425de42b344425943467c730fc5e0c dc4924120ef84147a5e5d5a148216e95--06425de42b344425943467c730fc5e0c 72127798141f4043b3c055baccfbee37 X 06425de42b344425943467c730fc5e0c--72127798141f4043b3c055baccfbee37 72127798141f4043b3c055baccfbee37--1a2b60e924fa46cfbd7a47920e247f68 228af1a99bd44f9483b4db5141d21f72 RZ(g0) 72127798141f4043b3c055baccfbee37--228af1a99bd44f9483b4db5141d21f72 803545531da14432b443a1243084ad66 X 228af1a99bd44f9483b4db5141d21f72--803545531da14432b443a1243084ad66 803545531da14432b443a1243084ad66--ac6755d343cc496f9766913d98e0d321 32bead4badcb4f55b3f6467df75622cf 803545531da14432b443a1243084ad66--32bead4badcb4f55b3f6467df75622cf 18ad358013e24efba5b1097f442bfc16 32bead4badcb4f55b3f6467df75622cf--18ad358013e24efba5b1097f442bfc16 25104c81178a4d3ba299072cf5903fe4 18ad358013e24efba5b1097f442bfc16--25104c81178a4d3ba299072cf5903fe4 3b94e33eee0542c9ac94094b265307bd X 25104c81178a4d3ba299072cf5903fe4--3b94e33eee0542c9ac94094b265307bd 3b94e33eee0542c9ac94094b265307bd--dddfe9aada5e408e8e39756d9adf5819 f5dd1497a3a04a55b8441e1c196c0b2c RZ(g0) 3b94e33eee0542c9ac94094b265307bd--f5dd1497a3a04a55b8441e1c196c0b2c 96d45ed3e035415b93ce6e11b9d22641 X f5dd1497a3a04a55b8441e1c196c0b2c--96d45ed3e035415b93ce6e11b9d22641 96d45ed3e035415b93ce6e11b9d22641--9a69239e9c7c48ff9c121df75e89b99c e7ba724334c0474da41f8d5495754e1e X 96d45ed3e035415b93ce6e11b9d22641--e7ba724334c0474da41f8d5495754e1e e7ba724334c0474da41f8d5495754e1e--e8f27f358ec24002a14d0d6274cfcb67 5a237f52a7254cde835d014858f0aec1 RZ(g0) e7ba724334c0474da41f8d5495754e1e--5a237f52a7254cde835d014858f0aec1 1386b97676b84173b19e9e76ec24044a X 5a237f52a7254cde835d014858f0aec1--1386b97676b84173b19e9e76ec24044a 1386b97676b84173b19e9e76ec24044a--4228069c5bc0463c89d6895add9c8384 e77c41fb68534e58a6b5ad682ff60091 RX(b0) 1386b97676b84173b19e9e76ec24044a--e77c41fb68534e58a6b5ad682ff60091 e77c41fb68534e58a6b5ad682ff60091--2f25f6dcaa0e4e29b1142dbf1a9ad379

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-21T16:35:19.849230 image/svg+xml Matplotlib v3.7.5, https://matplotlib.org/

References


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