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-11-05T09:41:54.183746 image/svg+xml Matplotlib v3.9.2, 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_482c7211164d4b49b2e708d4410c82ff mixing cluster_13ace87a6ac94d168ded2b47fa07bcbc cost b07a460371784451bdf71ddf445b44b1 0 82c193c12ee5480d9129bde26a664f2e H b07a460371784451bdf71ddf445b44b1--82c193c12ee5480d9129bde26a664f2e af756148aaff47e995e5ea6129991c3c 1 e338bcea4a0e40ecad8b1eac7a7a33a5 82c193c12ee5480d9129bde26a664f2e--e338bcea4a0e40ecad8b1eac7a7a33a5 85d029c6ca7e41b78ef979111b9d299b e338bcea4a0e40ecad8b1eac7a7a33a5--85d029c6ca7e41b78ef979111b9d299b 3722e3562dba45729f9d9a343e203e3e 85d029c6ca7e41b78ef979111b9d299b--3722e3562dba45729f9d9a343e203e3e b5d428a518d043848e99117f9e565440 3722e3562dba45729f9d9a343e203e3e--b5d428a518d043848e99117f9e565440 9297a65e89de49e4af928c6f10616619 b5d428a518d043848e99117f9e565440--9297a65e89de49e4af928c6f10616619 63ecd8ae433446e0b3fa1def9700e814 9297a65e89de49e4af928c6f10616619--63ecd8ae433446e0b3fa1def9700e814 ecc494c0731e4fc583f7dec67fae0838 63ecd8ae433446e0b3fa1def9700e814--ecc494c0731e4fc583f7dec67fae0838 0f6b86b28f2c42abb92632e49c0a1688 ecc494c0731e4fc583f7dec67fae0838--0f6b86b28f2c42abb92632e49c0a1688 cda4e0d117ee4b81a0da2e2aa9c11e10 0f6b86b28f2c42abb92632e49c0a1688--cda4e0d117ee4b81a0da2e2aa9c11e10 4a44d421add344ea932cfe0d41fc2719 cda4e0d117ee4b81a0da2e2aa9c11e10--4a44d421add344ea932cfe0d41fc2719 66c42391415a44d3afb6697547262529 4a44d421add344ea932cfe0d41fc2719--66c42391415a44d3afb6697547262529 3a7972f08abe4cf18abcd8270af77319 66c42391415a44d3afb6697547262529--3a7972f08abe4cf18abcd8270af77319 13f517da7d3b4c6f849a6f3f13bd6ed0 3a7972f08abe4cf18abcd8270af77319--13f517da7d3b4c6f849a6f3f13bd6ed0 94c2c5c8d85e4b2d8052761d54b10c2a 13f517da7d3b4c6f849a6f3f13bd6ed0--94c2c5c8d85e4b2d8052761d54b10c2a 75e5cd8d67c1416fac20862666bdaae4 94c2c5c8d85e4b2d8052761d54b10c2a--75e5cd8d67c1416fac20862666bdaae4 d702562f06eb49d593ff14e0684d3114 75e5cd8d67c1416fac20862666bdaae4--d702562f06eb49d593ff14e0684d3114 ac63ee7fc11342e19afbb7900d0d0c58 d702562f06eb49d593ff14e0684d3114--ac63ee7fc11342e19afbb7900d0d0c58 9f10eca45a5346839420399b3eeab35b ac63ee7fc11342e19afbb7900d0d0c58--9f10eca45a5346839420399b3eeab35b d82233c6e659488db18e4a48e6b004cb RX(b0) 9f10eca45a5346839420399b3eeab35b--d82233c6e659488db18e4a48e6b004cb 3e84c901278444d9b813a16d436d58b9 d82233c6e659488db18e4a48e6b004cb--3e84c901278444d9b813a16d436d58b9 e1da0972e6ea4b9fbce292470175dcf1 2e5b9abf6c5245e59538eb6930a04d78 H af756148aaff47e995e5ea6129991c3c--2e5b9abf6c5245e59538eb6930a04d78 e3e8a149cb64482c8baede7ffb8c29f1 2 07266dc1ead244d1abad77960661085e X 2e5b9abf6c5245e59538eb6930a04d78--07266dc1ead244d1abad77960661085e 07266dc1ead244d1abad77960661085e--e338bcea4a0e40ecad8b1eac7a7a33a5 0bda8cac9ccc4f1ea06779ca49f25203 RZ(g0) 07266dc1ead244d1abad77960661085e--0bda8cac9ccc4f1ea06779ca49f25203 ec18a62bcc594960a96c46fda0f17399 X 0bda8cac9ccc4f1ea06779ca49f25203--ec18a62bcc594960a96c46fda0f17399 ec18a62bcc594960a96c46fda0f17399--3722e3562dba45729f9d9a343e203e3e b96a6c270c524fd885ee1bbdf2f6c1b0 ec18a62bcc594960a96c46fda0f17399--b96a6c270c524fd885ee1bbdf2f6c1b0 a26fa1c172f14445ab85826a488d0ccd b96a6c270c524fd885ee1bbdf2f6c1b0--a26fa1c172f14445ab85826a488d0ccd b96fb717866640e987c1d2f25cd2d32a a26fa1c172f14445ab85826a488d0ccd--b96fb717866640e987c1d2f25cd2d32a ee9d78be471443b781f697d7d59636bb b96fb717866640e987c1d2f25cd2d32a--ee9d78be471443b781f697d7d59636bb 5412f6b58a894ff6851988b0ec3431a2 ee9d78be471443b781f697d7d59636bb--5412f6b58a894ff6851988b0ec3431a2 57ed9484d3f843f7aabb855e843d4f34 5412f6b58a894ff6851988b0ec3431a2--57ed9484d3f843f7aabb855e843d4f34 39be55eef2e74b9dac580d7cf1e695e9 57ed9484d3f843f7aabb855e843d4f34--39be55eef2e74b9dac580d7cf1e695e9 cac1e2be148f481994b2cb7042fec114 39be55eef2e74b9dac580d7cf1e695e9--cac1e2be148f481994b2cb7042fec114 58be96d6dac14016a6026584ef598be9 cac1e2be148f481994b2cb7042fec114--58be96d6dac14016a6026584ef598be9 254ca95f68cf402d84d17a3177e7b015 58be96d6dac14016a6026584ef598be9--254ca95f68cf402d84d17a3177e7b015 b545b1f7e8774817b3ebb644d149c6c3 254ca95f68cf402d84d17a3177e7b015--b545b1f7e8774817b3ebb644d149c6c3 9643b759b4b1409ba9e65105f76e8ec1 b545b1f7e8774817b3ebb644d149c6c3--9643b759b4b1409ba9e65105f76e8ec1 bbecfa16f8014d2199ecf49d89f99ca8 9643b759b4b1409ba9e65105f76e8ec1--bbecfa16f8014d2199ecf49d89f99ca8 f82956960880454c9457af5429304660 bbecfa16f8014d2199ecf49d89f99ca8--f82956960880454c9457af5429304660 d5b33702f03f469398f1a10fa38caff6 f82956960880454c9457af5429304660--d5b33702f03f469398f1a10fa38caff6 594c90e9b3bf4610a5cc80ddad9ff9e9 RX(b0) d5b33702f03f469398f1a10fa38caff6--594c90e9b3bf4610a5cc80ddad9ff9e9 594c90e9b3bf4610a5cc80ddad9ff9e9--e1da0972e6ea4b9fbce292470175dcf1 e5b1a0b08a1b4c7686f0b4485af06585 af2814a100e84f0c9393c76e1596b7dc H e3e8a149cb64482c8baede7ffb8c29f1--af2814a100e84f0c9393c76e1596b7dc 3b6790ca231b455fa904428841e60078 3 181c7d7e6cd64d0db81ce99b402d6803 af2814a100e84f0c9393c76e1596b7dc--181c7d7e6cd64d0db81ce99b402d6803 91d1adfe96c3438bb150bd9d34d4dc71 181c7d7e6cd64d0db81ce99b402d6803--91d1adfe96c3438bb150bd9d34d4dc71 d6c1405c76494818985380bf27a94cb1 91d1adfe96c3438bb150bd9d34d4dc71--d6c1405c76494818985380bf27a94cb1 26205936b5594d77bfb3c44d9dcebb48 X d6c1405c76494818985380bf27a94cb1--26205936b5594d77bfb3c44d9dcebb48 26205936b5594d77bfb3c44d9dcebb48--b5d428a518d043848e99117f9e565440 008f6f81788d4288940893e98e7b6535 RZ(g0) 26205936b5594d77bfb3c44d9dcebb48--008f6f81788d4288940893e98e7b6535 0242a4b4a9df410a937571fa9ffc4325 X 008f6f81788d4288940893e98e7b6535--0242a4b4a9df410a937571fa9ffc4325 0242a4b4a9df410a937571fa9ffc4325--63ecd8ae433446e0b3fa1def9700e814 31d2629d45f04c2b98a3708678050076 0242a4b4a9df410a937571fa9ffc4325--31d2629d45f04c2b98a3708678050076 39bbb7b3db4746a7b2cecd9312185050 31d2629d45f04c2b98a3708678050076--39bbb7b3db4746a7b2cecd9312185050 be51f53db9fd4896bac3e3094966c171 39bbb7b3db4746a7b2cecd9312185050--be51f53db9fd4896bac3e3094966c171 4017402860d04e87b5cbddd9293bc2d3 X be51f53db9fd4896bac3e3094966c171--4017402860d04e87b5cbddd9293bc2d3 4017402860d04e87b5cbddd9293bc2d3--39be55eef2e74b9dac580d7cf1e695e9 5644945e95714729b8a9ed1e781f453b RZ(g0) 4017402860d04e87b5cbddd9293bc2d3--5644945e95714729b8a9ed1e781f453b 2fd48439a86e460d93829805cbf1e682 X 5644945e95714729b8a9ed1e781f453b--2fd48439a86e460d93829805cbf1e682 2fd48439a86e460d93829805cbf1e682--58be96d6dac14016a6026584ef598be9 e44f2a6672e143469c645f558c740431 2fd48439a86e460d93829805cbf1e682--e44f2a6672e143469c645f558c740431 0a3d334071934094852888e2858f9f8a e44f2a6672e143469c645f558c740431--0a3d334071934094852888e2858f9f8a 806633815a384712b40b8723edc6127f 0a3d334071934094852888e2858f9f8a--806633815a384712b40b8723edc6127f 3086c4ee3c8b49b6bd46136f498d7084 806633815a384712b40b8723edc6127f--3086c4ee3c8b49b6bd46136f498d7084 29fd49d374974272943ba19441d916ee 3086c4ee3c8b49b6bd46136f498d7084--29fd49d374974272943ba19441d916ee 0b6b8b4974744ca4b2a3bda9173e4163 29fd49d374974272943ba19441d916ee--0b6b8b4974744ca4b2a3bda9173e4163 2f8d837f11b847fe8f83940d4489a8ec RX(b0) 0b6b8b4974744ca4b2a3bda9173e4163--2f8d837f11b847fe8f83940d4489a8ec 2f8d837f11b847fe8f83940d4489a8ec--e5b1a0b08a1b4c7686f0b4485af06585 c7033c82b4fc468cba4d712204173c82 79e762b1dec94642abef245e2f6e0c02 H 3b6790ca231b455fa904428841e60078--79e762b1dec94642abef245e2f6e0c02 ea67130e7b694e7fb530f19f2da49811 79e762b1dec94642abef245e2f6e0c02--ea67130e7b694e7fb530f19f2da49811 febe42ce394045a682eedc81c1161323 ea67130e7b694e7fb530f19f2da49811--febe42ce394045a682eedc81c1161323 b22a105a9dfa47f3b4207d93ac6c4fea febe42ce394045a682eedc81c1161323--b22a105a9dfa47f3b4207d93ac6c4fea 858fc69a36324c6895d920c54aa32f59 b22a105a9dfa47f3b4207d93ac6c4fea--858fc69a36324c6895d920c54aa32f59 681413cb243042be9ea1de98d572ed4f 858fc69a36324c6895d920c54aa32f59--681413cb243042be9ea1de98d572ed4f 26195442b8d049329c93013424442ed8 681413cb243042be9ea1de98d572ed4f--26195442b8d049329c93013424442ed8 94ecd003186c492b9513da4011094b47 X 26195442b8d049329c93013424442ed8--94ecd003186c492b9513da4011094b47 94ecd003186c492b9513da4011094b47--ecc494c0731e4fc583f7dec67fae0838 7866c54925f74f0a96f0df262ac9e02d RZ(g0) 94ecd003186c492b9513da4011094b47--7866c54925f74f0a96f0df262ac9e02d cf7dceec9b0e4135a6d82a913bcfc826 X 7866c54925f74f0a96f0df262ac9e02d--cf7dceec9b0e4135a6d82a913bcfc826 cf7dceec9b0e4135a6d82a913bcfc826--cda4e0d117ee4b81a0da2e2aa9c11e10 83bb978781fb49acaa9c103ee18a49e7 cf7dceec9b0e4135a6d82a913bcfc826--83bb978781fb49acaa9c103ee18a49e7 f35694e0c0f3478b838c336ad3c016cd 83bb978781fb49acaa9c103ee18a49e7--f35694e0c0f3478b838c336ad3c016cd 8fa7ed81f6ca4c1395254165a7af5ef0 f35694e0c0f3478b838c336ad3c016cd--8fa7ed81f6ca4c1395254165a7af5ef0 c623b4583cbe43f594715e58192b9652 X 8fa7ed81f6ca4c1395254165a7af5ef0--c623b4583cbe43f594715e58192b9652 c623b4583cbe43f594715e58192b9652--254ca95f68cf402d84d17a3177e7b015 8dabb160f4574058b8694cbde0bfc145 RZ(g0) c623b4583cbe43f594715e58192b9652--8dabb160f4574058b8694cbde0bfc145 482f029aac7642c68dc745dce510fc39 X 8dabb160f4574058b8694cbde0bfc145--482f029aac7642c68dc745dce510fc39 482f029aac7642c68dc745dce510fc39--9643b759b4b1409ba9e65105f76e8ec1 524674fcba294f7d87132eb3522dd7a6 X 482f029aac7642c68dc745dce510fc39--524674fcba294f7d87132eb3522dd7a6 524674fcba294f7d87132eb3522dd7a6--3086c4ee3c8b49b6bd46136f498d7084 8ea56c8054314575a28aaa4a9da5e9e0 RZ(g0) 524674fcba294f7d87132eb3522dd7a6--8ea56c8054314575a28aaa4a9da5e9e0 4d6c15af402a4a10902681d965d79733 X 8ea56c8054314575a28aaa4a9da5e9e0--4d6c15af402a4a10902681d965d79733 4d6c15af402a4a10902681d965d79733--0b6b8b4974744ca4b2a3bda9173e4163 3e7567668b5c4bc7bc36044167e83e5f RX(b0) 4d6c15af402a4a10902681d965d79733--3e7567668b5c4bc7bc36044167e83e5f 3e7567668b5c4bc7bc36044167e83e5f--c7033c82b4fc468cba4d712204173c82

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.7470706807026417
MaxCut cost at iteration 20: 3.8378810288930216
MaxCut cost at iteration 30: 3.9424197899236133
MaxCut cost at iteration 40: 3.9981256255766002
MaxCut cost at iteration 50: 3.996470528508214
MaxCut cost at iteration 60: 3.9991374608876606
MaxCut cost at iteration 70: 3.9994678542919555
MaxCut cost at iteration 80: 3.999872558672829
MaxCut cost at iteration 90: 3.9999475834121063
MaxCut cost at iteration 100: 3.9999793311641003

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-11-05T09:41:58.942987 image/svg+xml Matplotlib v3.9.2, https://matplotlib.org/

References


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