QAOA
This notebook demonstrates the Classiq performance re. the Quantum Approximate Optimization Algorithm (QAOA), focusing on the Max Clique problem.
1. Calling the Built-in QAOA
This section calls the built-in QAOA of Classiq, constructing the corresponding quantum model from a combinatorial optimization Pyomo model.
1.1. Generating a Pyomo Model
import time
import networkx as nx
import numpy as np
import pyomo.environ as pyo
np.random.seed(2)
def define_max_clique_model(graph):
model = pyo.ConcreteModel()
# each x_i states if node i belongs to the cliques
model.x = pyo.Var(graph.nodes, domain=pyo.Binary)
x_variables = np.array(list(model.x.values()))
# define the complement adjacency matrix as the matrix where 1 exists for each non-existing edge
adjacency_matrix = nx.convert_matrix.to_numpy_array(graph, nonedge=0)
complement_adjacency_matrix = (
1
- nx.convert_matrix.to_numpy_array(graph, nonedge=0)
- np.identity(len(model.x))
)
# constraint that 2 nodes without an edge in the graph cannot be chosen together
model.clique_constraint = pyo.Constraint(
expr=x_variables @ complement_adjacency_matrix @ x_variables == 0
)
# maximize the number of nodes in the chosen clique
model.value = pyo.Objective(expr=sum(x_variables), sense=pyo.maximize)
return model
Setting a specific problem and some hyperparameters.
QAOA_NUM_LAYERS = 10
NUM_SHOTS = 1e4
NUM_QUBITS = 7
graph = nx.erdos_renyi_graph(NUM_QUBITS, 0.6, seed=79)
max_clique_model = define_max_clique_model(graph)
# transpilation_options = {"classiq": "custom", "qiskit": 3}
transpilation_options = {"classiq": "auto optimize", "qiskit": 1}
1.2 Constructing, Synthesizing, and Running a QAOA Model
from classiq import (
CustomHardwareSettings,
Preferences,
QuantumProgram,
construct_combinatorial_optimization_model,
execute,
set_execution_preferences,
set_preferences,
show,
synthesize,
)
from classiq.applications.combinatorial_optimization import OptimizerConfig, QAOAConfig
from classiq.execution import ExecutionPreferences
qaoa_config = QAOAConfig(num_layers=QAOA_NUM_LAYERS)
optimizer_config = OptimizerConfig(max_iteration=400, alpha_cvar=1)
qmod = construct_combinatorial_optimization_model(
pyo_model=max_clique_model,
qaoa_config=qaoa_config,
optimizer_config=optimizer_config,
)
execution_preferences = ExecutionPreferences(num_shots=NUM_SHOTS)
preferences = Preferences(
custom_hardware_settings=CustomHardwareSettings(basis_gates=["cx", "u"]),
transpilation_option=transpilation_options["classiq"],
)
qmod = set_execution_preferences(qmod, execution_preferences)
qmod = set_preferences(qmod, preferences=preferences)
qprog = synthesize(qmod)
res = execute(qprog).result()
circuit = QuantumProgram.from_qprog(qprog)
depth_classiq = circuit.transpiled_circuit.depth
cx_counts_classiq = circuit.transpiled_circuit.count_ops["cx"]
classiq_solving_time = res[0].value.time
interations_classiq = [
intermediate_result.iteration_number
for intermediate_result in res[0].value.intermediate_results
]
results_classiq = [
-intermediate_result.mean_all_solutions
for intermediate_result in res[0].value.intermediate_results
]
2. Comparing to Qiskit
We use qiskit version 1.0.
Qiskit has no module in which to specify a generic optimization problem; therefore, you have to do the preprocessing and post-processing yourself. Retrieve the Hamiltonian that enters into the VQE.
from typing import List
from classiq import Pauli as ClassiqPauli
def get_classiq_hamiltonian(execution_result) -> List[List[str]]:
hamiltonian_result = execution_result[1].value
parsed_pauli_list = list()
for pauli_term in hamiltonian_result:
pauli_str = "".join(ClassiqPauli(pauli).name for pauli in pauli_term["pauli"])
coefficient = pauli_term["coefficient"]
parsed_pauli_list.append([pauli_str, coefficient])
return parsed_pauli_list
Define a function for running QAOA on Qiskit and returning the results.
Due to long runtime the code for generating the qiskit data is commented out and the results are hard-coded in the notebook. For running the full code please uncomment the code three cells below.
import pathlib
path = (
pathlib.Path(__file__).parent.resolve()
if "__file__" in locals()
else pathlib.Path(".")
)
qiskit_result = np.load(path / "qiskit_res.npy")
max_classiq_iter = len(interations_classiq)
depth_qiskit = 1646
cx_counts_qiskit = 1660
iterations_qiskit = np.linspace(1, len(qiskit_result), len(qiskit_result))
# from importlib.metadata import version
# try:
# import qiskit
# if version('qiskit') != "1.0.0":
# !pip uninstall qiskit -y
# !pip install qiskit==1.0.0
# except ImportError:
# !pip install qiskit==1.0.0
# from qiskit import QuantumCircuit, transpile
# from qiskit.primitives import Estimator, Sampler
# from qiskit.quantum_info import Pauli, SparsePauliOp, Statevector
# from qiskit.result import QuasiDistribution
# from qiskit_algorithms.minimum_eigensolvers import QAOA
# from qiskit_algorithms.optimizers import COBYLA
# def qiskit_qaoa(hamiltonian, qaoa_num_layers, num_qubits):
# """
# Gets a Hamiltonian for QAOA and num of quantum layers, returning the most probable solution and its corresponding cost
# as well as intermediate results
# """
# counts = []
# values = []
# def store_intermediate_result(eval_count, parameters, mean, std):
# # callable to store results
# counts.append(eval_count)
# values.append(mean)
# def objective_value(x, hamiltonian):
# # get objective value for a given computational basis state
# qc = QuantumCircuit(num_qubits)
# for k in range(num_qubits):
# if x[k] == 1:
# qc.x(k)
# estimated_cost = estimator.run(qc, hamiltonian).result().values[0]
# return -estimated_cost
# def bitfield(n: int, L: int) -> list[int]:
# # binary representation as list
# result = np.binary_repr(n, L)
# return [int(digit) for digit in result] # [2:] to chop off the "0b" part
# def sample_most_likely(state_vector: QuasiDistribution | Statevector) -> np.ndarray:
# """Compute the most likely binary string from the state vector.
# Args:
# state_vector: State vector or quasi-distribution.
# Returns:
# Binary string as an array of ints.
# """
# if isinstance(state_vector, QuasiDistribution):
# values = list(state_vector.values())
# else:
# values = state_vector
# n = int(np.log2(len(values)))
# k = np.argmax(np.abs(values))
# x = bitfield(k, n)
# x.reverse()
# return np.asarray(x)
# estimator = Estimator(options={"shots": int(NUM_SHOTS)})
# sampler = Sampler()
# optimizer = COBYLA()
# qaoa = QAOA(
# sampler, optimizer, reps=qaoa_num_layers, callback=store_intermediate_result
# )
# start = time.time()
# result = qaoa.compute_minimum_eigenvalue(hamiltonian)
# solving_time = time.time() - start
# x = sample_most_likely(result.eigenstate)
# transpiled_circuit = transpile(
# result.optimal_circuit,
# basis_gates=["u", "cx"],
# optimization_level=transpilation_options["qiskit"],
# )
# return (
# transpiled_circuit.depth(),
# transpiled_circuit.count_ops()["cx"],
# solving_time,
# x,
# objective_value(x, hamiltonian),
# counts,
# values,
# )
The same QAOA with Qiskit
# pauli_list = get_classiq_hamiltonian(res)
# hamiltonian = SparsePauliOp.from_list(pauli_list)
# (
# depth_qiskit,
# cx_counts_qiskit,
# time_qiskit,
# most_probable_state,
# cost,
# iterations_qiskit,
# results_qiskit,
# ) = qiskit_qaoa(hamiltonian, QAOA_NUM_LAYERS, NUM_QUBITS)
# qiskit_result = np.real(results_qiskit[0:max_classiq_iter])
# iterations_qiskit = iterations_qiskit[0:max_classiq_iter]
3. Plotting the Data
import matplotlib.pyplot as plt
classiq_color = "#F43764"
qiskit_color = "#6FA4FF"
plt.rcParams["font.family"] = "serif"
plt.rc("savefig", dpi=300)
plt.rcParams["axes.linewidth"] = 1
plt.rcParams["xtick.major.size"] = 5
plt.rcParams["xtick.minor.size"] = 5
plt.rcParams["ytick.major.size"] = 5
plt.rcParams["ytick.minor.size"] = 5
qiskit_label = f"qiskit: depth={depth_qiskit} \n cx-counts={cx_counts_qiskit}"
classiq_label = f"classiq: depth={depth_classiq},\n cx-counts={cx_counts_classiq}"
plt.plot(
iterations_qiskit,
qiskit_result,
"-",
label=qiskit_label,
linewidth=1.5,
color=qiskit_color,
)
plt.plot(
interations_classiq[0:max_classiq_iter],
results_classiq[0:max_classiq_iter],
"-",
label=classiq_label,
linewidth=1.5,
color=classiq_color,
)
# plt.ylim(0,90)
plt.ylabel("Energy", fontsize=16)
plt.xlabel("Iteration", fontsize=16)
plt.yticks(fontsize=16)
plt.xticks(fontsize=16)
plt.legend(loc="upper right", fontsize=16)
<matplotlib.legend.Legend at 0x174d7abd0>