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)
2025-01-08T18:02:40.374133 image/svg+xml Matplotlib v3.10.0, 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_d1dddc5b361f4a90ae386f9d0ff7e2f7 mixing cluster_bfa4e2e4cd0a413eaf6d56c9029a7b10 cost fb85874d57714caaae40342f43781887 0 dfd11c6f83ed48af96e35913bbed21e4 H fb85874d57714caaae40342f43781887--dfd11c6f83ed48af96e35913bbed21e4 d6c32e4abef941078dc2376bf8f8cf1d 1 6bd4764b82d048c1a8dd05648a3105d0 dfd11c6f83ed48af96e35913bbed21e4--6bd4764b82d048c1a8dd05648a3105d0 51bcbfcc31f64b7ba7e845e43f4ccb97 6bd4764b82d048c1a8dd05648a3105d0--51bcbfcc31f64b7ba7e845e43f4ccb97 a477119d58e8470991067e68d22ef87c 51bcbfcc31f64b7ba7e845e43f4ccb97--a477119d58e8470991067e68d22ef87c ffc23220e7914254b082787e5dd64066 a477119d58e8470991067e68d22ef87c--ffc23220e7914254b082787e5dd64066 0d92803beb9643e3b7ef7431b0455ffc ffc23220e7914254b082787e5dd64066--0d92803beb9643e3b7ef7431b0455ffc 0044241a82f24f08a6e6bcfd03d7659c 0d92803beb9643e3b7ef7431b0455ffc--0044241a82f24f08a6e6bcfd03d7659c ad9d45b352bf4d17b65e15b98696867f 0044241a82f24f08a6e6bcfd03d7659c--ad9d45b352bf4d17b65e15b98696867f 3a21a3a7b7cd41ccabe87983364cde3c ad9d45b352bf4d17b65e15b98696867f--3a21a3a7b7cd41ccabe87983364cde3c 61d8503497584e5ea19302ff8188fde5 3a21a3a7b7cd41ccabe87983364cde3c--61d8503497584e5ea19302ff8188fde5 8d790c410c6e4c3eb7ce2755f3668d9a 61d8503497584e5ea19302ff8188fde5--8d790c410c6e4c3eb7ce2755f3668d9a 61048834955243529c2b612c679eefc3 8d790c410c6e4c3eb7ce2755f3668d9a--61048834955243529c2b612c679eefc3 d5eb5cab4c8242e1a924158d4b3e4fb3 61048834955243529c2b612c679eefc3--d5eb5cab4c8242e1a924158d4b3e4fb3 26cb4db01cba465cb288f9071db708c8 d5eb5cab4c8242e1a924158d4b3e4fb3--26cb4db01cba465cb288f9071db708c8 5a83d8884a9c4e768910d107047f88ad 26cb4db01cba465cb288f9071db708c8--5a83d8884a9c4e768910d107047f88ad 52dd79e2bce14789b4894dad1948cf2b 5a83d8884a9c4e768910d107047f88ad--52dd79e2bce14789b4894dad1948cf2b 31b2b523ceaf468ab33f6266c7f46c1a 52dd79e2bce14789b4894dad1948cf2b--31b2b523ceaf468ab33f6266c7f46c1a e8710b5769f24bc381fee20c98d25454 31b2b523ceaf468ab33f6266c7f46c1a--e8710b5769f24bc381fee20c98d25454 14ca4700c9b94c44abaf1f680b6fb251 e8710b5769f24bc381fee20c98d25454--14ca4700c9b94c44abaf1f680b6fb251 f941dcd023094d42a1bb055ec69a0082 RX(b0) 14ca4700c9b94c44abaf1f680b6fb251--f941dcd023094d42a1bb055ec69a0082 ec45cb003bae470f90d98dde52fe537e f941dcd023094d42a1bb055ec69a0082--ec45cb003bae470f90d98dde52fe537e fd6a7d4cbed7438b990055249a6c2d3c 98e0534a2a8145f782a9fd96da463c4d H d6c32e4abef941078dc2376bf8f8cf1d--98e0534a2a8145f782a9fd96da463c4d 37de1221f59c4844acb11023b13ca732 2 de0acc0806214014b566a78840ae2eb7 X 98e0534a2a8145f782a9fd96da463c4d--de0acc0806214014b566a78840ae2eb7 de0acc0806214014b566a78840ae2eb7--6bd4764b82d048c1a8dd05648a3105d0 ec60b8c465fe4cac921ef6ed7a086251 RZ(g0) de0acc0806214014b566a78840ae2eb7--ec60b8c465fe4cac921ef6ed7a086251 5ac530d6dfe241fb81fbdfe21f5c155d X ec60b8c465fe4cac921ef6ed7a086251--5ac530d6dfe241fb81fbdfe21f5c155d 5ac530d6dfe241fb81fbdfe21f5c155d--a477119d58e8470991067e68d22ef87c 23415c4d8fcd4b428976c7c7ca881a05 5ac530d6dfe241fb81fbdfe21f5c155d--23415c4d8fcd4b428976c7c7ca881a05 76d70c1943df4b74a93e0c3074c51965 23415c4d8fcd4b428976c7c7ca881a05--76d70c1943df4b74a93e0c3074c51965 3051a67171fd4aab9e27ca520452fe54 76d70c1943df4b74a93e0c3074c51965--3051a67171fd4aab9e27ca520452fe54 7023127519644116b9a72ac5bcacac45 3051a67171fd4aab9e27ca520452fe54--7023127519644116b9a72ac5bcacac45 e660eaaf16c348939f32d9d39858db0e 7023127519644116b9a72ac5bcacac45--e660eaaf16c348939f32d9d39858db0e 31a8645df6044005b8370be8dcdca3bb e660eaaf16c348939f32d9d39858db0e--31a8645df6044005b8370be8dcdca3bb 29199d6062484e4898abbf7e8b962d01 31a8645df6044005b8370be8dcdca3bb--29199d6062484e4898abbf7e8b962d01 b50f23a023be4264aafe3e36230519c7 29199d6062484e4898abbf7e8b962d01--b50f23a023be4264aafe3e36230519c7 598eaff68c0c4a8fbaa4beb084f3690c b50f23a023be4264aafe3e36230519c7--598eaff68c0c4a8fbaa4beb084f3690c 688f4c7b4f5148aebc6603c6eeaa3bf8 598eaff68c0c4a8fbaa4beb084f3690c--688f4c7b4f5148aebc6603c6eeaa3bf8 58b45214649f45d1a3ec6cc3c7dc5632 688f4c7b4f5148aebc6603c6eeaa3bf8--58b45214649f45d1a3ec6cc3c7dc5632 4e396eb0ff89443a8e250073c8349d60 58b45214649f45d1a3ec6cc3c7dc5632--4e396eb0ff89443a8e250073c8349d60 e0f0fd2bacf847caba671e048d566c6a 4e396eb0ff89443a8e250073c8349d60--e0f0fd2bacf847caba671e048d566c6a 17ccfc3fcd144129b3b696609d05ce81 e0f0fd2bacf847caba671e048d566c6a--17ccfc3fcd144129b3b696609d05ce81 331c4b0b4ed6440793d973c8bbdc2028 17ccfc3fcd144129b3b696609d05ce81--331c4b0b4ed6440793d973c8bbdc2028 36bbfe72f5394d41a49ab1392b6289cd RX(b0) 331c4b0b4ed6440793d973c8bbdc2028--36bbfe72f5394d41a49ab1392b6289cd 36bbfe72f5394d41a49ab1392b6289cd--fd6a7d4cbed7438b990055249a6c2d3c fbd1886237b741739b7b62725a06636c 5ceb82511ed14d03928d266dd87a6ec3 H 37de1221f59c4844acb11023b13ca732--5ceb82511ed14d03928d266dd87a6ec3 90e7351282bd4c95916cf0e9f894e297 3 4d8755da25244e5f9871941a945d44c5 5ceb82511ed14d03928d266dd87a6ec3--4d8755da25244e5f9871941a945d44c5 5125947faca14b42944a5d8a3cd009d6 4d8755da25244e5f9871941a945d44c5--5125947faca14b42944a5d8a3cd009d6 15662d81b2be484c8369f3a5eb80b714 5125947faca14b42944a5d8a3cd009d6--15662d81b2be484c8369f3a5eb80b714 4d8f948ccf1e4f82943724ea54445979 X 15662d81b2be484c8369f3a5eb80b714--4d8f948ccf1e4f82943724ea54445979 4d8f948ccf1e4f82943724ea54445979--ffc23220e7914254b082787e5dd64066 eb1d82221b684205a1848cc52756e0e9 RZ(g0) 4d8f948ccf1e4f82943724ea54445979--eb1d82221b684205a1848cc52756e0e9 07267370217a488b8148efc1267ff13c X eb1d82221b684205a1848cc52756e0e9--07267370217a488b8148efc1267ff13c 07267370217a488b8148efc1267ff13c--0044241a82f24f08a6e6bcfd03d7659c 5d5546c940ab44b8bb0fd10bb518608e 07267370217a488b8148efc1267ff13c--5d5546c940ab44b8bb0fd10bb518608e ab34bc95e9b6429eabd2c72c916d6d90 5d5546c940ab44b8bb0fd10bb518608e--ab34bc95e9b6429eabd2c72c916d6d90 031771fc747a4e84bde04c1cdd10f201 ab34bc95e9b6429eabd2c72c916d6d90--031771fc747a4e84bde04c1cdd10f201 393553d19fa141f2ae332dbdfc888b19 X 031771fc747a4e84bde04c1cdd10f201--393553d19fa141f2ae332dbdfc888b19 393553d19fa141f2ae332dbdfc888b19--29199d6062484e4898abbf7e8b962d01 c04bf5075f894e5a8a692d1c6d26c1a4 RZ(g0) 393553d19fa141f2ae332dbdfc888b19--c04bf5075f894e5a8a692d1c6d26c1a4 2c34b92539a94acdbccf55c38d1daaff X c04bf5075f894e5a8a692d1c6d26c1a4--2c34b92539a94acdbccf55c38d1daaff 2c34b92539a94acdbccf55c38d1daaff--598eaff68c0c4a8fbaa4beb084f3690c fd89fc36095644f0a9063e717832cea0 2c34b92539a94acdbccf55c38d1daaff--fd89fc36095644f0a9063e717832cea0 b3abc391bc014fa594b24c135c4074e5 fd89fc36095644f0a9063e717832cea0--b3abc391bc014fa594b24c135c4074e5 07f6160b52be4c58a5c5bd6e4b53ee5c b3abc391bc014fa594b24c135c4074e5--07f6160b52be4c58a5c5bd6e4b53ee5c f5e737fccd8440c0a221c3fd8af08894 07f6160b52be4c58a5c5bd6e4b53ee5c--f5e737fccd8440c0a221c3fd8af08894 8780ec934ea04d6b84fecba6445ed563 f5e737fccd8440c0a221c3fd8af08894--8780ec934ea04d6b84fecba6445ed563 f3409bbffcbc4ef08adf52a0016179c1 8780ec934ea04d6b84fecba6445ed563--f3409bbffcbc4ef08adf52a0016179c1 5c826c313c11470181069a0fea8b65c6 RX(b0) f3409bbffcbc4ef08adf52a0016179c1--5c826c313c11470181069a0fea8b65c6 5c826c313c11470181069a0fea8b65c6--fbd1886237b741739b7b62725a06636c 42192d76dfb24efba156a7d1b25552ed b7b2c910fbb645bf8162302cdbcdd20b H 90e7351282bd4c95916cf0e9f894e297--b7b2c910fbb645bf8162302cdbcdd20b daf1e7c7a8574c6aa6cc1725a92f58b7 b7b2c910fbb645bf8162302cdbcdd20b--daf1e7c7a8574c6aa6cc1725a92f58b7 2fff1b753b714ebe9325e76dc1b943c2 daf1e7c7a8574c6aa6cc1725a92f58b7--2fff1b753b714ebe9325e76dc1b943c2 382f7fc71b8247ccbf805df422c3b000 2fff1b753b714ebe9325e76dc1b943c2--382f7fc71b8247ccbf805df422c3b000 c9e53f0d9c224406b8ca80d5c9ac102d 382f7fc71b8247ccbf805df422c3b000--c9e53f0d9c224406b8ca80d5c9ac102d 422c2e8cb52a4504b2d151c368446c31 c9e53f0d9c224406b8ca80d5c9ac102d--422c2e8cb52a4504b2d151c368446c31 06328a0a0c554d23a9c6ed911d7eb147 422c2e8cb52a4504b2d151c368446c31--06328a0a0c554d23a9c6ed911d7eb147 1f94406c52b74e49a5af3f07b50bf015 X 06328a0a0c554d23a9c6ed911d7eb147--1f94406c52b74e49a5af3f07b50bf015 1f94406c52b74e49a5af3f07b50bf015--ad9d45b352bf4d17b65e15b98696867f 653be5026b0f4461995019dd498aae29 RZ(g0) 1f94406c52b74e49a5af3f07b50bf015--653be5026b0f4461995019dd498aae29 c5601ca8b26a48dfafd5b78fc90d2c29 X 653be5026b0f4461995019dd498aae29--c5601ca8b26a48dfafd5b78fc90d2c29 c5601ca8b26a48dfafd5b78fc90d2c29--61d8503497584e5ea19302ff8188fde5 00914024600c4cf09c222d0b94fabb49 c5601ca8b26a48dfafd5b78fc90d2c29--00914024600c4cf09c222d0b94fabb49 eca5629a90d645b78d43aac89303aaa0 00914024600c4cf09c222d0b94fabb49--eca5629a90d645b78d43aac89303aaa0 b1a5277f36dc41c99fd3b86806eafa2c eca5629a90d645b78d43aac89303aaa0--b1a5277f36dc41c99fd3b86806eafa2c 26acb97d25f94754baa38dab2b67e4d1 X b1a5277f36dc41c99fd3b86806eafa2c--26acb97d25f94754baa38dab2b67e4d1 26acb97d25f94754baa38dab2b67e4d1--688f4c7b4f5148aebc6603c6eeaa3bf8 f74aa89a92744fa78b2b613ed65c65a7 RZ(g0) 26acb97d25f94754baa38dab2b67e4d1--f74aa89a92744fa78b2b613ed65c65a7 7f4629ed71ff4e74bfe02cf9370fb105 X f74aa89a92744fa78b2b613ed65c65a7--7f4629ed71ff4e74bfe02cf9370fb105 7f4629ed71ff4e74bfe02cf9370fb105--4e396eb0ff89443a8e250073c8349d60 c6e03714bb8a42e89ed54a1e072e0940 X 7f4629ed71ff4e74bfe02cf9370fb105--c6e03714bb8a42e89ed54a1e072e0940 c6e03714bb8a42e89ed54a1e072e0940--f5e737fccd8440c0a221c3fd8af08894 c8a8a9ffd8404447aa864e2892c5bc2c RZ(g0) c6e03714bb8a42e89ed54a1e072e0940--c8a8a9ffd8404447aa864e2892c5bc2c 5d19c4c0ff98448ba2b2b09dd743c74e X c8a8a9ffd8404447aa864e2892c5bc2c--5d19c4c0ff98448ba2b2b09dd743c74e 5d19c4c0ff98448ba2b2b09dd743c74e--f3409bbffcbc4ef08adf52a0016179c1 8c86f35fa89441efb4e7250ddabf1d60 RX(b0) 5d19c4c0ff98448ba2b2b09dd743c74e--8c86f35fa89441efb4e7250ddabf1d60 8c86f35fa89441efb4e7250ddabf1d60--42192d76dfb24efba156a7d1b25552ed

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 2025-01-08T18:02:44.494318 image/svg+xml Matplotlib v3.10.0, https://matplotlib.org/

References


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