Tutorial - Variational Quantum Algorithms

Exercise 1: Max 4-Cut

  1. Problem Definition

The Max 4-Cut problem is defined on a graph \( G = (V, E) \), with a corresponding problem Hamiltonian given by:

\[ H_P = \sum_{e \in E} w_{e} H_{e}, \]

where \( w_e \) represents the weight of edge \( e \in E\), and \( H_e \) is the Hamiltonian associated with that edge.

  1. Generic Graph Problem

In this exercise, the class GraphProblem from the qaoa.problems module is used to define the problem. The class requires two inputs:

  1. A networkx graph \(G\),

  2. The number of qubits per vertex/node.

The GraphProblem class includes a method to create a quantum circuit for implementing the phase separating Hamiltonian \(e^{-i\theta H_P}\).

  1. Specific problem implementation

However, GraphProblem relies on an abstract method, create_edge_circuit, which must be implemented to define the phase separating Hamiltonian \( H_e \) for an edge.

import networkx as nx
import numpy as np

from qaoa.problems import GraphProblem

from qiskit import QuantumCircuit
from qiskit.circuit.library import PhaseGate

class Max4Cut(GraphProblem):
    def __init__(self, G: nx.Graph ) -> None:
        
        N_qubits_per_node = 2
        super().__init__(G, N_qubits_per_node)

        # each color is associated with a bitstring combination
        self.colors = {
            "color1": ["00"],
            "color2": ["01"],
            "color3": ["10"],
            "color4": ["11"],
        }
        # Create a dictionary to map each index to its corresponding set
        self.bitstring_to_color = {}
        for key, indices in self.colors.items():
            for index in indices:
                self.bitstring_to_color[index] = key

    def create_edge_circuit(self, theta):
        qc = QuantumCircuit(2 * self.N_qubits_per_node)

        " ---------------------"
        " implement the circuit"
        " ---------------------"
            
        return qc
    def create_edge_circuit_fixed_node(self, theta):
        pass

Show the circuit for one edge

test = Max4Cut(nx.Graph([(0, 1, {"weight": 1.0})]))
test.create_edge_circuit(0.2).draw("mpl")
  1. Graph instance

Let’s start by defining a Graph with 10 nodes

import matplotlib.pyplot as plt
# Create a graph with 8 nodes
G = nx.Graph()
# Add nodes
G.add_nodes_from(range(8))
# Add edges (example connections)
edges = [
    (0, 1), (0, 2), (1, 3), (1, 4), (2, 4), (2, 5),
    (3, 6), (4, 7), (5, 6), (6, 7), (7,3), (3,5)
]
G.add_edges_from(edges)

# Draw the graph
plt.figure(figsize=(3,3))
nx.draw(G, with_labels=True, pos = nx.circular_layout(G))
  1. Problem instance

Now we can instantiate a Max4Cut problem with the graph and create and draw the resulting circuit for the phase separating Hamiltonian. Observe that there is one circuit per edge in the graph.

max4cut = Max4Cut(G)
max4cut.create_circuit()
max4cut.circuit.draw("mpl")
  1. QAOA instance

We create a QAOA instance using

  • as initial state we use the \(\ket{+}\) state,

  • as mixing operator the \(X\)-mixer, and

  • as phase separation operator our newly created max4cut instance.

from qaoa import QAOA
from qaoa.initialstates import Plus
from qaoa.mixers import X
from qiskit_algorithms.optimizers import COBYLA

settings = {"maxiter": 100, "tol": 1e-3, "rhobeg": 0.1}
optimizer = [COBYLA, settings.copy()]

qaoa_k4 = QAOA(initialstate=Plus(), problem=max4cut, mixer=X(), optimizer=optimizer)
  1. Run QAOA

  • Sample the cost landscape and plot it.

