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-08-21T11:48:49.000829 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_d2b8b0979c7f4d56a518c711eefbe96c mixing cluster_a445123d38244525b9ebdfc30a9cd840 cost bc40783997d64d8bbc53f57e331480f3 0 857e32783db54d8bbaf2472ee7451fe9 H bc40783997d64d8bbc53f57e331480f3--857e32783db54d8bbaf2472ee7451fe9 adfe84af6b7145b597bf785397e6f1ef 1 c863396c5d70487ca47a5e323c6e6aa4 857e32783db54d8bbaf2472ee7451fe9--c863396c5d70487ca47a5e323c6e6aa4 5882d26f82aa494190c0e513a14a27fe c863396c5d70487ca47a5e323c6e6aa4--5882d26f82aa494190c0e513a14a27fe 71e98f8a882b437d9eaea855756f7aae 5882d26f82aa494190c0e513a14a27fe--71e98f8a882b437d9eaea855756f7aae ab615c7acc584f35999a173c85525245 71e98f8a882b437d9eaea855756f7aae--ab615c7acc584f35999a173c85525245 c8eb5252e405404fbb3b4c8d6f75f220 ab615c7acc584f35999a173c85525245--c8eb5252e405404fbb3b4c8d6f75f220 ba90d10f49274f8b9f4fe901a2b1f5a1 c8eb5252e405404fbb3b4c8d6f75f220--ba90d10f49274f8b9f4fe901a2b1f5a1 b296d38064b54a61b7ef28cbe25e832f ba90d10f49274f8b9f4fe901a2b1f5a1--b296d38064b54a61b7ef28cbe25e832f bedb475dece5458a83a19f3c9a56c3af b296d38064b54a61b7ef28cbe25e832f--bedb475dece5458a83a19f3c9a56c3af 85cd24cf6da74604a83ff1b683320cba bedb475dece5458a83a19f3c9a56c3af--85cd24cf6da74604a83ff1b683320cba 22d9c5195dba449a8d62bace9aa5c93e 85cd24cf6da74604a83ff1b683320cba--22d9c5195dba449a8d62bace9aa5c93e e5a89de7c1344162854544f3bda598d3 22d9c5195dba449a8d62bace9aa5c93e--e5a89de7c1344162854544f3bda598d3 6545e448ba1c47549cf68a3d781b1207 e5a89de7c1344162854544f3bda598d3--6545e448ba1c47549cf68a3d781b1207 11829ef07f1b4522bc65bff1c2495b2f 6545e448ba1c47549cf68a3d781b1207--11829ef07f1b4522bc65bff1c2495b2f c63b293218d04b8ab5c07bea499fdcaf 11829ef07f1b4522bc65bff1c2495b2f--c63b293218d04b8ab5c07bea499fdcaf 38b2305962e446b19217d16b03d91359 c63b293218d04b8ab5c07bea499fdcaf--38b2305962e446b19217d16b03d91359 09990e4ecb4241c499eb90f58610faf0 38b2305962e446b19217d16b03d91359--09990e4ecb4241c499eb90f58610faf0 0f8cd598532c4282a31a4364ec40a523 09990e4ecb4241c499eb90f58610faf0--0f8cd598532c4282a31a4364ec40a523 928bb54f2ccf4b888a3e4dd90182b728 0f8cd598532c4282a31a4364ec40a523--928bb54f2ccf4b888a3e4dd90182b728 102e1cd8a07d4e51bbb26ad608294386 RX(b0) 928bb54f2ccf4b888a3e4dd90182b728--102e1cd8a07d4e51bbb26ad608294386 eb6fb32cdd3a4ce1943bd0ec04e887eb 102e1cd8a07d4e51bbb26ad608294386--eb6fb32cdd3a4ce1943bd0ec04e887eb 0d14d2abccbe41d19c3419a8df69e85b bff1ba0a60f94703a0fa003fa45bfd3b H adfe84af6b7145b597bf785397e6f1ef--bff1ba0a60f94703a0fa003fa45bfd3b 06a5fd54e0a64de7a3ad4e35bb2cbd57 2 007ee71c7d8c4d8e98afd2c0e09d2c5f X bff1ba0a60f94703a0fa003fa45bfd3b--007ee71c7d8c4d8e98afd2c0e09d2c5f 007ee71c7d8c4d8e98afd2c0e09d2c5f--c863396c5d70487ca47a5e323c6e6aa4 dcb408eb31ab425ba1c6430a3691dec6 RZ(g0) 007ee71c7d8c4d8e98afd2c0e09d2c5f--dcb408eb31ab425ba1c6430a3691dec6 34c3ebb2e2154eb899ac96ed0ccc9ef7 X dcb408eb31ab425ba1c6430a3691dec6--34c3ebb2e2154eb899ac96ed0ccc9ef7 34c3ebb2e2154eb899ac96ed0ccc9ef7--71e98f8a882b437d9eaea855756f7aae 99255d887e8643a980422ae908a4b4a6 34c3ebb2e2154eb899ac96ed0ccc9ef7--99255d887e8643a980422ae908a4b4a6 a2a559cf503240c8a69150c7c1ea688b 99255d887e8643a980422ae908a4b4a6--a2a559cf503240c8a69150c7c1ea688b 34a018a16ea74d7085c38edb39b1533a a2a559cf503240c8a69150c7c1ea688b--34a018a16ea74d7085c38edb39b1533a dd86da6027a54ca79ce7adbeb053054c 34a018a16ea74d7085c38edb39b1533a--dd86da6027a54ca79ce7adbeb053054c 2b6bcb49080f427abaea96a2ba11c354 dd86da6027a54ca79ce7adbeb053054c--2b6bcb49080f427abaea96a2ba11c354 06862f9d1f5e4134a6e6e353dc829be5 2b6bcb49080f427abaea96a2ba11c354--06862f9d1f5e4134a6e6e353dc829be5 5f84f9d343f9404fb0af2024b06182b5 06862f9d1f5e4134a6e6e353dc829be5--5f84f9d343f9404fb0af2024b06182b5 55e639cd529848e18bb30705b34be4b5 5f84f9d343f9404fb0af2024b06182b5--55e639cd529848e18bb30705b34be4b5 fcb531a7577c48c295a8614d5b5d903b 55e639cd529848e18bb30705b34be4b5--fcb531a7577c48c295a8614d5b5d903b a7be82ec77fa491683516795ae8efb04 fcb531a7577c48c295a8614d5b5d903b--a7be82ec77fa491683516795ae8efb04 57f727b3599949bc9192394302511cb8 a7be82ec77fa491683516795ae8efb04--57f727b3599949bc9192394302511cb8 9bd05b50508d4562aa73a56c12b5fe74 57f727b3599949bc9192394302511cb8--9bd05b50508d4562aa73a56c12b5fe74 d1f5171ef58d4003ba0c93c624493598 9bd05b50508d4562aa73a56c12b5fe74--d1f5171ef58d4003ba0c93c624493598 f6250f3470504a5585662aac551907ba d1f5171ef58d4003ba0c93c624493598--f6250f3470504a5585662aac551907ba 54c047bda1fe4128ad219275906c21f9 f6250f3470504a5585662aac551907ba--54c047bda1fe4128ad219275906c21f9 a44e64d64dd044aab66f9d5820346d75 RX(b0) 54c047bda1fe4128ad219275906c21f9--a44e64d64dd044aab66f9d5820346d75 a44e64d64dd044aab66f9d5820346d75--0d14d2abccbe41d19c3419a8df69e85b d4ca6106f54c4c969aee8d4aec39952d d8bc3ad701b540fc823d37c94a4c6516 H 06a5fd54e0a64de7a3ad4e35bb2cbd57--d8bc3ad701b540fc823d37c94a4c6516 5f4f6fdd43184735bc91b4dd44239ee0 3 7817cd6b3af14b6d84f5c5ab28577213 d8bc3ad701b540fc823d37c94a4c6516--7817cd6b3af14b6d84f5c5ab28577213 561ff27b9f5544118838012ede178d4e 7817cd6b3af14b6d84f5c5ab28577213--561ff27b9f5544118838012ede178d4e 80518b928a5d45a9963ffeac28acdcd6 561ff27b9f5544118838012ede178d4e--80518b928a5d45a9963ffeac28acdcd6 dc6e45455d524baeb34ee8bd90af8a5c X 80518b928a5d45a9963ffeac28acdcd6--dc6e45455d524baeb34ee8bd90af8a5c dc6e45455d524baeb34ee8bd90af8a5c--ab615c7acc584f35999a173c85525245 e610ba85239f4f188778e1faf1fff7f9 RZ(g0) dc6e45455d524baeb34ee8bd90af8a5c--e610ba85239f4f188778e1faf1fff7f9 a91f3ee2e8814ee9bd093cf2d21ade5f X e610ba85239f4f188778e1faf1fff7f9--a91f3ee2e8814ee9bd093cf2d21ade5f a91f3ee2e8814ee9bd093cf2d21ade5f--ba90d10f49274f8b9f4fe901a2b1f5a1 02ae7a939e5c4c58abe40809bd23b4fa a91f3ee2e8814ee9bd093cf2d21ade5f--02ae7a939e5c4c58abe40809bd23b4fa d2f36dee3e034efc8321f4ff5b354e6e 02ae7a939e5c4c58abe40809bd23b4fa--d2f36dee3e034efc8321f4ff5b354e6e f9050270c33c49958790414d692cef6a d2f36dee3e034efc8321f4ff5b354e6e--f9050270c33c49958790414d692cef6a e75740f942ec48d0b1c9377f28f8ba36 X f9050270c33c49958790414d692cef6a--e75740f942ec48d0b1c9377f28f8ba36 e75740f942ec48d0b1c9377f28f8ba36--5f84f9d343f9404fb0af2024b06182b5 e3c719396d5f441eb72dcfd5f0d6ca18 RZ(g0) e75740f942ec48d0b1c9377f28f8ba36--e3c719396d5f441eb72dcfd5f0d6ca18 7b1d24a58d584276bf151997cd8902b3 X e3c719396d5f441eb72dcfd5f0d6ca18--7b1d24a58d584276bf151997cd8902b3 7b1d24a58d584276bf151997cd8902b3--fcb531a7577c48c295a8614d5b5d903b 3828fb84f6024d0d90cd1c8d9d990b1e 7b1d24a58d584276bf151997cd8902b3--3828fb84f6024d0d90cd1c8d9d990b1e 9c911c4187874f4facaff28358562b77 3828fb84f6024d0d90cd1c8d9d990b1e--9c911c4187874f4facaff28358562b77 b1b9bbc57a204b0e8d405e77be3c8d68 9c911c4187874f4facaff28358562b77--b1b9bbc57a204b0e8d405e77be3c8d68 160bdcaba4bb43568d5e036916569419 b1b9bbc57a204b0e8d405e77be3c8d68--160bdcaba4bb43568d5e036916569419 fcc7144288f54faea25e334aa01961a3 160bdcaba4bb43568d5e036916569419--fcc7144288f54faea25e334aa01961a3 82dff68eeaca40b4a15f9ada5f5be1e4 fcc7144288f54faea25e334aa01961a3--82dff68eeaca40b4a15f9ada5f5be1e4 05f60fdaa5be4f8f8d109709f5b00e55 RX(b0) 82dff68eeaca40b4a15f9ada5f5be1e4--05f60fdaa5be4f8f8d109709f5b00e55 05f60fdaa5be4f8f8d109709f5b00e55--d4ca6106f54c4c969aee8d4aec39952d fae556fa17d1467a9d267bb1190cdece 15affe2f6cf4471fb31beaac4ba85005 H 5f4f6fdd43184735bc91b4dd44239ee0--15affe2f6cf4471fb31beaac4ba85005 49e21edf1c324ee88b6de6054211f3fd 15affe2f6cf4471fb31beaac4ba85005--49e21edf1c324ee88b6de6054211f3fd 860d8660f0fb4a12afff9c2707ddc79e 49e21edf1c324ee88b6de6054211f3fd--860d8660f0fb4a12afff9c2707ddc79e b9e7b48af6b84463ad369256ae4f92f1 860d8660f0fb4a12afff9c2707ddc79e--b9e7b48af6b84463ad369256ae4f92f1 a6eabea5fd40434ca50389b0e577f712 b9e7b48af6b84463ad369256ae4f92f1--a6eabea5fd40434ca50389b0e577f712 b4f29b9152e645ccb55da5114a94dbf4 a6eabea5fd40434ca50389b0e577f712--b4f29b9152e645ccb55da5114a94dbf4 da8575dfa48f4962aa156d0706ce7bb3 b4f29b9152e645ccb55da5114a94dbf4--da8575dfa48f4962aa156d0706ce7bb3 bae4f2ecb3824f88a7587de65e2db0f7 X da8575dfa48f4962aa156d0706ce7bb3--bae4f2ecb3824f88a7587de65e2db0f7 bae4f2ecb3824f88a7587de65e2db0f7--b296d38064b54a61b7ef28cbe25e832f 4fd946a4b9fc48468c642b6d79f3837f RZ(g0) bae4f2ecb3824f88a7587de65e2db0f7--4fd946a4b9fc48468c642b6d79f3837f 99f3410e65e64b0dbc6bc4e3794f23f7 X 4fd946a4b9fc48468c642b6d79f3837f--99f3410e65e64b0dbc6bc4e3794f23f7 99f3410e65e64b0dbc6bc4e3794f23f7--85cd24cf6da74604a83ff1b683320cba 1cc2d8cad63244a18f2806f1ec00e942 99f3410e65e64b0dbc6bc4e3794f23f7--1cc2d8cad63244a18f2806f1ec00e942 288f2948d95445f795c24a7161f02832 1cc2d8cad63244a18f2806f1ec00e942--288f2948d95445f795c24a7161f02832 d5e9fbac74664347ac1e8886c4234f21 288f2948d95445f795c24a7161f02832--d5e9fbac74664347ac1e8886c4234f21 b19940bd07324c618d6abbd0711a6f5f X d5e9fbac74664347ac1e8886c4234f21--b19940bd07324c618d6abbd0711a6f5f b19940bd07324c618d6abbd0711a6f5f--a7be82ec77fa491683516795ae8efb04 e61d370f51534d189792a21e7f740d19 RZ(g0) b19940bd07324c618d6abbd0711a6f5f--e61d370f51534d189792a21e7f740d19 2a881c83526f4046ba2fc1f964e9a953 X e61d370f51534d189792a21e7f740d19--2a881c83526f4046ba2fc1f964e9a953 2a881c83526f4046ba2fc1f964e9a953--9bd05b50508d4562aa73a56c12b5fe74 c43f49f60909476094cae1cce1e49e64 X 2a881c83526f4046ba2fc1f964e9a953--c43f49f60909476094cae1cce1e49e64 c43f49f60909476094cae1cce1e49e64--160bdcaba4bb43568d5e036916569419 5481033f49474abc8cf292c2ea004270 RZ(g0) c43f49f60909476094cae1cce1e49e64--5481033f49474abc8cf292c2ea004270 fb5209721d6d4681aac1a5cdc43ca171 X 5481033f49474abc8cf292c2ea004270--fb5209721d6d4681aac1a5cdc43ca171 fb5209721d6d4681aac1a5cdc43ca171--82dff68eeaca40b4a15f9ada5f5be1e4 ed044b4c2db34c7a9db1e37d0e22d8a1 RX(b0) fb5209721d6d4681aac1a5cdc43ca171--ed044b4c2db34c7a9db1e37d0e22d8a1 ed044b4c2db34c7a9db1e37d0e22d8a1--fae556fa17d1467a9d267bb1190cdece

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-08-21T11:48:53.557508 image/svg+xml Matplotlib v3.7.5, https://matplotlib.org/

References


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