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-06-04T14:16:57.059014 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_7f075669667645dfb0679cdbe21bd12e mixing cluster_5026779353174097b976e95d21eb263b cost 9797052418e34964ac19a60ae14d29d4 0 c97e7d34c3c2425ea8440418be5ea6cb H 9797052418e34964ac19a60ae14d29d4--c97e7d34c3c2425ea8440418be5ea6cb 6ed334c23ce042c3883e5182b67633c9 1 b3fe5df189b340cd9673640d8c7dfaf9 c97e7d34c3c2425ea8440418be5ea6cb--b3fe5df189b340cd9673640d8c7dfaf9 6aada7e79a1d485b982d070cf35f5f7c b3fe5df189b340cd9673640d8c7dfaf9--6aada7e79a1d485b982d070cf35f5f7c fe783cf5fb5d4d6d801d6ed69a46fa69 6aada7e79a1d485b982d070cf35f5f7c--fe783cf5fb5d4d6d801d6ed69a46fa69 f9de15889c4844de97ff1e0c9831fc71 fe783cf5fb5d4d6d801d6ed69a46fa69--f9de15889c4844de97ff1e0c9831fc71 c0b1608b833a4bc2a5e654fc19611a2d f9de15889c4844de97ff1e0c9831fc71--c0b1608b833a4bc2a5e654fc19611a2d c93cd50cea5844b6bb426a68c062d09c c0b1608b833a4bc2a5e654fc19611a2d--c93cd50cea5844b6bb426a68c062d09c dce0ea7a92b14fbfb5abf07820a3404c c93cd50cea5844b6bb426a68c062d09c--dce0ea7a92b14fbfb5abf07820a3404c 8fcf98e4ee024f0a9b28782c1d3b0ed7 dce0ea7a92b14fbfb5abf07820a3404c--8fcf98e4ee024f0a9b28782c1d3b0ed7 fb5d4ae94a0744f98f9a307dc4cf16aa 8fcf98e4ee024f0a9b28782c1d3b0ed7--fb5d4ae94a0744f98f9a307dc4cf16aa ef28a476361640c2bc5b7f417f49ef87 fb5d4ae94a0744f98f9a307dc4cf16aa--ef28a476361640c2bc5b7f417f49ef87 ea0b2f75a9f84817969bdb5ec47caa7c ef28a476361640c2bc5b7f417f49ef87--ea0b2f75a9f84817969bdb5ec47caa7c 8e53e82747ea4c5391174283b2e34d44 ea0b2f75a9f84817969bdb5ec47caa7c--8e53e82747ea4c5391174283b2e34d44 5f86a712d4944aa99e391f94dc61b55c 8e53e82747ea4c5391174283b2e34d44--5f86a712d4944aa99e391f94dc61b55c 6df339877b05435a81ce1063b8181a8f 5f86a712d4944aa99e391f94dc61b55c--6df339877b05435a81ce1063b8181a8f 55a6f285873743acb31859344aa8fe1b 6df339877b05435a81ce1063b8181a8f--55a6f285873743acb31859344aa8fe1b 3764eb905922471cbcaf6b8b0c994de8 55a6f285873743acb31859344aa8fe1b--3764eb905922471cbcaf6b8b0c994de8 89227034450e4594b2b33624b9253f69 3764eb905922471cbcaf6b8b0c994de8--89227034450e4594b2b33624b9253f69 ef967888614742eb8a1a9c7ada085fc2 89227034450e4594b2b33624b9253f69--ef967888614742eb8a1a9c7ada085fc2 26debffe2fd64f39b5ef1f21e1ab8388 RX(b0) ef967888614742eb8a1a9c7ada085fc2--26debffe2fd64f39b5ef1f21e1ab8388 31a5bcfd3e3d40d38203f03ddd65ce0f 26debffe2fd64f39b5ef1f21e1ab8388--31a5bcfd3e3d40d38203f03ddd65ce0f 4f9a93d458b14c1e8341e86f321e0f78 581046bff90a4012b41bfb61c70bdf4f H 6ed334c23ce042c3883e5182b67633c9--581046bff90a4012b41bfb61c70bdf4f 01aa8f33b66643e3b7168d80ee5ad4a1 2 af48bfd041a04400959bd2ff7311fce5 X 581046bff90a4012b41bfb61c70bdf4f--af48bfd041a04400959bd2ff7311fce5 af48bfd041a04400959bd2ff7311fce5--b3fe5df189b340cd9673640d8c7dfaf9 b8a4fea9e34245cbb14c3e2b1bf1f8d2 RZ(g0) af48bfd041a04400959bd2ff7311fce5--b8a4fea9e34245cbb14c3e2b1bf1f8d2 b4e311bf48af4d6986fe8be535372de6 X b8a4fea9e34245cbb14c3e2b1bf1f8d2--b4e311bf48af4d6986fe8be535372de6 b4e311bf48af4d6986fe8be535372de6--fe783cf5fb5d4d6d801d6ed69a46fa69 727f12411230405bb1dc7b1766c77d04 b4e311bf48af4d6986fe8be535372de6--727f12411230405bb1dc7b1766c77d04 3c3bae92704043dfb533a77f421b2f8a 727f12411230405bb1dc7b1766c77d04--3c3bae92704043dfb533a77f421b2f8a 5e19e6ed87c841bf9379539a1fa7b6c5 3c3bae92704043dfb533a77f421b2f8a--5e19e6ed87c841bf9379539a1fa7b6c5 bf031e083242498cad02e58f1a854376 5e19e6ed87c841bf9379539a1fa7b6c5--bf031e083242498cad02e58f1a854376 5274a797e09f4a3eaee31349df1e6fe6 bf031e083242498cad02e58f1a854376--5274a797e09f4a3eaee31349df1e6fe6 0baaf2b8434e4a55ae332c2f41049f8f 5274a797e09f4a3eaee31349df1e6fe6--0baaf2b8434e4a55ae332c2f41049f8f 931537de528a42509ab36e8f027dec45 0baaf2b8434e4a55ae332c2f41049f8f--931537de528a42509ab36e8f027dec45 63a57c4f328448489d72005b35c8ef97 931537de528a42509ab36e8f027dec45--63a57c4f328448489d72005b35c8ef97 4c0b9882c22246559af9356c49425769 63a57c4f328448489d72005b35c8ef97--4c0b9882c22246559af9356c49425769 cf485c68bb10496a9924897690dd3c57 4c0b9882c22246559af9356c49425769--cf485c68bb10496a9924897690dd3c57 e0d5e6f178b4494fb3223bc28bab80e6 cf485c68bb10496a9924897690dd3c57--e0d5e6f178b4494fb3223bc28bab80e6 78e1e9459f42414784effdc7f0381b9c e0d5e6f178b4494fb3223bc28bab80e6--78e1e9459f42414784effdc7f0381b9c 89fa37806abc4cbb9d09e45320b63754 78e1e9459f42414784effdc7f0381b9c--89fa37806abc4cbb9d09e45320b63754 3854d3f3e0154fd6ababd1a770b3d8a4 89fa37806abc4cbb9d09e45320b63754--3854d3f3e0154fd6ababd1a770b3d8a4 b22a95d0e0d24928bbf770173dec7906 3854d3f3e0154fd6ababd1a770b3d8a4--b22a95d0e0d24928bbf770173dec7906 e1df7ebe9c1048f1a79e530682dc3d4f RX(b0) b22a95d0e0d24928bbf770173dec7906--e1df7ebe9c1048f1a79e530682dc3d4f e1df7ebe9c1048f1a79e530682dc3d4f--4f9a93d458b14c1e8341e86f321e0f78 3019c21b1b1b40b5b00cb87a8990c891 d8242594388f4ea4a524e438bc62a3b4 H 01aa8f33b66643e3b7168d80ee5ad4a1--d8242594388f4ea4a524e438bc62a3b4 955634729cc94a8bb5093c3051cad9d8 3 dcf75ac62d174c41a0f0577af8d185c6 d8242594388f4ea4a524e438bc62a3b4--dcf75ac62d174c41a0f0577af8d185c6 6f49d0fa2aba4ac89697fde7f7f134dc dcf75ac62d174c41a0f0577af8d185c6--6f49d0fa2aba4ac89697fde7f7f134dc f51b17ac735b426b962f90427630959b 6f49d0fa2aba4ac89697fde7f7f134dc--f51b17ac735b426b962f90427630959b 266f98e5c8b94c96bd53f8c691883441 X f51b17ac735b426b962f90427630959b--266f98e5c8b94c96bd53f8c691883441 266f98e5c8b94c96bd53f8c691883441--f9de15889c4844de97ff1e0c9831fc71 9a2004da3fe8439784ceb01f8e71a517 RZ(g0) 266f98e5c8b94c96bd53f8c691883441--9a2004da3fe8439784ceb01f8e71a517 8fcaeb3d409444fc8586d7af6aa41f0f X 9a2004da3fe8439784ceb01f8e71a517--8fcaeb3d409444fc8586d7af6aa41f0f 8fcaeb3d409444fc8586d7af6aa41f0f--c93cd50cea5844b6bb426a68c062d09c 034ad0b4b7a545a7b70e3111e3216a7f 8fcaeb3d409444fc8586d7af6aa41f0f--034ad0b4b7a545a7b70e3111e3216a7f 1a9138570dd8465980de59fb4e4444ae 034ad0b4b7a545a7b70e3111e3216a7f--1a9138570dd8465980de59fb4e4444ae 8d47f79f034f4e858713de44260afb36 1a9138570dd8465980de59fb4e4444ae--8d47f79f034f4e858713de44260afb36 3bd1954c89c64ad99d981dadc8f3067c X 8d47f79f034f4e858713de44260afb36--3bd1954c89c64ad99d981dadc8f3067c 3bd1954c89c64ad99d981dadc8f3067c--931537de528a42509ab36e8f027dec45 a61a886aaf1143d1b6d98104f02694e1 RZ(g0) 3bd1954c89c64ad99d981dadc8f3067c--a61a886aaf1143d1b6d98104f02694e1 5603ecf3f77e46fda01578f6ff98bd82 X a61a886aaf1143d1b6d98104f02694e1--5603ecf3f77e46fda01578f6ff98bd82 5603ecf3f77e46fda01578f6ff98bd82--4c0b9882c22246559af9356c49425769 ae5e91a1c16545a89be0a569a0abc969 5603ecf3f77e46fda01578f6ff98bd82--ae5e91a1c16545a89be0a569a0abc969 084f986cfb2044b3a6ddb83bbd343f3d ae5e91a1c16545a89be0a569a0abc969--084f986cfb2044b3a6ddb83bbd343f3d 39dab07a567f4b94b90cb4769ec67584 084f986cfb2044b3a6ddb83bbd343f3d--39dab07a567f4b94b90cb4769ec67584 cef11867ee634b75a46e56d1ba2e2dac 39dab07a567f4b94b90cb4769ec67584--cef11867ee634b75a46e56d1ba2e2dac 6fde84afd4f143e69e81f215a2641186 cef11867ee634b75a46e56d1ba2e2dac--6fde84afd4f143e69e81f215a2641186 10ecf96683224aa487961c96fad4fed8 6fde84afd4f143e69e81f215a2641186--10ecf96683224aa487961c96fad4fed8 6ed74608fa9c4b93b18edb2ec4381b80 RX(b0) 10ecf96683224aa487961c96fad4fed8--6ed74608fa9c4b93b18edb2ec4381b80 6ed74608fa9c4b93b18edb2ec4381b80--3019c21b1b1b40b5b00cb87a8990c891 bc9ef31ce58e4d39998bfb8286e5e85a 4e2ecf63dba8472eab6700cf35e7761c H 955634729cc94a8bb5093c3051cad9d8--4e2ecf63dba8472eab6700cf35e7761c 1bdba4a7c58f468eb8e27169e2c3048e 4e2ecf63dba8472eab6700cf35e7761c--1bdba4a7c58f468eb8e27169e2c3048e 77aa9032af82496487f8398ddc6b271d 1bdba4a7c58f468eb8e27169e2c3048e--77aa9032af82496487f8398ddc6b271d 07f7c81c74f246d1b9167a3eeb471c3c 77aa9032af82496487f8398ddc6b271d--07f7c81c74f246d1b9167a3eeb471c3c db9ff1a5d5bf45f5b5b2e79a4bddc293 07f7c81c74f246d1b9167a3eeb471c3c--db9ff1a5d5bf45f5b5b2e79a4bddc293 34b86820a9d441f8babac7e4d2fd39d3 db9ff1a5d5bf45f5b5b2e79a4bddc293--34b86820a9d441f8babac7e4d2fd39d3 6537a76fb0e84c27845a2d56ecc9a339 34b86820a9d441f8babac7e4d2fd39d3--6537a76fb0e84c27845a2d56ecc9a339 7db082453ccf484b826d14e10b312fff X 6537a76fb0e84c27845a2d56ecc9a339--7db082453ccf484b826d14e10b312fff 7db082453ccf484b826d14e10b312fff--dce0ea7a92b14fbfb5abf07820a3404c 089a7f6a3c0748248889386be6625d27 RZ(g0) 7db082453ccf484b826d14e10b312fff--089a7f6a3c0748248889386be6625d27 aecf53ddf3a74ca28e9b939d32c3df09 X 089a7f6a3c0748248889386be6625d27--aecf53ddf3a74ca28e9b939d32c3df09 aecf53ddf3a74ca28e9b939d32c3df09--fb5d4ae94a0744f98f9a307dc4cf16aa 56d20704143545a1ae9145824312b8ec aecf53ddf3a74ca28e9b939d32c3df09--56d20704143545a1ae9145824312b8ec 6a536a2050504ebabe1de033174c0887 56d20704143545a1ae9145824312b8ec--6a536a2050504ebabe1de033174c0887 a69030129b6d4467a3f2ab11bbb305d8 6a536a2050504ebabe1de033174c0887--a69030129b6d4467a3f2ab11bbb305d8 220c5048ccc74fc2989157abf98b2894 X a69030129b6d4467a3f2ab11bbb305d8--220c5048ccc74fc2989157abf98b2894 220c5048ccc74fc2989157abf98b2894--cf485c68bb10496a9924897690dd3c57 cdf22542c2064047a7af3b6b5649bdf7 RZ(g0) 220c5048ccc74fc2989157abf98b2894--cdf22542c2064047a7af3b6b5649bdf7 a786c57590f6410e8b6f7d119d37e8f9 X cdf22542c2064047a7af3b6b5649bdf7--a786c57590f6410e8b6f7d119d37e8f9 a786c57590f6410e8b6f7d119d37e8f9--78e1e9459f42414784effdc7f0381b9c f7008ba0204244a6b4390fce0f1445d9 X a786c57590f6410e8b6f7d119d37e8f9--f7008ba0204244a6b4390fce0f1445d9 f7008ba0204244a6b4390fce0f1445d9--cef11867ee634b75a46e56d1ba2e2dac 3f257695e9504698aa9a6b7b9d0f8467 RZ(g0) f7008ba0204244a6b4390fce0f1445d9--3f257695e9504698aa9a6b7b9d0f8467 743ee52384f14355bde6847d790463f8 X 3f257695e9504698aa9a6b7b9d0f8467--743ee52384f14355bde6847d790463f8 743ee52384f14355bde6847d790463f8--10ecf96683224aa487961c96fad4fed8 c6fa4a3658294b6f9056547ee05684b3 RX(b0) 743ee52384f14355bde6847d790463f8--c6fa4a3658294b6f9056547ee05684b3 c6fa4a3658294b6f9056547ee05684b3--bc9ef31ce58e4d39998bfb8286e5e85a

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-06-04T14:17:01.424694 image/svg+xml Matplotlib v3.7.5, https://matplotlib.org/

References


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