from plot_functions import *
qaoa_k4.sample_cost_landscape( angles={"gamma": [0, np.pi, 20], "beta": [0, np.pi/2, 10]} )
plot_E(qaoa_k4)
  • Use a local optimizer to find the optimum for \(p=1\). We converge quickly to the local optimum.

qaoa_k4.optimize(depth=1)

fig = plt.figure(figsize=(6, 6))
gamma = []
beta = []
angles = qaoa_k4.optimization_results[1].angles
for i in range(len(angles)):
    gamma.append(angles[i][0])
    beta.append(angles[i][1])
plt.plot(gamma, beta, "x-k")
plt.plot(gamma[0], beta[0], "wo")
plt.plot(gamma[-1], beta[-1], "or")
plot_E(qaoa_k4, fig=fig)
  • Run QAOA up to depth \(p=5\). We can see how the approximation ratio increases with \(p\).

maxdepth = 5
qaoa_k4.optimize(depth=maxdepth)

p = np.arange(1, len(np.array(qaoa_k4.get_Exp())) + 1)
maxval=12
plt.plot(p, -np.array(qaoa_k4.get_Exp()) / maxval, marker="x")
plt.xlabel("depth")
plt.ylabel("Approx. ratio")
plt.title("QAOA Max 4-Cut")
plt.ylim([0, 1])
plt.show()

Exercise 2: Max 3-Cut using the full Hilbert space

  1. Find the circuit with pen and paper

Given \(\operatorname{clr}^3_{< 3}\) we now want to devide the states into the following sets (containing power of two states),

  • set one consist of \(\ket{1011}, \ket{1110}\), and

  • set two consists of \(\ket{0000}, \ket{0101}, \ket{1010}, \ket{1111}, \).

Apply Theorem 1 from [1] and work out what the circuit for an edge should look like, using pen and paper.

Are there any gates that cancel?

  1. Specific problem implementation

class Max3CutFullH(GraphProblem):
    def __init__(self, G: nx.Graph ) -> None:

        N_qubits_per_node = 2
        super().__init__(G, N_qubits_per_node)

        self.colors = {
            "color1": ["00"],
            "color2": ["01"],
            "color3": ["10", "11"],
        }
        # Create a dictionary to map each index to its corresponding set
        self.bitstring_to_color = {}
        for key, indices in self.colors.items():
            for index in indices:
                self.bitstring_to_color[index] = key

    def create_edge_circuit(self, theta):
        qc = QuantumCircuit(2 * self.N_qubits_per_node)
        
        " ---------------------"
        " implement the circuit"
        " ---------------------"
        
        return qc

    def create_edge_circuit_fixed_node(self, theta):
        pass
test = Max3CutFullH(nx.Graph([(0, 1, {"weight": 1.0})]))
test.create_edge_circuit(0.2).draw("mpl")
  1. Run QAOA

qaoa_k3_fullH = QAOA(initialstate=Plus(), problem=Max3CutFullH(G), mixer=X(), optimizer=optimizer)
qaoa_k3_fullH.optimize(depth=1, angles={"gamma": [0, np.pi, 20], "beta": [0, np.pi/2, 10]} )

fig = plt.figure(figsize=(6, 6))
gamma = []
beta = []
angles = qaoa_k3_fullH.optimization_results[1].angles
for i in range(len(angles)):
    gamma.append(angles[i][0])
    beta.append(angles[i][1])
plt.plot(gamma, beta, "x-k")
plt.plot(gamma[0], beta[0], "wo")
plt.plot(gamma[-1], beta[-1], "or")
plot_E(qaoa_k3_fullH, fig=fig)
maxdepth = 5
qaoa_k3_fullH.optimize(depth=maxdepth)

p = np.arange(1, len(np.array(qaoa_k3_fullH.get_Exp())) + 1)
maxval=12
plt.plot(p, -np.array(qaoa_k3_fullH.get_Exp()) / maxval, marker="x")
plt.xlabel("depth")
plt.ylabel("Approx. ratio")
plt.title("QAOA Max 3-Cut using the full Hilber space")
plt.ylim([0, 1])
plt.show()

