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-20T14:47:42.679879 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_5b65dd6d07b24c6da1570bd5c4e7374e mixing cluster_706f874e84fd4bd2bfe8a1838b04f06e cost 65131febb4e04b55a10671769e8d37a8 0 2bb06b4fefc14eaea3da3b7be75a0713 H 65131febb4e04b55a10671769e8d37a8--2bb06b4fefc14eaea3da3b7be75a0713 0d65f1f46de64e468e62e408e96ca86c 1 64e9b9ce01c04da1a8db2b93fe704bf0 2bb06b4fefc14eaea3da3b7be75a0713--64e9b9ce01c04da1a8db2b93fe704bf0 140e1cf8e65a4453ae66e82540e55739 64e9b9ce01c04da1a8db2b93fe704bf0--140e1cf8e65a4453ae66e82540e55739 1132c38a0d964e149fe11c05d95fbfbf 140e1cf8e65a4453ae66e82540e55739--1132c38a0d964e149fe11c05d95fbfbf 22139af461d148c4a32637b942aa0511 1132c38a0d964e149fe11c05d95fbfbf--22139af461d148c4a32637b942aa0511 9614ffa7ed9341ea89d0f8d1396ac964 22139af461d148c4a32637b942aa0511--9614ffa7ed9341ea89d0f8d1396ac964 01a15382d3c74abf886e0c183472c18f 9614ffa7ed9341ea89d0f8d1396ac964--01a15382d3c74abf886e0c183472c18f 92339e5520b0427d8fc9779552da95c5 01a15382d3c74abf886e0c183472c18f--92339e5520b0427d8fc9779552da95c5 e5fbcacac342496082fb42f814ba84b6 92339e5520b0427d8fc9779552da95c5--e5fbcacac342496082fb42f814ba84b6 35dd420d7713432abcbe0ae372d17acb e5fbcacac342496082fb42f814ba84b6--35dd420d7713432abcbe0ae372d17acb c741a0a0501f402ea79a8204703040eb 35dd420d7713432abcbe0ae372d17acb--c741a0a0501f402ea79a8204703040eb 19926e8d639c4383a6edbd8a8097f0db c741a0a0501f402ea79a8204703040eb--19926e8d639c4383a6edbd8a8097f0db af747caebc8749a08c096e8b6e0696cb 19926e8d639c4383a6edbd8a8097f0db--af747caebc8749a08c096e8b6e0696cb 2de81fa6855a4c71834b7f30198b7963 af747caebc8749a08c096e8b6e0696cb--2de81fa6855a4c71834b7f30198b7963 99790e252240404ab0e59267fb2d3074 2de81fa6855a4c71834b7f30198b7963--99790e252240404ab0e59267fb2d3074 1144cec2ed004d5196c33ef89502ead4 99790e252240404ab0e59267fb2d3074--1144cec2ed004d5196c33ef89502ead4 a699044b11b247a8a29ef8e129a0f579 1144cec2ed004d5196c33ef89502ead4--a699044b11b247a8a29ef8e129a0f579 d54864fd551e425eb2f6d6b06e75231e a699044b11b247a8a29ef8e129a0f579--d54864fd551e425eb2f6d6b06e75231e 9360430b53724b91b22a992fa25bdb2f d54864fd551e425eb2f6d6b06e75231e--9360430b53724b91b22a992fa25bdb2f a709f383328046bea887a473f8d8a5a3 RX(b0) 9360430b53724b91b22a992fa25bdb2f--a709f383328046bea887a473f8d8a5a3 213be3b51e4f438e984f810de824db6b a709f383328046bea887a473f8d8a5a3--213be3b51e4f438e984f810de824db6b f06deb6272f241ac99ddbda08c034dcf 854356851b1c4e66ac9aa26cacad205f H 0d65f1f46de64e468e62e408e96ca86c--854356851b1c4e66ac9aa26cacad205f 484e0544f647418298c94e54203edfdc 2 0624ab20def94204b32ac35fa8679eb9 X 854356851b1c4e66ac9aa26cacad205f--0624ab20def94204b32ac35fa8679eb9 0624ab20def94204b32ac35fa8679eb9--64e9b9ce01c04da1a8db2b93fe704bf0 d3e5cc0ed41f4363984defdf0fdb95da RZ(g0) 0624ab20def94204b32ac35fa8679eb9--d3e5cc0ed41f4363984defdf0fdb95da a4edd59d0fac47019d350146eb070f7b X d3e5cc0ed41f4363984defdf0fdb95da--a4edd59d0fac47019d350146eb070f7b a4edd59d0fac47019d350146eb070f7b--1132c38a0d964e149fe11c05d95fbfbf aa418323258c47279e5035bf23d9c985 a4edd59d0fac47019d350146eb070f7b--aa418323258c47279e5035bf23d9c985 f1b5408f215b484cac93dddf14535568 aa418323258c47279e5035bf23d9c985--f1b5408f215b484cac93dddf14535568 2d219f5d480549cca9ff11b3d4b3481b f1b5408f215b484cac93dddf14535568--2d219f5d480549cca9ff11b3d4b3481b f995258bf82443aa830e0d07ee61fe80 2d219f5d480549cca9ff11b3d4b3481b--f995258bf82443aa830e0d07ee61fe80 ee37c2d8d8ad4671bc8c025cf8c919f6 f995258bf82443aa830e0d07ee61fe80--ee37c2d8d8ad4671bc8c025cf8c919f6 21419e19be3e446592f8bb68b39496b7 ee37c2d8d8ad4671bc8c025cf8c919f6--21419e19be3e446592f8bb68b39496b7 3b7ea4caa56b4da2a837e7885cbc44ab 21419e19be3e446592f8bb68b39496b7--3b7ea4caa56b4da2a837e7885cbc44ab 071a9210b6044d708e8927f02bebaf44 3b7ea4caa56b4da2a837e7885cbc44ab--071a9210b6044d708e8927f02bebaf44 0817ae6b44214daab85a0f704811695a 071a9210b6044d708e8927f02bebaf44--0817ae6b44214daab85a0f704811695a 04ebe4376dd746a69ed1a5e1e7abdbcd 0817ae6b44214daab85a0f704811695a--04ebe4376dd746a69ed1a5e1e7abdbcd e89884ba88e9480e8a988bb8e1c2b8e9 04ebe4376dd746a69ed1a5e1e7abdbcd--e89884ba88e9480e8a988bb8e1c2b8e9 807638f531804b1093c922806f8c242d e89884ba88e9480e8a988bb8e1c2b8e9--807638f531804b1093c922806f8c242d 25d59feb158a41958e9a026ae6b04f77 807638f531804b1093c922806f8c242d--25d59feb158a41958e9a026ae6b04f77 cde4f42ff04d4d8b873648303f8d85d3 25d59feb158a41958e9a026ae6b04f77--cde4f42ff04d4d8b873648303f8d85d3 53df408c3ea84c2490dc178c57d9f1e4 cde4f42ff04d4d8b873648303f8d85d3--53df408c3ea84c2490dc178c57d9f1e4 31e5ace4a6ad49be86cdd174b125fefc RX(b0) 53df408c3ea84c2490dc178c57d9f1e4--31e5ace4a6ad49be86cdd174b125fefc 31e5ace4a6ad49be86cdd174b125fefc--f06deb6272f241ac99ddbda08c034dcf 02502cc1079a45ad91786ec3b5849923 971ecc67e9744ab7b78fcba5dd31a4e0 H 484e0544f647418298c94e54203edfdc--971ecc67e9744ab7b78fcba5dd31a4e0 9451f7d673194938bb528142753621cd 3 d1de7af8f85d4d96b9e44843d593f778 971ecc67e9744ab7b78fcba5dd31a4e0--d1de7af8f85d4d96b9e44843d593f778 2a4b1b0616ab4159b6867751b6db08fa d1de7af8f85d4d96b9e44843d593f778--2a4b1b0616ab4159b6867751b6db08fa 3bc90535e93a4853968961a81ad20e80 2a4b1b0616ab4159b6867751b6db08fa--3bc90535e93a4853968961a81ad20e80 9591ae77a37f4ea8ac6ce092352b4342 X 3bc90535e93a4853968961a81ad20e80--9591ae77a37f4ea8ac6ce092352b4342 9591ae77a37f4ea8ac6ce092352b4342--22139af461d148c4a32637b942aa0511 d4144af0413e46b5bb47e909c361e63b RZ(g0) 9591ae77a37f4ea8ac6ce092352b4342--d4144af0413e46b5bb47e909c361e63b b1160125fc874f3bb0b0e51aeb1cf40c X d4144af0413e46b5bb47e909c361e63b--b1160125fc874f3bb0b0e51aeb1cf40c b1160125fc874f3bb0b0e51aeb1cf40c--01a15382d3c74abf886e0c183472c18f 4e1c830296b84328a6e08841754dddd1 b1160125fc874f3bb0b0e51aeb1cf40c--4e1c830296b84328a6e08841754dddd1 7fc470bda817472d8dbdfc2b9c52f4fe 4e1c830296b84328a6e08841754dddd1--7fc470bda817472d8dbdfc2b9c52f4fe 0a0d00058a0a44d6a2606a6f3b9fe9ba 7fc470bda817472d8dbdfc2b9c52f4fe--0a0d00058a0a44d6a2606a6f3b9fe9ba 8d5e0a74ef1048189ec57c46df694c86 X 0a0d00058a0a44d6a2606a6f3b9fe9ba--8d5e0a74ef1048189ec57c46df694c86 8d5e0a74ef1048189ec57c46df694c86--3b7ea4caa56b4da2a837e7885cbc44ab a4fdf6dddba940c382c61294bf02805a RZ(g0) 8d5e0a74ef1048189ec57c46df694c86--a4fdf6dddba940c382c61294bf02805a ed522b0d8bfe42b5a3916a8fdb70c249 X a4fdf6dddba940c382c61294bf02805a--ed522b0d8bfe42b5a3916a8fdb70c249 ed522b0d8bfe42b5a3916a8fdb70c249--0817ae6b44214daab85a0f704811695a 4c20e77a7753419b8f21eba1f0e7cf36 ed522b0d8bfe42b5a3916a8fdb70c249--4c20e77a7753419b8f21eba1f0e7cf36 491168c55d5b4c15898f95c41c196f46 4c20e77a7753419b8f21eba1f0e7cf36--491168c55d5b4c15898f95c41c196f46 6fc41b8f1e5648d08488888dee6774f6 491168c55d5b4c15898f95c41c196f46--6fc41b8f1e5648d08488888dee6774f6 59ed5bdddfb0434a87de3bc49c3ff706 6fc41b8f1e5648d08488888dee6774f6--59ed5bdddfb0434a87de3bc49c3ff706 ffed8dfb6ac24a86b0581265d3a02687 59ed5bdddfb0434a87de3bc49c3ff706--ffed8dfb6ac24a86b0581265d3a02687 d6d1e537a64d4d788b7f383ee44eb09b ffed8dfb6ac24a86b0581265d3a02687--d6d1e537a64d4d788b7f383ee44eb09b 0326c7f29f6747abbaefa5adf4e8a3f9 RX(b0) d6d1e537a64d4d788b7f383ee44eb09b--0326c7f29f6747abbaefa5adf4e8a3f9 0326c7f29f6747abbaefa5adf4e8a3f9--02502cc1079a45ad91786ec3b5849923 0c0cf8d1dd6d47e4a25c99416930d85e a7a927a72cbe465cb5208624290c4bfb H 9451f7d673194938bb528142753621cd--a7a927a72cbe465cb5208624290c4bfb c87211d0b851499b94abce20016abdaf a7a927a72cbe465cb5208624290c4bfb--c87211d0b851499b94abce20016abdaf 562512557fe74f02acf220006faec1c9 c87211d0b851499b94abce20016abdaf--562512557fe74f02acf220006faec1c9 7bb88ab816dc4170a49a6fed6d7acb99 562512557fe74f02acf220006faec1c9--7bb88ab816dc4170a49a6fed6d7acb99 2827fc19d2bf49e389013f40279d2fcc 7bb88ab816dc4170a49a6fed6d7acb99--2827fc19d2bf49e389013f40279d2fcc 144c06e2d88c44ab812ea199c7362bb9 2827fc19d2bf49e389013f40279d2fcc--144c06e2d88c44ab812ea199c7362bb9 2a1a964e95ee445994daea890a075cfb 144c06e2d88c44ab812ea199c7362bb9--2a1a964e95ee445994daea890a075cfb d2e0e330c1f84b728cc20a27a5d11954 X 2a1a964e95ee445994daea890a075cfb--d2e0e330c1f84b728cc20a27a5d11954 d2e0e330c1f84b728cc20a27a5d11954--92339e5520b0427d8fc9779552da95c5 933d32c40bc2409e99c6b5774a0df6f3 RZ(g0) d2e0e330c1f84b728cc20a27a5d11954--933d32c40bc2409e99c6b5774a0df6f3 4f1b080e1b2749c892633eb7d91b30d3 X 933d32c40bc2409e99c6b5774a0df6f3--4f1b080e1b2749c892633eb7d91b30d3 4f1b080e1b2749c892633eb7d91b30d3--35dd420d7713432abcbe0ae372d17acb def1da6301894d0c86920c847e51938b 4f1b080e1b2749c892633eb7d91b30d3--def1da6301894d0c86920c847e51938b 6defde9f00784b6d928c2119e66349f6 def1da6301894d0c86920c847e51938b--6defde9f00784b6d928c2119e66349f6 0726381364114490afec3f64db23f1f1 6defde9f00784b6d928c2119e66349f6--0726381364114490afec3f64db23f1f1 1a2ecbd06e0f44baa1b814229a763002 X 0726381364114490afec3f64db23f1f1--1a2ecbd06e0f44baa1b814229a763002 1a2ecbd06e0f44baa1b814229a763002--04ebe4376dd746a69ed1a5e1e7abdbcd 539695e2907e46babb2580d1ca778115 RZ(g0) 1a2ecbd06e0f44baa1b814229a763002--539695e2907e46babb2580d1ca778115 06dead82b0f14ff9af325bd0886c1335 X 539695e2907e46babb2580d1ca778115--06dead82b0f14ff9af325bd0886c1335 06dead82b0f14ff9af325bd0886c1335--807638f531804b1093c922806f8c242d 99cb604c996243f8b3f0d893510e2bd3 X 06dead82b0f14ff9af325bd0886c1335--99cb604c996243f8b3f0d893510e2bd3 99cb604c996243f8b3f0d893510e2bd3--59ed5bdddfb0434a87de3bc49c3ff706 b0f94d56b699454f9d971f95a4a0abe3 RZ(g0) 99cb604c996243f8b3f0d893510e2bd3--b0f94d56b699454f9d971f95a4a0abe3 7cd7377101a94440b029fdd54a789d9f X b0f94d56b699454f9d971f95a4a0abe3--7cd7377101a94440b029fdd54a789d9f 7cd7377101a94440b029fdd54a789d9f--d6d1e537a64d4d788b7f383ee44eb09b 6a92893d1da3480193c8ef92a9de9a95 RX(b0) 7cd7377101a94440b029fdd54a789d9f--6a92893d1da3480193c8ef92a9de9a95 6a92893d1da3480193c8ef92a9de9a95--0c0cf8d1dd6d47e4a25c99416930d85e

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-20T14:47:47.066010 image/svg+xml Matplotlib v3.7.5, https://matplotlib.org/

References


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