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-19T12:07:10.043399 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_1c5728d67ef248e2a3bc9a7a93edf793 mixing cluster_0a5bf85d029946b7a033bc23d988b243 cost 941a68ec124b405ca771f5972204f8ac 0 72521444543947a2bf3b8e826dac3e47 H 941a68ec124b405ca771f5972204f8ac--72521444543947a2bf3b8e826dac3e47 da840377c0bc417798a8fe99b1fac79b 1 c276cec8d7c94449aa3650d8ff2b27e4 72521444543947a2bf3b8e826dac3e47--c276cec8d7c94449aa3650d8ff2b27e4 866d3bc730b845ce82d48717403baac0 c276cec8d7c94449aa3650d8ff2b27e4--866d3bc730b845ce82d48717403baac0 012ff865f10e4c9cb238fa5ce10a761e 866d3bc730b845ce82d48717403baac0--012ff865f10e4c9cb238fa5ce10a761e 61fcc77748b24334bab918a5d55f89ee 012ff865f10e4c9cb238fa5ce10a761e--61fcc77748b24334bab918a5d55f89ee 1bd647adc6cf44cebe94b1ca9305f189 61fcc77748b24334bab918a5d55f89ee--1bd647adc6cf44cebe94b1ca9305f189 cef07be291c44cb99dd4afe482f7bf19 1bd647adc6cf44cebe94b1ca9305f189--cef07be291c44cb99dd4afe482f7bf19 9d04ec1eb98f4d67af253dae2eaf0546 cef07be291c44cb99dd4afe482f7bf19--9d04ec1eb98f4d67af253dae2eaf0546 28efe74d305d4f3d81c7212827cf765b 9d04ec1eb98f4d67af253dae2eaf0546--28efe74d305d4f3d81c7212827cf765b 5321b0175bba471b821897a9c9703eff 28efe74d305d4f3d81c7212827cf765b--5321b0175bba471b821897a9c9703eff 4b8173606338435fbeb0700f67c2fd1a 5321b0175bba471b821897a9c9703eff--4b8173606338435fbeb0700f67c2fd1a c6f145b2dc79402181975e1c51f2840e 4b8173606338435fbeb0700f67c2fd1a--c6f145b2dc79402181975e1c51f2840e 755aba395d164181a26264d80224622d c6f145b2dc79402181975e1c51f2840e--755aba395d164181a26264d80224622d 980a90b1ab884306a77bfe7a67ea73f1 755aba395d164181a26264d80224622d--980a90b1ab884306a77bfe7a67ea73f1 4edc68a0dc934f3daedcfd2d135575a3 980a90b1ab884306a77bfe7a67ea73f1--4edc68a0dc934f3daedcfd2d135575a3 9054f43da60d40a9a8598e82ba559649 4edc68a0dc934f3daedcfd2d135575a3--9054f43da60d40a9a8598e82ba559649 fd42c6e8f28845e9ac5d2228fa84fc67 9054f43da60d40a9a8598e82ba559649--fd42c6e8f28845e9ac5d2228fa84fc67 9a96e6a8f3604de8a3d97d769aa0b497 fd42c6e8f28845e9ac5d2228fa84fc67--9a96e6a8f3604de8a3d97d769aa0b497 0346e06e877c4b49858dc8c536fbd50c 9a96e6a8f3604de8a3d97d769aa0b497--0346e06e877c4b49858dc8c536fbd50c 2e4a04db96ec418c941c5623d0299d2e RX(b0) 0346e06e877c4b49858dc8c536fbd50c--2e4a04db96ec418c941c5623d0299d2e c813096d6f1c41dfa79dfa0b4c1da691 2e4a04db96ec418c941c5623d0299d2e--c813096d6f1c41dfa79dfa0b4c1da691 0a8fd9fbde6b4303b601524021a6cdef d513376f6e48401da4b2d8a28603f6d5 H da840377c0bc417798a8fe99b1fac79b--d513376f6e48401da4b2d8a28603f6d5 15ec0898b7864c459a634a86689285f3 2 d98cc59ea3ca4a14be547bcc53023467 X d513376f6e48401da4b2d8a28603f6d5--d98cc59ea3ca4a14be547bcc53023467 d98cc59ea3ca4a14be547bcc53023467--c276cec8d7c94449aa3650d8ff2b27e4 1d27d4aab117438385840d0dae93739c RZ(g0) d98cc59ea3ca4a14be547bcc53023467--1d27d4aab117438385840d0dae93739c 5e92ae46f3be47388443f6cc93933149 X 1d27d4aab117438385840d0dae93739c--5e92ae46f3be47388443f6cc93933149 5e92ae46f3be47388443f6cc93933149--012ff865f10e4c9cb238fa5ce10a761e e466b44932ab4dc18e5feb435d1a0920 5e92ae46f3be47388443f6cc93933149--e466b44932ab4dc18e5feb435d1a0920 0d2a8d0653174d328aae0737add0a550 e466b44932ab4dc18e5feb435d1a0920--0d2a8d0653174d328aae0737add0a550 5414284af8a641b0b64445c5c6fd03c3 0d2a8d0653174d328aae0737add0a550--5414284af8a641b0b64445c5c6fd03c3 9b1fe6788ca6461c9d66d789143172e0 5414284af8a641b0b64445c5c6fd03c3--9b1fe6788ca6461c9d66d789143172e0 abdf2352519c4a9899a2a1eb8faff1ee 9b1fe6788ca6461c9d66d789143172e0--abdf2352519c4a9899a2a1eb8faff1ee 6d73c77b3eb74cf794520f02a944bfba abdf2352519c4a9899a2a1eb8faff1ee--6d73c77b3eb74cf794520f02a944bfba b8b5475f15384fabb660009c61836d76 6d73c77b3eb74cf794520f02a944bfba--b8b5475f15384fabb660009c61836d76 dc0e18af6e1246618e3e1273709ee3d4 b8b5475f15384fabb660009c61836d76--dc0e18af6e1246618e3e1273709ee3d4 cccfce4ca2e94d1195b29a5750ac5a9a dc0e18af6e1246618e3e1273709ee3d4--cccfce4ca2e94d1195b29a5750ac5a9a aa09727dc53048c096838024aaa2cad1 cccfce4ca2e94d1195b29a5750ac5a9a--aa09727dc53048c096838024aaa2cad1 34267c5559dd47118a049b2035979224 aa09727dc53048c096838024aaa2cad1--34267c5559dd47118a049b2035979224 a46075250f804a21b2f5425e75c8aa19 34267c5559dd47118a049b2035979224--a46075250f804a21b2f5425e75c8aa19 2384c311dbfa4487b61a96028d6921e6 a46075250f804a21b2f5425e75c8aa19--2384c311dbfa4487b61a96028d6921e6 e2211ef5d1f0470192db62c0d5aa212f 2384c311dbfa4487b61a96028d6921e6--e2211ef5d1f0470192db62c0d5aa212f c981242e28614ac89fbbac8400a346d8 e2211ef5d1f0470192db62c0d5aa212f--c981242e28614ac89fbbac8400a346d8 fd2b1a8344064bb2a1b031628fd03abc RX(b0) c981242e28614ac89fbbac8400a346d8--fd2b1a8344064bb2a1b031628fd03abc fd2b1a8344064bb2a1b031628fd03abc--0a8fd9fbde6b4303b601524021a6cdef 266b7f958488447d859a7030a8b13e37 fbe1fd748a504f4eb82a89c71cf7e1f3 H 15ec0898b7864c459a634a86689285f3--fbe1fd748a504f4eb82a89c71cf7e1f3 6dcbc0070fe04d28a135022e3a7a5da0 3 bdc503ef87ed4652a7f454eaf0062cd9 fbe1fd748a504f4eb82a89c71cf7e1f3--bdc503ef87ed4652a7f454eaf0062cd9 6375677c99344d30a98c102e912f5cce bdc503ef87ed4652a7f454eaf0062cd9--6375677c99344d30a98c102e912f5cce d0c20941e51d477b8913c24914eb9bc6 6375677c99344d30a98c102e912f5cce--d0c20941e51d477b8913c24914eb9bc6 17df3b88299a477fa80c688e2d69e54d X d0c20941e51d477b8913c24914eb9bc6--17df3b88299a477fa80c688e2d69e54d 17df3b88299a477fa80c688e2d69e54d--61fcc77748b24334bab918a5d55f89ee b9399c8d6a434e9180f2e2c77e1e3987 RZ(g0) 17df3b88299a477fa80c688e2d69e54d--b9399c8d6a434e9180f2e2c77e1e3987 46aa200959af4be080e3676b47a815fb X b9399c8d6a434e9180f2e2c77e1e3987--46aa200959af4be080e3676b47a815fb 46aa200959af4be080e3676b47a815fb--cef07be291c44cb99dd4afe482f7bf19 a55691513725419db1210a802ef06792 46aa200959af4be080e3676b47a815fb--a55691513725419db1210a802ef06792 8885d44a36e5405588d783e77cc7e8ad a55691513725419db1210a802ef06792--8885d44a36e5405588d783e77cc7e8ad 98531f7d5bec480783136022b5196998 8885d44a36e5405588d783e77cc7e8ad--98531f7d5bec480783136022b5196998 fc7636db8f76422f98dca212c3827b96 X 98531f7d5bec480783136022b5196998--fc7636db8f76422f98dca212c3827b96 fc7636db8f76422f98dca212c3827b96--b8b5475f15384fabb660009c61836d76 f36fc334a1724a329ad9041048b122ab RZ(g0) fc7636db8f76422f98dca212c3827b96--f36fc334a1724a329ad9041048b122ab ab9d5b057ded4fc9bd9f774560853f5f X f36fc334a1724a329ad9041048b122ab--ab9d5b057ded4fc9bd9f774560853f5f ab9d5b057ded4fc9bd9f774560853f5f--cccfce4ca2e94d1195b29a5750ac5a9a 6dca83b061e944b091d7cc0e51ae467c ab9d5b057ded4fc9bd9f774560853f5f--6dca83b061e944b091d7cc0e51ae467c 6162efba7d7043389860a1efa9dc1c40 6dca83b061e944b091d7cc0e51ae467c--6162efba7d7043389860a1efa9dc1c40 8e8c74a270394664a6cf89134c739668 6162efba7d7043389860a1efa9dc1c40--8e8c74a270394664a6cf89134c739668 6b2c8af5adf74aa4adc9f2deefe150d9 8e8c74a270394664a6cf89134c739668--6b2c8af5adf74aa4adc9f2deefe150d9 e8c14e6225444863a8f0b9cdea0f7dea 6b2c8af5adf74aa4adc9f2deefe150d9--e8c14e6225444863a8f0b9cdea0f7dea 5789331a1f4447cf81692c022c417ad0 e8c14e6225444863a8f0b9cdea0f7dea--5789331a1f4447cf81692c022c417ad0 19105f3302dd4a75998ea29f02032d0e RX(b0) 5789331a1f4447cf81692c022c417ad0--19105f3302dd4a75998ea29f02032d0e 19105f3302dd4a75998ea29f02032d0e--266b7f958488447d859a7030a8b13e37 7f08d9f934304aedbd5419794aa5e011 1f720dff852a46a786a45c256c3bba80 H 6dcbc0070fe04d28a135022e3a7a5da0--1f720dff852a46a786a45c256c3bba80 9996aada3e434422bf83f4f6e5c395a2 1f720dff852a46a786a45c256c3bba80--9996aada3e434422bf83f4f6e5c395a2 04e5fef3f0324f64a19e68f887d0c8d6 9996aada3e434422bf83f4f6e5c395a2--04e5fef3f0324f64a19e68f887d0c8d6 3594152e91ae41fc9d2bf54db327765f 04e5fef3f0324f64a19e68f887d0c8d6--3594152e91ae41fc9d2bf54db327765f 21ed2ce79e32416ba142a91ac5b3197a 3594152e91ae41fc9d2bf54db327765f--21ed2ce79e32416ba142a91ac5b3197a 1af5187a373e4a0a985c848ed85be945 21ed2ce79e32416ba142a91ac5b3197a--1af5187a373e4a0a985c848ed85be945 2ce346bca6134d7b9a9cbfe862b219f5 1af5187a373e4a0a985c848ed85be945--2ce346bca6134d7b9a9cbfe862b219f5 851c1dfc4b284578b8eb99e29af168fa X 2ce346bca6134d7b9a9cbfe862b219f5--851c1dfc4b284578b8eb99e29af168fa 851c1dfc4b284578b8eb99e29af168fa--9d04ec1eb98f4d67af253dae2eaf0546 bbb7a67d3a474865954738b5c393ba1d RZ(g0) 851c1dfc4b284578b8eb99e29af168fa--bbb7a67d3a474865954738b5c393ba1d 2b7d83d44fb84ad89477876667f21949 X bbb7a67d3a474865954738b5c393ba1d--2b7d83d44fb84ad89477876667f21949 2b7d83d44fb84ad89477876667f21949--5321b0175bba471b821897a9c9703eff 6b341c3d761d46d0a0a837c7f85361c3 2b7d83d44fb84ad89477876667f21949--6b341c3d761d46d0a0a837c7f85361c3 7983bad8b3a642b892a9e836bd2b1ede 6b341c3d761d46d0a0a837c7f85361c3--7983bad8b3a642b892a9e836bd2b1ede ab0304847fde4c739c4fb648abfaac9f 7983bad8b3a642b892a9e836bd2b1ede--ab0304847fde4c739c4fb648abfaac9f c105edc3da9048c59b38ed5eb45532f3 X ab0304847fde4c739c4fb648abfaac9f--c105edc3da9048c59b38ed5eb45532f3 c105edc3da9048c59b38ed5eb45532f3--aa09727dc53048c096838024aaa2cad1 7c2b9a9c5dad4d139a0ba3787001f6bb RZ(g0) c105edc3da9048c59b38ed5eb45532f3--7c2b9a9c5dad4d139a0ba3787001f6bb e2404fa932ea44c68e81a7fdb39b3122 X 7c2b9a9c5dad4d139a0ba3787001f6bb--e2404fa932ea44c68e81a7fdb39b3122 e2404fa932ea44c68e81a7fdb39b3122--a46075250f804a21b2f5425e75c8aa19 757c172071344e9baecc48631b39952c X e2404fa932ea44c68e81a7fdb39b3122--757c172071344e9baecc48631b39952c 757c172071344e9baecc48631b39952c--6b2c8af5adf74aa4adc9f2deefe150d9 b47b1394962445a48e2b7627b1bffa2f RZ(g0) 757c172071344e9baecc48631b39952c--b47b1394962445a48e2b7627b1bffa2f 08573ce912cf42d280b5b66cd16f63ea X b47b1394962445a48e2b7627b1bffa2f--08573ce912cf42d280b5b66cd16f63ea 08573ce912cf42d280b5b66cd16f63ea--5789331a1f4447cf81692c022c417ad0 8daa811b0ca348f18c27cb4a3467720a RX(b0) 08573ce912cf42d280b5b66cd16f63ea--8daa811b0ca348f18c27cb4a3467720a 8daa811b0ca348f18c27cb4a3467720a--7f08d9f934304aedbd5419794aa5e011

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-19T12:07:14.289377 image/svg+xml Matplotlib v3.7.5, https://matplotlib.org/

References


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