Exercise 3: Max 3-Cut using subspaces

  1. Show that the Grover mixer is valid using pen and paper

\[ U_m(\beta) = e^{-i\beta \ket{F}\bra{F}}, \quad \text{where } U_S \ket{0} = \ket{F} := \frac{1}{\sqrt{|B|}} \sum_{x \in B} \ket{x} \]
  1. Create circuit for Grover mixer

Using the package qaoa create a Grover mixer for \(\ket{ltk3} = \frac{\ket{00} + \ket{01} + \ket{11}}{\sqrt{3}}\). For this we import the following:

from qaoa.initialstates import LessThanK

ltk3 = LessThanK(3)
ltk3.create_circuit()
ltk3.circuit.draw("mpl")

Create a circuit for \(\ket{ltk3}^{\otimes 3}\). For this we can use the Tensor class.

from qaoa.initialstates import Tensor

numNodes = 3
phi0 = Tensor(ltk3, numNodes)
phi0.create_circuit()
phi0.circuit.draw("mpl")

create the Grover mixer for \(\ket{ltk3}^{\otimes 3}\)

from qaoa.mixers import Grover
" ---------------------"
" define the Grover mixer"
" ---------------------"
  1. Numerically test that the Grover mixer is valid

Show that we have a valid mixer by plotting the overlap \(\bra{z_1} U_M(\beta) \ket{z_2}\) depending on \(\beta\):

  • Pick two feasible computational basis states \(\ket{z_1} \neq \ket{z_2}\) and see that the overlap is nonzero for some values of \(\beta\)

  • Pick a feasible and an infeasible state, and show that the overlap is always zero

Let’s start by importing the necessary things, before we can do some plots.

import numpy as np
import matplotlib.pyplot as pl
from tqdm import tqdm
from qiskit_aer import Aer
from qiskit import (
    QuantumCircuit,
    QuantumRegister,
    ClassicalRegister,
    transpile,
)
backend=Aer.get_backend("qasm_simulator")
overlap = []
betavalues = np.linspace(0,2*np.pi,50)
shots=10**5
for beta in tqdm(betavalues):
    q = QuantumRegister(numNodes*2)
    c = ClassicalRegister(numNodes*2)
    circuit = QuantumCircuit(q, c)
        " ---------------------"
        " implement a circuit for <z_1|"
        " ---------------------"
    circuit.barrier()
    circuit.append(grover.circuit, q)
    circuit.measure(q, c)
    circuit = transpile(circuit, backend)
    
    # Assign float values to the parameters
    parameters = list(circuit.parameters)
    parameter_values = [beta]
    bound_circuit = circuit.assign_parameters(dict(zip(parameters, parameter_values)))

    
    # Run the job
    job = backend.run(bound_circuit, shots=shots)
    counts = job.result().get_counts()
    overlap.append(counts.get(
        " ---------------------"
        " choose string for |z_2>"
        " ---------------------"
        ,0.)/shots)
    
# bound_circuit.draw('mpl')

pl.plot(betavalues, overlap)
4. <u>Problem class</u>
class Max3Cut(GraphProblem):
    def __init__(self, G: nx.Graph ) -> None:
        
        N_qubits_per_node = 2
        super().__init__(G, N_qubits_per_node)

        # each color is associated with a bitstring combination
        self.colors = {
            "color1": ["00"],
            "color2": ["01"],
            "color3": ["10"],
        }
        # Create a dictionary to map each index to its corresponding set
        self.bitstring_to_color = {}
        for key, indices in self.colors.items():
            for index in indices:
                self.bitstring_to_color[index] = key

    def create_edge_circuit(self, theta):
        qc = QuantumCircuit(2 * self.N_qubits_per_node)

        qc.cx(0, 2)
        qc.cx(1, 3)
        qc.x([2, 3])
        phase_gate = PhaseGate(-theta).control(1)
        qc.append(phase_gate, [2, 3])
        qc.x([2, 3])
        qc.cx(1, 3)
        qc.cx(0, 2)
            
        return qc

        
    def create_edge_circuit_fixed_node(self, theta):
        # we will not use this function, so we can skip it
        pass
  1. Initial state class

