Quantum Approximate Optimization Algorithm¶
Let’s now see how we can implement the Quantum Approximate Optimization Algorithm (QAOA) to compute the Max-Cut of a rectangular graph by leveraging
cudaq:vqe
. For more on the QAOA algorithm and the Max-Cut problem, refer to
this paper.
import cudaq
from cudaq import spin
from typing import List
import numpy as np
# Here we build up a kernel for QAOA with `p` layers, with each layer
# containing the alternating set of unitaries corresponding to the problem
# and the mixer Hamiltonians. The algorithm leverages the VQE algorithm
# to compute the Max-Cut of a rectangular graph illustrated below.
# v0 0---------------------0 v1
# | |
# | |
# | |
# | |
# v3 0---------------------0 v2
# The Max-Cut for this problem is 0101 or 1010.
# The problem Hamiltonian
hamiltonian = 0.5 * spin.z(0) * spin.z(1) + 0.5 * spin.z(1) * spin.z(2) \
+ 0.5 * spin.z(0) * spin.z(3) + 0.5 * spin.z(2) * spin.z(3)
# Problem parameters.
qubit_count: int = 4
layer_count: int = 2
parameter_count: int = 2 * layer_count
@cudaq.kernel
def kernel_qaoa(qubit_count: int, layer_count: int, thetas: List[float]):
"""QAOA ansatz for Max-Cut"""
qvector = cudaq.qvector(qubit_count)
# Create superposition
h(qvector)
# Loop over the layers
for layer in range(layer_count):
# Loop over the qubits
# Problem unitary
for qubit in range(qubit_count):
x.ctrl(qvector[qubit], qvector[(qubit + 1) % qubit_count])
rz(2.0 * thetas[layer], qvector[(qubit + 1) % qubit_count])
x.ctrl(qvector[qubit], qvector[(qubit + 1) % qubit_count])
# Mixer unitary
for qubit in range(qubit_count):
rx(2.0 * thetas[layer + layer_count], qvector[qubit])
# Specify the optimizer and its initial parameters. Make it repeatable.
cudaq.set_random_seed(13)
optimizer = cudaq.optimizers.COBYLA()
np.random.seed(13)
optimizer.initial_parameters = np.random.uniform(-np.pi / 8.0, np.pi / 8.0,
parameter_count)
print("Initial parameters = ", optimizer.initial_parameters)
# Define the objective, return `<state(params) | H | state(params)>`
def objective(parameters):
return cudaq.observe(kernel_qaoa, hamiltonian, qubit_count, layer_count,
parameters).expectation()
# Optimize!
optimal_expectation, optimal_parameters = optimizer.optimize(
dimensions=parameter_count, function=objective)
# Print the optimized value and its parameters
print("Optimal value = ", optimal_expectation)
print("Optimal parameters = ", optimal_parameters)
# Sample the circuit using the optimized parameters
counts = cudaq.sample(kernel_qaoa, qubit_count, layer_count, optimal_parameters)
print(counts)
Let’s now see how we can implement the Quantum Approximate Optimization Algorithm (QAOA) to compute the Max-Cut of a rectangular graph For more on the QAOA algorithm and the Max-Cut problem, refer to this paper.
#include <cudaq.h>
#include <cudaq/algorithm.h>
#include <cudaq/gradients.h>
#include <cudaq/optimizers.h>
#include <cudaq/spin_op.h>
// Here we build up a CUDA Quantum kernel for QAOA with p layers, with each
// layer containing the alternating set of unitaries corresponding to the
// problem and the mixer Hamiltonians. The algorithm leverages the CUDA Quantum
// VQE support to compute the Max-Cut of a rectangular graph illustrated below.
//
// v0 0---------------------0 v1
// | |
// | |
// | |
// | |
// v3 0---------------------0 v2
// The Max-Cut for this problem is 0101 or 1010.
struct ansatz {
void operator()(std::vector<double> theta, const int n_qubits,
const int n_layers) __qpu__ {
cudaq::qvector q(n_qubits);
// Prepare the initial state by superposition
h(q);
// Loop over all the layers
for (int i = 0; i < n_layers; ++i) {
// Problem Hamiltonian
for (int j = 0; j < n_qubits; ++j) {
x<cudaq::ctrl>(q[j], q[(j + 1) % n_qubits]);
rz(2.0 * theta[i], q[(j + 1) % n_qubits]);
x<cudaq::ctrl>(q[j], q[(j + 1) % n_qubits]);
}
for (int j = 0; j < n_qubits; ++j) {
// Mixer Hamiltonian
rx(2.0 * theta[i + n_layers], q[j]);
}
}
}
};
int main() {
using namespace cudaq::spin;
cudaq::set_random_seed(13); // set for repeatability
// Problem Hamiltonian
const cudaq::spin_op Hp = 0.5 * z(0) * z(1) + 0.5 * z(1) * z(2) +
0.5 * z(0) * z(3) + 0.5 * z(2) * z(3);
// Problem parameters
const int n_qubits = 4;
const int n_layers = 2;
const int n_params = 2 * n_layers;
// Instantiate the optimizer
cudaq::optimizers::cobyla optimizer; // gradient-free COBYLA
// Set initial values for the optimization parameters
optimizer.initial_parameters = cudaq::random_vector(
-M_PI / 8.0, M_PI / 8.0, n_params, std::mt19937::default_seed);
// Call the optimizer
auto [opt_val, opt_params] = cudaq::vqe(
ansatz{}, Hp, optimizer, n_params, [&](std::vector<double> params) {
return std::make_tuple(params, n_qubits, n_layers);
});
// Print the optimized value and the parameters
printf("Optimal value = %.16lf\n", opt_val);
printf("Optimal params = (%.16lf, %.16lf, %.16lf, %.16lf) \n", opt_params[0],
opt_params[1], opt_params[2], opt_params[3]);
// Sample the circuit using optimized parameters
auto counts = cudaq::sample(ansatz{}, opt_params, n_qubits, n_layers);
// Dump the states and the counts
counts.dump();
return 0;
}