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-12T10:07:44.434745 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_68da9b6a9b8342e897ffaf57aa695070 mixing cluster_7414d2b8cf6f4a6d907b8dfc1bec7c56 cost bdd8b09d37cc42dbba60a84ba6c2a68b 0 7306b4cf1e524e959e34fb6d519e79c6 H bdd8b09d37cc42dbba60a84ba6c2a68b--7306b4cf1e524e959e34fb6d519e79c6 2811560612024a4aadbd1eca9605272e 1 2e31b7a40e7a411f808064789a156b1b 7306b4cf1e524e959e34fb6d519e79c6--2e31b7a40e7a411f808064789a156b1b f5abce6435594f8f95ced983497f27d1 2e31b7a40e7a411f808064789a156b1b--f5abce6435594f8f95ced983497f27d1 2b8274255f3841a7807252e200060600 f5abce6435594f8f95ced983497f27d1--2b8274255f3841a7807252e200060600 81ecdb5e7d8b4b5bbd949109098640a5 2b8274255f3841a7807252e200060600--81ecdb5e7d8b4b5bbd949109098640a5 0954ef644a034e36a44bddc5a8af816c 81ecdb5e7d8b4b5bbd949109098640a5--0954ef644a034e36a44bddc5a8af816c 97040edfc9a74759b80b243e52673bd7 0954ef644a034e36a44bddc5a8af816c--97040edfc9a74759b80b243e52673bd7 ef5eb03dac3b4acd8c1b0999a02ee127 97040edfc9a74759b80b243e52673bd7--ef5eb03dac3b4acd8c1b0999a02ee127 d5a975b4ddd8412f9f21a1b660ff93ba ef5eb03dac3b4acd8c1b0999a02ee127--d5a975b4ddd8412f9f21a1b660ff93ba a21559942ecb4ea2b20b71641ecae142 d5a975b4ddd8412f9f21a1b660ff93ba--a21559942ecb4ea2b20b71641ecae142 e5f9f8a935be4794886f6e8e425901a3 a21559942ecb4ea2b20b71641ecae142--e5f9f8a935be4794886f6e8e425901a3 95ed3976a9e34751a8756e155746af96 e5f9f8a935be4794886f6e8e425901a3--95ed3976a9e34751a8756e155746af96 14661f1eded8440690082f89af644a1e 95ed3976a9e34751a8756e155746af96--14661f1eded8440690082f89af644a1e ed03b9a62918492a8e16099cae0fbdd9 14661f1eded8440690082f89af644a1e--ed03b9a62918492a8e16099cae0fbdd9 21d33859466a4169a36584d9ea711c06 ed03b9a62918492a8e16099cae0fbdd9--21d33859466a4169a36584d9ea711c06 2490fbc3abae4555b4097dde62ccc207 21d33859466a4169a36584d9ea711c06--2490fbc3abae4555b4097dde62ccc207 956bdc88484944e5aeec94d004e0a1a0 2490fbc3abae4555b4097dde62ccc207--956bdc88484944e5aeec94d004e0a1a0 c5ef3aa471114aea9dbf8d5e4f45c05b 956bdc88484944e5aeec94d004e0a1a0--c5ef3aa471114aea9dbf8d5e4f45c05b e14d38fc95fa480ebc8244498354ad41 c5ef3aa471114aea9dbf8d5e4f45c05b--e14d38fc95fa480ebc8244498354ad41 a13f67c4a2694301859682f2cd9b23ef RX(b0) e14d38fc95fa480ebc8244498354ad41--a13f67c4a2694301859682f2cd9b23ef a99a18fe550a463191c821710ee44ff0 a13f67c4a2694301859682f2cd9b23ef--a99a18fe550a463191c821710ee44ff0 13dab6adae434a55a9ed6fb54337fb48 45c2b57514a943f3b9caf7ec32c0646a H 2811560612024a4aadbd1eca9605272e--45c2b57514a943f3b9caf7ec32c0646a 4b167eccc0124728b6b7bccd24198eff 2 93fcfb3cef4d4f4698ca9b6581a1ae3b X 45c2b57514a943f3b9caf7ec32c0646a--93fcfb3cef4d4f4698ca9b6581a1ae3b 93fcfb3cef4d4f4698ca9b6581a1ae3b--2e31b7a40e7a411f808064789a156b1b 4c376f9a94bd43f0aea889066423b653 RZ(g0) 93fcfb3cef4d4f4698ca9b6581a1ae3b--4c376f9a94bd43f0aea889066423b653 9f62cfde10734fb3b9412bdb07e256f6 X 4c376f9a94bd43f0aea889066423b653--9f62cfde10734fb3b9412bdb07e256f6 9f62cfde10734fb3b9412bdb07e256f6--2b8274255f3841a7807252e200060600 7ea6955c9fb149768db854e708d4f42e 9f62cfde10734fb3b9412bdb07e256f6--7ea6955c9fb149768db854e708d4f42e 21a032dac89f43ff96312ebdd2bac3e2 7ea6955c9fb149768db854e708d4f42e--21a032dac89f43ff96312ebdd2bac3e2 a2d798f0e81c41f8be661171e5bf5b37 21a032dac89f43ff96312ebdd2bac3e2--a2d798f0e81c41f8be661171e5bf5b37 b5ba022a48ee402cabd533f90ecda959 a2d798f0e81c41f8be661171e5bf5b37--b5ba022a48ee402cabd533f90ecda959 97819ddc08634928afd94e53b200c194 b5ba022a48ee402cabd533f90ecda959--97819ddc08634928afd94e53b200c194 5e1bc5c169f44d26920fc23c60793457 97819ddc08634928afd94e53b200c194--5e1bc5c169f44d26920fc23c60793457 c87fa3d03e7348dd99ed3885e044b1e3 5e1bc5c169f44d26920fc23c60793457--c87fa3d03e7348dd99ed3885e044b1e3 341f23f2583f41ea921bc39a61ff6f4a c87fa3d03e7348dd99ed3885e044b1e3--341f23f2583f41ea921bc39a61ff6f4a a57995fe50154881bfbcdb012efe5e52 341f23f2583f41ea921bc39a61ff6f4a--a57995fe50154881bfbcdb012efe5e52 c8cd03100fea44b2a301cb1789b05c4a a57995fe50154881bfbcdb012efe5e52--c8cd03100fea44b2a301cb1789b05c4a 40f254ef572540d2b46dce960821c45b c8cd03100fea44b2a301cb1789b05c4a--40f254ef572540d2b46dce960821c45b 0d114552eedf4d7187336f656e8c7cbf 40f254ef572540d2b46dce960821c45b--0d114552eedf4d7187336f656e8c7cbf 378e707b5b804460881c397d42c514da 0d114552eedf4d7187336f656e8c7cbf--378e707b5b804460881c397d42c514da ef43d433308b463aa6ddd1e1fc82cf57 378e707b5b804460881c397d42c514da--ef43d433308b463aa6ddd1e1fc82cf57 b59660af36c94de3b79c73782552a01c ef43d433308b463aa6ddd1e1fc82cf57--b59660af36c94de3b79c73782552a01c 8618e841ba8c40a5b98e35cf482197c1 RX(b0) b59660af36c94de3b79c73782552a01c--8618e841ba8c40a5b98e35cf482197c1 8618e841ba8c40a5b98e35cf482197c1--13dab6adae434a55a9ed6fb54337fb48 021fd5260ad24689ac636199f6935635 c81f602106274bbabf6f7c8d55a7eaf1 H 4b167eccc0124728b6b7bccd24198eff--c81f602106274bbabf6f7c8d55a7eaf1 ced808668c724a3299e142812bc61d2d 3 77c8695e00474b0f812e622fd15445f9 c81f602106274bbabf6f7c8d55a7eaf1--77c8695e00474b0f812e622fd15445f9 21a97b72183d4cc6b93e8a73b55b3255 77c8695e00474b0f812e622fd15445f9--21a97b72183d4cc6b93e8a73b55b3255 c581ab48d8844147a8aee0b75a20d1a4 21a97b72183d4cc6b93e8a73b55b3255--c581ab48d8844147a8aee0b75a20d1a4 1bb5c186f92e4cc18afd2446de16fa07 X c581ab48d8844147a8aee0b75a20d1a4--1bb5c186f92e4cc18afd2446de16fa07 1bb5c186f92e4cc18afd2446de16fa07--81ecdb5e7d8b4b5bbd949109098640a5 d91d2be62cc9430bbd19035e9f6613f1 RZ(g0) 1bb5c186f92e4cc18afd2446de16fa07--d91d2be62cc9430bbd19035e9f6613f1 f022931a1ef24aaab6815bc695bc1b4d X d91d2be62cc9430bbd19035e9f6613f1--f022931a1ef24aaab6815bc695bc1b4d f022931a1ef24aaab6815bc695bc1b4d--97040edfc9a74759b80b243e52673bd7 8d057866ab7d400fbfb3867e5cfcca75 f022931a1ef24aaab6815bc695bc1b4d--8d057866ab7d400fbfb3867e5cfcca75 feb747cff28a4c2894a1a45f72661e17 8d057866ab7d400fbfb3867e5cfcca75--feb747cff28a4c2894a1a45f72661e17 8bdfa916664f4eb2bd3961b9499309cf feb747cff28a4c2894a1a45f72661e17--8bdfa916664f4eb2bd3961b9499309cf c83361e1a92142b99cf2690a5d2f7a85 X 8bdfa916664f4eb2bd3961b9499309cf--c83361e1a92142b99cf2690a5d2f7a85 c83361e1a92142b99cf2690a5d2f7a85--c87fa3d03e7348dd99ed3885e044b1e3 fd41c60b14b247f89da9b4d694520f67 RZ(g0) c83361e1a92142b99cf2690a5d2f7a85--fd41c60b14b247f89da9b4d694520f67 823ba79264f9453abda27d4a4f4ec72b X fd41c60b14b247f89da9b4d694520f67--823ba79264f9453abda27d4a4f4ec72b 823ba79264f9453abda27d4a4f4ec72b--a57995fe50154881bfbcdb012efe5e52 24fcaf7088814d4792c338f972990344 823ba79264f9453abda27d4a4f4ec72b--24fcaf7088814d4792c338f972990344 84b28c25297d4428a961677ca000fa39 24fcaf7088814d4792c338f972990344--84b28c25297d4428a961677ca000fa39 87f15983a2f449ab971b5c9c2de72702 84b28c25297d4428a961677ca000fa39--87f15983a2f449ab971b5c9c2de72702 e852419a5bd144d99846de3c70c01a69 87f15983a2f449ab971b5c9c2de72702--e852419a5bd144d99846de3c70c01a69 a10fc95fe6aa4ab696a43e50410c7798 e852419a5bd144d99846de3c70c01a69--a10fc95fe6aa4ab696a43e50410c7798 4add7b6e5b2e45a4a20c6c5304162e40 a10fc95fe6aa4ab696a43e50410c7798--4add7b6e5b2e45a4a20c6c5304162e40 c2c7386c9e484abe83491474d2e653d2 RX(b0) 4add7b6e5b2e45a4a20c6c5304162e40--c2c7386c9e484abe83491474d2e653d2 c2c7386c9e484abe83491474d2e653d2--021fd5260ad24689ac636199f6935635 52935b7e8b5843f4a9a19554fee324db 47523fa3cf4a4aeda1de96e238f480de H ced808668c724a3299e142812bc61d2d--47523fa3cf4a4aeda1de96e238f480de 2ec25fb265524f74a34bfd7e2119b9a4 47523fa3cf4a4aeda1de96e238f480de--2ec25fb265524f74a34bfd7e2119b9a4 4cb55ed68a194a479b113af002f54402 2ec25fb265524f74a34bfd7e2119b9a4--4cb55ed68a194a479b113af002f54402 84624a6b25324c4398c768e4de5a4a42 4cb55ed68a194a479b113af002f54402--84624a6b25324c4398c768e4de5a4a42 3e80400b533347699811fe777d466278 84624a6b25324c4398c768e4de5a4a42--3e80400b533347699811fe777d466278 772dbbdf5c2e43988a5c9b3605db15c5 3e80400b533347699811fe777d466278--772dbbdf5c2e43988a5c9b3605db15c5 6fb8a1fb6948461cbb2f0e9cee290a84 772dbbdf5c2e43988a5c9b3605db15c5--6fb8a1fb6948461cbb2f0e9cee290a84 abffb7de404b4676b21a6eba88399f18 X 6fb8a1fb6948461cbb2f0e9cee290a84--abffb7de404b4676b21a6eba88399f18 abffb7de404b4676b21a6eba88399f18--ef5eb03dac3b4acd8c1b0999a02ee127 133be4af98cb41df9367c0a4c46bcd79 RZ(g0) abffb7de404b4676b21a6eba88399f18--133be4af98cb41df9367c0a4c46bcd79 623ce631ff544b54aa95f2913bedd5e6 X 133be4af98cb41df9367c0a4c46bcd79--623ce631ff544b54aa95f2913bedd5e6 623ce631ff544b54aa95f2913bedd5e6--a21559942ecb4ea2b20b71641ecae142 ea7b9f49ed5c44d4a465c25ad0ff3b46 623ce631ff544b54aa95f2913bedd5e6--ea7b9f49ed5c44d4a465c25ad0ff3b46 6c9937df535d411d9ed29c6742aa6089 ea7b9f49ed5c44d4a465c25ad0ff3b46--6c9937df535d411d9ed29c6742aa6089 6071181848614b92abb23c06fa1ac2ef 6c9937df535d411d9ed29c6742aa6089--6071181848614b92abb23c06fa1ac2ef ee682a80ac62417685ec749ebc916953 X 6071181848614b92abb23c06fa1ac2ef--ee682a80ac62417685ec749ebc916953 ee682a80ac62417685ec749ebc916953--c8cd03100fea44b2a301cb1789b05c4a b96f4f187cd847c8a26522b441a52602 RZ(g0) ee682a80ac62417685ec749ebc916953--b96f4f187cd847c8a26522b441a52602 94f67f9d53dc4fe5b08a9ce47902d273 X b96f4f187cd847c8a26522b441a52602--94f67f9d53dc4fe5b08a9ce47902d273 94f67f9d53dc4fe5b08a9ce47902d273--0d114552eedf4d7187336f656e8c7cbf 36488d021fa14247801d2b88f2970d96 X 94f67f9d53dc4fe5b08a9ce47902d273--36488d021fa14247801d2b88f2970d96 36488d021fa14247801d2b88f2970d96--e852419a5bd144d99846de3c70c01a69 478922d301ac4692ae0337ad497c00e3 RZ(g0) 36488d021fa14247801d2b88f2970d96--478922d301ac4692ae0337ad497c00e3 3fa08931de1544f28084d65ff7a61805 X 478922d301ac4692ae0337ad497c00e3--3fa08931de1544f28084d65ff7a61805 3fa08931de1544f28084d65ff7a61805--4add7b6e5b2e45a4a20c6c5304162e40 27e895891a944f089de1013dd37f76ed RX(b0) 3fa08931de1544f28084d65ff7a61805--27e895891a944f089de1013dd37f76ed 27e895891a944f089de1013dd37f76ed--52935b7e8b5843f4a9a19554fee324db

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-12T10:07:49.167099 image/svg+xml Matplotlib v3.7.5, https://matplotlib.org/

References


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