To run QAOA we need to implement the InitialState class that creates a Tensor of \(\ket{ltk3}\) state, which can be done just as above

from qaoa.initialstates import InitialState
class Max3CutInitialState(InitialState):
    
    def create_circuit(self) -> None:

        numQubitsPerNode = 2
        self.num_V = int(self.N_qubits/numQubitsPerNode)
        
        tg = 
        
        " ---------------------"
        " define the initial state"
        " ---------------------"
        
        tg.create_circuit()
        self.circuit = tg.circuit
  1. Mixer class

We also need to implement the Mixer class that creates a Tensor of Grover mixer, which can be done just as above

from qaoa.mixers import Mixer
class Max3CutGrover(Mixer):
        
    def create_circuit(self) -> None:

        numQubitsPerNode = 2
        self.num_V = int(self.N_qubits/numQubitsPerNode)
        
        gm = 
        
        " ---------------------"
        " define the Box Grover mixer"
        " ---------------------"
        
        gm.create_circuit()
        self.circuit = gm.circuit
  1. Run QAOA

create an instance of QAOA and plot the resulting landscape

qaoa_k3_subH = QAOA(
    initialstate=Max3CutInitialState(),
    problem=Max4Cut(G),
    mixer=Max3CutGrover(),
    optimizer=optimizer)

qaoa_k3_subH.optimize(depth=1, angles={"gamma": [0, 2*np.pi, 20], "beta": [0, 2*np.pi, 20]} )

fig = plt.figure(figsize=(6, 6))
gamma = []
beta = []
angles = qaoa_k3_subH.optimization_results[1].angles
for i in range(len(angles)):
    gamma.append(angles[i][0])
    beta.append(angles[i][1])
plt.plot(gamma, beta, "x-k")
plt.plot(gamma[0], beta[0], "wo")
plt.plot(gamma[-1], beta[-1], "or")
plot_E(qaoa_k3_subH, fig=fig)
  1. Comparison for MAX 3-CUT

Compare the convergence for the method using the full Hilbert space and the subspace encoding up to d

maxdepth = 5
qaoa_k3_subH.optimize(depth=maxdepth)

p = np.arange(1, len(np.array(qaoa_k3_subH.get_Exp())) + 1)
maxval=12
plt.plot(p, -np.array(qaoa_k3_fullH.get_Exp()) / maxval, marker="x", label="full H")
plt.plot(p, -np.array(qaoa_k3_subH.get_Exp()) / maxval, marker="x", label="sub H")
plt.xlabel("depth")
plt.ylabel("Approx. ratio")
plt.legend()
plt.title("QAOA Max 3-Cut comparison")
plt.ylim([0, 1])
plt.show()

For more info you can look into the following reference.

References

[1] Fuchs, Franz G., Ruben P. Bassa, and Frida Lien. “Encodings of the weighted MAX k-CUT on qubit systems.” Preprint arXiv 2024. arXiv:2411.08594

[2] Fuchs, Franz G., Ruben P. Bassa. “LX-mixers for QAOA: Optimal mixers restricted to subspaces and the stabilizer formalism.” Quantum 8 (2024): 1535. 10.22331/q-2024-11-25-1535

[3] Bärtschi, Andreas, and Stephan Eidenbenz. “Grover mixers for QAOA: Shifting complexity from mixer design to state preparation.” In 2020 IEEE International Conference on Quantum Computing and Engineering (QCE), pp. 72-82. IEEE, 2020. 10.1109/QCE49297.2020.00020