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-09-03T10:07:54.807917 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_24afc829a38742b592317d288a0b6f3d mixing cluster_c716f9e7685f455bbc0b351dfdbdbe80 cost a6cb9764b08048c8b509738eb74685d6 0 b1963e0eeb8148e3a7bb900dd488539e H a6cb9764b08048c8b509738eb74685d6--b1963e0eeb8148e3a7bb900dd488539e 87c5660c9f96402c8a905c78c059ea3e 1 80767a81c19b4f3293a59f975be622fa b1963e0eeb8148e3a7bb900dd488539e--80767a81c19b4f3293a59f975be622fa 4d48b100e44a4909874c4858487c2791 80767a81c19b4f3293a59f975be622fa--4d48b100e44a4909874c4858487c2791 ab52f53a98644a6b9d3fa8ea73615b64 4d48b100e44a4909874c4858487c2791--ab52f53a98644a6b9d3fa8ea73615b64 d8c2021ccf96425bbdb56ef93c0d7584 ab52f53a98644a6b9d3fa8ea73615b64--d8c2021ccf96425bbdb56ef93c0d7584 8204784617a94502a6c954a80a81310e d8c2021ccf96425bbdb56ef93c0d7584--8204784617a94502a6c954a80a81310e 2990b2cd89ac4355aa652d19c54b290d 8204784617a94502a6c954a80a81310e--2990b2cd89ac4355aa652d19c54b290d 558857fa7db94b4ab620ac20b6f0b4ff 2990b2cd89ac4355aa652d19c54b290d--558857fa7db94b4ab620ac20b6f0b4ff 6a65ef5ff4a84829842a97e9a385a0d0 558857fa7db94b4ab620ac20b6f0b4ff--6a65ef5ff4a84829842a97e9a385a0d0 5aaa5bd6060149f49ba12f14f8214c74 6a65ef5ff4a84829842a97e9a385a0d0--5aaa5bd6060149f49ba12f14f8214c74 62c942d10fb94c85967514275c3a5b1c 5aaa5bd6060149f49ba12f14f8214c74--62c942d10fb94c85967514275c3a5b1c 33eb8c56bb8842e6b839c5ff85ddcd21 62c942d10fb94c85967514275c3a5b1c--33eb8c56bb8842e6b839c5ff85ddcd21 747df47b190b4606aebfda57f47ec0a7 33eb8c56bb8842e6b839c5ff85ddcd21--747df47b190b4606aebfda57f47ec0a7 fa1f551375c34709ac2a1ad9d0ce349e 747df47b190b4606aebfda57f47ec0a7--fa1f551375c34709ac2a1ad9d0ce349e d4fe6a2884e348f8b945119d9145e75f fa1f551375c34709ac2a1ad9d0ce349e--d4fe6a2884e348f8b945119d9145e75f 8f538cb13ea84ad3bace6e8b56799678 d4fe6a2884e348f8b945119d9145e75f--8f538cb13ea84ad3bace6e8b56799678 8d2c930a50284da2ab2cf0d34bd41d11 8f538cb13ea84ad3bace6e8b56799678--8d2c930a50284da2ab2cf0d34bd41d11 595578bb5d634d929c60bd4e7e4728d6 8d2c930a50284da2ab2cf0d34bd41d11--595578bb5d634d929c60bd4e7e4728d6 d7b97996e227436d94a19e58d19cb2b7 595578bb5d634d929c60bd4e7e4728d6--d7b97996e227436d94a19e58d19cb2b7 29addb8687e24510a32cbaa774bb2d45 RX(b0) d7b97996e227436d94a19e58d19cb2b7--29addb8687e24510a32cbaa774bb2d45 a9243e586f9948b4944e7bb8b86af884 29addb8687e24510a32cbaa774bb2d45--a9243e586f9948b4944e7bb8b86af884 7cdd568295d44eb2ad0170f2806f4d63 6267ac260d544bf991e728a53a5d1ffd H 87c5660c9f96402c8a905c78c059ea3e--6267ac260d544bf991e728a53a5d1ffd b8e20d617a954435aeac9154f08f2597 2 b2558ee14f124c57ae39153a3137127a X 6267ac260d544bf991e728a53a5d1ffd--b2558ee14f124c57ae39153a3137127a b2558ee14f124c57ae39153a3137127a--80767a81c19b4f3293a59f975be622fa 2602d589d0ff479f9a1582ad7f8f180e RZ(g0) b2558ee14f124c57ae39153a3137127a--2602d589d0ff479f9a1582ad7f8f180e 9c84692a999d40fda87c27d88d644dba X 2602d589d0ff479f9a1582ad7f8f180e--9c84692a999d40fda87c27d88d644dba 9c84692a999d40fda87c27d88d644dba--ab52f53a98644a6b9d3fa8ea73615b64 64062f2e64b64e18bfedee50d8961798 9c84692a999d40fda87c27d88d644dba--64062f2e64b64e18bfedee50d8961798 37fc55742ed74006b119cb27e57e8831 64062f2e64b64e18bfedee50d8961798--37fc55742ed74006b119cb27e57e8831 8f01f5b6d9fe483888ab9761750a1949 37fc55742ed74006b119cb27e57e8831--8f01f5b6d9fe483888ab9761750a1949 8a84c690bdaa4d02b881177c36e47c34 8f01f5b6d9fe483888ab9761750a1949--8a84c690bdaa4d02b881177c36e47c34 7bf8fba2ff3d49069094ad9cc47823a4 8a84c690bdaa4d02b881177c36e47c34--7bf8fba2ff3d49069094ad9cc47823a4 7f574e85b6fe487a97e4e4f082dee757 7bf8fba2ff3d49069094ad9cc47823a4--7f574e85b6fe487a97e4e4f082dee757 ad79e61a695842f788a4fb6ae48b4739 7f574e85b6fe487a97e4e4f082dee757--ad79e61a695842f788a4fb6ae48b4739 8bf3650070ad4a89b003441716489e0e ad79e61a695842f788a4fb6ae48b4739--8bf3650070ad4a89b003441716489e0e f6c856f1905a48118c396f79cbf779c6 8bf3650070ad4a89b003441716489e0e--f6c856f1905a48118c396f79cbf779c6 7f7f5130de03493ab50c6c18c8ff2697 f6c856f1905a48118c396f79cbf779c6--7f7f5130de03493ab50c6c18c8ff2697 6364dafbb0d84ce296e98e6cf54ddd0b 7f7f5130de03493ab50c6c18c8ff2697--6364dafbb0d84ce296e98e6cf54ddd0b acb8c73d144944bd8595228e4499a12b 6364dafbb0d84ce296e98e6cf54ddd0b--acb8c73d144944bd8595228e4499a12b 869d10af89264265a0ca98069cd3b4b8 acb8c73d144944bd8595228e4499a12b--869d10af89264265a0ca98069cd3b4b8 d1092707b06743f7bd1642a6347825a5 869d10af89264265a0ca98069cd3b4b8--d1092707b06743f7bd1642a6347825a5 4b2f8aa10c91443da2bdc4fefebeac27 d1092707b06743f7bd1642a6347825a5--4b2f8aa10c91443da2bdc4fefebeac27 4acb10d212064fa2b482327e31160c96 RX(b0) 4b2f8aa10c91443da2bdc4fefebeac27--4acb10d212064fa2b482327e31160c96 4acb10d212064fa2b482327e31160c96--7cdd568295d44eb2ad0170f2806f4d63 e499b7283cb3473aa6b79e0335ba59cb 4f9fc6a41cf445f2b783c76ce60c81a4 H b8e20d617a954435aeac9154f08f2597--4f9fc6a41cf445f2b783c76ce60c81a4 fee8bb3d5810469684b8e5210d6b0043 3 ca91e67447b445978259748ae4b813a0 4f9fc6a41cf445f2b783c76ce60c81a4--ca91e67447b445978259748ae4b813a0 54b26105841946399b9d15a9cfa9d08b ca91e67447b445978259748ae4b813a0--54b26105841946399b9d15a9cfa9d08b 13b164d3a9964dafb8b01406d0975e08 54b26105841946399b9d15a9cfa9d08b--13b164d3a9964dafb8b01406d0975e08 80d52d2a1836448fb133129e73170a9d X 13b164d3a9964dafb8b01406d0975e08--80d52d2a1836448fb133129e73170a9d 80d52d2a1836448fb133129e73170a9d--d8c2021ccf96425bbdb56ef93c0d7584 d61ec579b3844669b914084beae478cb RZ(g0) 80d52d2a1836448fb133129e73170a9d--d61ec579b3844669b914084beae478cb 9394a7ece2f14e02a5b19dc3f6385a0f X d61ec579b3844669b914084beae478cb--9394a7ece2f14e02a5b19dc3f6385a0f 9394a7ece2f14e02a5b19dc3f6385a0f--2990b2cd89ac4355aa652d19c54b290d 37e3cdeffc294b44a37f93688340a3a9 9394a7ece2f14e02a5b19dc3f6385a0f--37e3cdeffc294b44a37f93688340a3a9 f7b8d9de03fb4063a90698e313858920 37e3cdeffc294b44a37f93688340a3a9--f7b8d9de03fb4063a90698e313858920 96dc606f14c84fa5b7e640147044a10e f7b8d9de03fb4063a90698e313858920--96dc606f14c84fa5b7e640147044a10e 601e4a3c92984ec68ae9961e861ee8f1 X 96dc606f14c84fa5b7e640147044a10e--601e4a3c92984ec68ae9961e861ee8f1 601e4a3c92984ec68ae9961e861ee8f1--ad79e61a695842f788a4fb6ae48b4739 334d249823d44668bb8bc1c6a7d9c5ba RZ(g0) 601e4a3c92984ec68ae9961e861ee8f1--334d249823d44668bb8bc1c6a7d9c5ba 660f906f25354b4eb3daa05b51653d18 X 334d249823d44668bb8bc1c6a7d9c5ba--660f906f25354b4eb3daa05b51653d18 660f906f25354b4eb3daa05b51653d18--f6c856f1905a48118c396f79cbf779c6 4f8b07c8a0ac461ea8a05367d0f4b69a 660f906f25354b4eb3daa05b51653d18--4f8b07c8a0ac461ea8a05367d0f4b69a 18e01b4226394cac891f2dad1c7c4216 4f8b07c8a0ac461ea8a05367d0f4b69a--18e01b4226394cac891f2dad1c7c4216 6c8f0363f03b453cae9742cd406eeae2 18e01b4226394cac891f2dad1c7c4216--6c8f0363f03b453cae9742cd406eeae2 7afece864a4a4d9da8afb8a48670b9bb 6c8f0363f03b453cae9742cd406eeae2--7afece864a4a4d9da8afb8a48670b9bb 22e90f9c922646ccbcd1d76b96c4002f 7afece864a4a4d9da8afb8a48670b9bb--22e90f9c922646ccbcd1d76b96c4002f 2cab8516c70943cfbe7338f263173030 22e90f9c922646ccbcd1d76b96c4002f--2cab8516c70943cfbe7338f263173030 505db768fac54a27bffa1a3f15f497c9 RX(b0) 2cab8516c70943cfbe7338f263173030--505db768fac54a27bffa1a3f15f497c9 505db768fac54a27bffa1a3f15f497c9--e499b7283cb3473aa6b79e0335ba59cb 296dad9afc424f07b96bae3c703260c0 d99904f5bdf047bf9454de093bfa1ee8 H fee8bb3d5810469684b8e5210d6b0043--d99904f5bdf047bf9454de093bfa1ee8 a0425cb739254f738c3c1164f9bf1b3f d99904f5bdf047bf9454de093bfa1ee8--a0425cb739254f738c3c1164f9bf1b3f 649187f1ef4248d59ea75c6e465d79df a0425cb739254f738c3c1164f9bf1b3f--649187f1ef4248d59ea75c6e465d79df 6b2b31d80b4f49db9a982e5216aa05a1 649187f1ef4248d59ea75c6e465d79df--6b2b31d80b4f49db9a982e5216aa05a1 17a6f77a4d65438bb0b5a1faa3ab3b87 6b2b31d80b4f49db9a982e5216aa05a1--17a6f77a4d65438bb0b5a1faa3ab3b87 c3b849da53b849daaa95197b546be546 17a6f77a4d65438bb0b5a1faa3ab3b87--c3b849da53b849daaa95197b546be546 7aa784ed83b345dfb3ad432c161baea9 c3b849da53b849daaa95197b546be546--7aa784ed83b345dfb3ad432c161baea9 702cbf81ece24c65843ba90590cf6fef X 7aa784ed83b345dfb3ad432c161baea9--702cbf81ece24c65843ba90590cf6fef 702cbf81ece24c65843ba90590cf6fef--558857fa7db94b4ab620ac20b6f0b4ff 04406f89252141f1914aa066c5a031db RZ(g0) 702cbf81ece24c65843ba90590cf6fef--04406f89252141f1914aa066c5a031db 3e1d05088ed24f56b2d0a55efabedc17 X 04406f89252141f1914aa066c5a031db--3e1d05088ed24f56b2d0a55efabedc17 3e1d05088ed24f56b2d0a55efabedc17--5aaa5bd6060149f49ba12f14f8214c74 4f588bd6bcdb4a92a1c4a6ac0622a5cc 3e1d05088ed24f56b2d0a55efabedc17--4f588bd6bcdb4a92a1c4a6ac0622a5cc 0c9920f390bf4b73a6689fc894c4e58d 4f588bd6bcdb4a92a1c4a6ac0622a5cc--0c9920f390bf4b73a6689fc894c4e58d 7c6e2d812bc34441b3ce4a12f278720d 0c9920f390bf4b73a6689fc894c4e58d--7c6e2d812bc34441b3ce4a12f278720d 1e26069429ac4bc480144c8f24148aae X 7c6e2d812bc34441b3ce4a12f278720d--1e26069429ac4bc480144c8f24148aae 1e26069429ac4bc480144c8f24148aae--7f7f5130de03493ab50c6c18c8ff2697 8e4861925d1742d1b6b4723af8e25856 RZ(g0) 1e26069429ac4bc480144c8f24148aae--8e4861925d1742d1b6b4723af8e25856 8e004e054cca455d9105df7d026dca9a X 8e4861925d1742d1b6b4723af8e25856--8e004e054cca455d9105df7d026dca9a 8e004e054cca455d9105df7d026dca9a--acb8c73d144944bd8595228e4499a12b 20779a748c424d4f967e36c0f61f9485 X 8e004e054cca455d9105df7d026dca9a--20779a748c424d4f967e36c0f61f9485 20779a748c424d4f967e36c0f61f9485--7afece864a4a4d9da8afb8a48670b9bb 1c6911628abf4190a3d9dd9b85ffd9b7 RZ(g0) 20779a748c424d4f967e36c0f61f9485--1c6911628abf4190a3d9dd9b85ffd9b7 b96cabc4c308496ead57e95a7fc98252 X 1c6911628abf4190a3d9dd9b85ffd9b7--b96cabc4c308496ead57e95a7fc98252 b96cabc4c308496ead57e95a7fc98252--2cab8516c70943cfbe7338f263173030 3d4912774ac54526915ba0676600eba1 RX(b0) b96cabc4c308496ead57e95a7fc98252--3d4912774ac54526915ba0676600eba1 3d4912774ac54526915ba0676600eba1--296dad9afc424f07b96bae3c703260c0

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

References


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