# ********************************************************************************
# * Copyright (c) 2026 the Qrisp authors
# *
# * This program and the accompanying materials are made available under the
# * terms of the Eclipse Public License 2.0 which is available at
# * http://www.eclipse.org/legal/epl-2.0.
# *
# * This Source Code may also be made available under the following Secondary
# * Licenses when the conditions for such availability set forth in the Eclipse
# * Public License, v. 2.0 are satisfied: GNU General Public License, version 2
# * with the GNU Classpath Exception which is
# * available at https://www.gnu.org/software/classpath/license.html.
# *
# * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
# ********************************************************************************
"""Defines the QAOAProblem class encapsulating the cost operator, mixer, and optimization routine."""
import time
import jax
import jax.numpy as jnp
import numpy as np
from scipy.optimize import minimize
from sympy import Symbol
from qrisp import QuantumArray, h, x
from qrisp.algorithms.qaoa.qaoa_benchmark_data import QAOABenchmark
from qrisp.jasp import QuantumVariableTemplate, check_for_tracing_mode, jrange, sample
from qrisp.jasp.optimization_tools.optimize import minimize as jasp_minimize
[docs]
class QAOAProblem:
r"""Central structure to facilitate treatment of QAOA problems.
This class encapsulates the cost operator, mixer operator, and classical cost function for a specific QAOA problem instance. It also provides methods to set the initial state preparation function, classical cost post-processing function, and optimizer for the problem.
For a quick demonstration, we import the relevant functions from already implemented problem instances:
::
from networkx import Graph
G = Graph()
G.add_edges_from([[0,3],[0,4],[1,3],[1,4],[2,3],[2,4]])
from qrisp.qaoa import (QAOAProblem,
create_maxcut_cost_operator,
create_maxcut_cl_cost_function,
RX_mixer)
maxcut_instance = QAOAProblem(cost_operator = create_maxcut_cost_operator(G),
mixer = RX_mixer,
cl_cost_function = create_maxcut_cl_cost_function(G))
from qrisp import QuantumVariable
res = maxcut_instance.run(QuantumVariable(5),
depth = 4,
max_iter = 25)
print(res)
#Yields: {'11100': 0.2847, '00011': 0.2847, '10000': 0.0219, '01000': 0.0219, '00100': 0.0219, '11011': 0.0219, '10111': 0.0219, '01111': 0.0219, '00010': 0.02, '11110': 0.02, '00001': 0.02, '11101': 0.02, '00000': 0.0173, '11111': 0.0173, '10010': 0.0143, '01010': 0.0143, '11010': 0.0143, '00110': 0.0143, '10110': 0.0143, '01110': 0.0143, '10001': 0.0143, '01001': 0.0143, '11001': 0.0143, '00101': 0.0143, '10101': 0.0143, '01101': 0.0143, '11000': 0.0021, '10100': 0.0021, '01100': 0.0021, '10011': 0.0021, '01011': 0.0021, '00111': 0.0021}
For an in-depth tutorial, make sure to check out :ref:`MaxCutQAOA`!
Parameters
----------
cost_operator : callable
A function receiving a :ref:`QuantumVariable` or :ref:`QuantumArray` and parameter $\gamma$. This function performs the application of the cost operator.
mixer : callable
A function receiving a :ref:`QuantumVariable` or :ref:`QuantumArray` and parameter $\beta$. This function performs the application mixing operator.
cl_cost_function : callable
The classical cost function for the specific QAOA problem instance, which takes a dictionary of measurement results as input.
init_function : callable, optional
A function receiving a :ref:`QuantumVariable` or :ref:`QuantumArray` for preparing the inital state.
By default, the uniform superposition state $\ket{+}^n$ is prepared.
callback : bool, optional
If ``True``, intermediate results are stored. The default is ``False``.
"""
def __init__(self, cost_operator, mixer, cl_cost_function, init_function=None, callback=False):
self.cost_operator = cost_operator
self.mixer = mixer
self.cl_cost_function = cl_cost_function
self.init_function = init_function
# parameters for callback
self.callback = callback
self.optimization_params = []
self.optimization_costs = []
def set_callback(self):
"""Sets ``callback=True`` for saving intermediate results."""
self.callback = True
[docs]
def set_init_function(self, init_function):
"""Set the initial state preparation function for the QAOA problem.
Parameters
----------
init_function : function
The initial state preparation function for the specific QAOA problem instance.
"""
self.init_function = init_function
def computeParams(self, p, dt):
"""Compute the angle parameters gamma and beta based on the given inputs. Used for the TQA warm starting the initial parameters for QAOA.
Parameters
----------
p : int
The number of partitions for the time interval.
dt : float
The time step.
Returns
-------
np.array
A concatenated numpy array of gamma and beta values.
"""
if check_for_tracing_mode():
import jax.numpy as jnp
else:
import numpy as jnp
t = (jnp.arange(1, p + 1) - 0.5) / p
gamma = t * dt
beta = (1 - t) * dt
return jnp.concatenate((gamma, beta))
[docs]
def compile_circuit(self, qarg, depth, init_type="random"):
r"""Compiles the circuit that is evaluated by the :meth:`run <qrisp.qaoa.QAOAProblem.run>` method.
Parameters
----------
qarg : :ref:`QuantumVariable` or :ref:`QuantumArray`
The argument to which the QAOA circuit is applied.
depth : int
The amount of QAOA layers.
init_type : string, optional
Specifies the way the initial optimization parameters are chosen. Available are ``random`` and ``tqa``. The default is ``random``:
The parameters are initialized uniformly at random in the interval $[0,\pi/2]$.
For ``tqa``, the parameters are chosen based on the `Trotterized Quantum Annealing <https://quantum-journal.org/papers/q-2021-07-01-491/>`_ protocol.
If ``tqa`` is chosen, and no ``init_function`` for the :ref:`QAOAProblem` is specified, the $\ket{-}^n$ state is prepared (the ground state for the X mixer).
Returns
-------
compiled_qc : :ref:`QuantumCircuit`
The parametrized, compiled quantum circuit without measurements.
list[sympy.Symbol]
A list of the parameters that appear in ``compiled_qc``.
Examples
--------
We create a MaxCut instance and compile the circuit:
>>> from networkx import Graph
>>> G = Graph([(0,1),(1,2),(2,0)])
>>> from qrisp.qaoa import maxcut_problem
>>> from qrisp import QuantumVariable
>>> p = 5
>>> qaoa_instance = maxcut_problem(G)
>>> qarg = QuantumVariable(len(G))
>>> qrisp_qc, symbols = qaoa_instance.compile_circuit(qarg, p)
>>> print(qrisp_qc)
βββββββββββββββββββββββββ Β»
qarg_dupl.0: β€ H ββ€ gphase ββ€ gphase ββββ βββββββββββββββββββββ ββΒ»
βββββ€βββββββββββββββββββββββ΄ββββββββββββββββββ β Β»
qarg_dupl.1: β€ H ββββββββββββββββββββββ€ X ββ€ P(2*gamma_0) ββββΌββΒ»
βββββ€ ββββββββββββββββββββββββ΄ββΒ»
qarg_dupl.2: β€ H βββββββββββββββββββββββββββββββββββββββββββ€ X βΒ»
βββββ βββββΒ»
Β« ββββββββββββββββ ββββββββββ Β»
Β«qarg_dupl.0: ββββββββ βββββββββββββββββββββ βββ€ Rx(2*beta_0) βββββ€ gphase ββββΒ»
Β« βββ΄ββ ββββββββββ β ββββββββββββββββ ββββββββββ Β»
Β«qarg_dupl.1: ββββββ€ X ββββββββ€ gphase ββββΌββββββββββ ββββββββββββββββββββββββΒ»
Β« ββββββ΄ββββ΄βββββββββββββββββββ΄ββ βββ΄ββ ββββββββββββββββΒ»
Β«qarg_dupl.2: β€ P(2*gamma_0) ββββββββββββ€ X βββββββ€ X ββββββββ€ P(2*gamma_0) βΒ»
Β« ββββββββββββββββ βββββ βββββ ββββββββββββββββΒ»
Β« ββββββββββ Β»
Β«qarg_dupl.0: β€ gphase ββββββββββββββββββββ βββββββββββββββββββββ ββΒ»
Β« βββββββββββββββββββββββββββββ΄ββββββββββββββββββ β Β»
Β«qarg_dupl.1: βββββ ββββββ€ Rx(2*beta_0) ββ€ X ββ€ P(2*gamma_1) ββββΌββΒ»
Β« βββ΄ββ ββββββββββββββββ€ββββββββββββββββββββββββ΄ββΒ»
Β«qarg_dupl.2: βββ€ X βββββ€ Rx(2*beta_0) βββββββββββββββββββββββ€ X βΒ»
Β« βββββ ββββββββββββββββ βββββΒ»
Β« ββββββββββββββββ ββββββββββ Β»
Β«qarg_dupl.0: ββββββββ βββββββββββββββββββββ βββ€ Rx(2*beta_1) βββββ€ gphase ββββΒ»
Β« βββ΄ββ ββββββββββ β ββββββββββββββββ ββββββββββ Β»
Β«qarg_dupl.1: ββββββ€ X ββββββββ€ gphase ββββΌββββββββββ ββββββββββββββββββββββββΒ»
Β« ββββββ΄ββββ΄βββββββββββββββββββ΄ββ βββ΄ββ ββββββββββββββββΒ»
Β«qarg_dupl.2: β€ P(2*gamma_1) ββββββββββββ€ X βββββββ€ X ββββββββ€ P(2*gamma_1) βΒ»
Β« ββββββββββββββββ βββββ βββββ ββββββββββββββββΒ»
Β« ββββββββββ Β»
Β«qarg_dupl.0: β€ gphase ββββββββββββββββββββ βββββββββββββββββββββ ββΒ»
Β« βββββββββββββββββββββββββββββ΄ββββββββββββββββββ β Β»
Β«qarg_dupl.1: βββββ ββββββ€ Rx(2*beta_1) ββ€ X ββ€ P(2*gamma_2) ββββΌββΒ»
Β« βββ΄ββ ββββββββββββββββ€ββββββββββββββββββββββββ΄ββΒ»
Β«qarg_dupl.2: βββ€ X βββββ€ Rx(2*beta_1) βββββββββββββββββββββββ€ X βΒ»
Β« βββββ ββββββββββββββββ βββββΒ»
Β« ββββββββββββββββ ββββββββββ Β»
Β«qarg_dupl.0: ββββββββ βββββββββββββββββββββ βββ€ Rx(2*beta_2) βββββ€ gphase ββββΒ»
Β« βββ΄ββ ββββββββββ β ββββββββββββββββ ββββββββββ Β»
Β«qarg_dupl.1: ββββββ€ X ββββββββ€ gphase ββββΌββββββββββ ββββββββββββββββββββββββΒ»
Β« ββββββ΄ββββ΄βββββββββββββββββββ΄ββ βββ΄ββ ββββββββββββββββΒ»
Β«qarg_dupl.2: β€ P(2*gamma_2) ββββββββββββ€ X βββββββ€ X ββββββββ€ P(2*gamma_2) βΒ»
Β« ββββββββββββββββ βββββ βββββ ββββββββββββββββΒ»
Β« ββββββββββ Β»
Β«qarg_dupl.0: β€ gphase ββββββββββββββββββββ βββββββββββββββββββββ ββΒ»
Β« βββββββββββββββββββββββββββββ΄ββββββββββββββββββ β Β»
Β«qarg_dupl.1: βββββ ββββββ€ Rx(2*beta_2) ββ€ X ββ€ P(2*gamma_3) ββββΌββΒ»
Β« βββ΄ββ ββββββββββββββββ€ββββββββββββββββββββββββ΄ββΒ»
Β«qarg_dupl.2: βββ€ X βββββ€ Rx(2*beta_2) βββββββββββββββββββββββ€ X βΒ»
Β« βββββ ββββββββββββββββ βββββΒ»
Β« ββββββββββββββββ ββββββββββ Β»
Β«qarg_dupl.0: ββββββββ βββββββββββββββββββββ βββ€ Rx(2*beta_3) βββββ€ gphase ββββΒ»
Β« βββ΄ββ ββββββββββ β ββββββββββββββββ ββββββββββ Β»
Β«qarg_dupl.1: ββββββ€ X ββββββββ€ gphase ββββΌββββββββββ ββββββββββββββββββββββββΒ»
Β« ββββββ΄ββββ΄βββββββββββββββββββ΄ββ βββ΄ββ ββββββββββββββββΒ»
Β«qarg_dupl.2: β€ P(2*gamma_3) ββββββββββββ€ X βββββββ€ X ββββββββ€ P(2*gamma_3) βΒ»
Β« ββββββββββββββββ βββββ βββββ ββββββββββββββββΒ»
Β« ββββββββββ Β»
Β«qarg_dupl.0: β€ gphase ββββββββββββββββββββ βββββββββββββββββββββ ββΒ»
Β« βββββββββββββββββββββββββββββ΄ββββββββββββββββββ β Β»
Β«qarg_dupl.1: βββββ ββββββ€ Rx(2*beta_3) ββ€ X ββ€ P(2*gamma_4) ββββΌββΒ»
Β« βββ΄ββ ββββββββββββββββ€ββββββββββββββββββββββββ΄ββΒ»
Β«qarg_dupl.2: βββ€ X βββββ€ Rx(2*beta_3) βββββββββββββββββββββββ€ X βΒ»
Β« βββββ ββββββββββββββββ βββββΒ»
Β« ββββββββββββββββ Β»
Β«qarg_dupl.0: ββββββββ βββββββββββββββββββββ βββ€ Rx(2*beta_4) βββββββββββββββββΒ»
Β« βββ΄ββ ββββββββββ β ββββββββββββββββ Β»
Β«qarg_dupl.1: ββββββ€ X ββββββββ€ gphase ββββΌββββββββββ ββββββββββββββββββββββββΒ»
Β« ββββββ΄ββββ΄βββββββββββββββββββ΄ββ βββ΄ββ ββββββββββββββββΒ»
Β«qarg_dupl.2: β€ P(2*gamma_4) ββββββββββββ€ X βββββββ€ X ββββββββ€ P(2*gamma_4) βΒ»
Β« ββββββββββββββββ βββββ βββββ ββββββββββββββββΒ»
Β«
Β«qarg_dupl.0: βββββββββββββββββββββ
Β« ββββββββββββββββ
Β«qarg_dupl.1: βββ βββ€ Rx(2*beta_4) β
Β« βββ΄ββββββββββββββββββ€
Β«qarg_dupl.2: β€ X ββ€ Rx(2*beta_4) β
Β« βββββββββββββββββββββ
"""
temp = list(qarg.qs.data)
# Define QAOA angle parameters gamma and beta for QAOA circuit
gamma = [Symbol("gamma_" + str(i)) for i in range(depth)]
beta = [Symbol("beta_" + str(i)) for i in range(depth)]
# Prepare initial state - if no init_function is specified, prepare uniform superposition
if self.init_function is not None:
self.init_function(qarg)
elif init_type == "tqa": # Prepare the ground state (eigenvalue -1) of the X mixer
x(qarg)
h(qarg)
else:
h(qarg)
# Apply p layers of phase separators and mixers
for i in range(depth):
self.cost_operator(qarg, gamma[i])
self.mixer(qarg, beta[i])
# Compile quantum circuit with intended measurements
if isinstance(qarg, QuantumArray):
intended_measurement_qubits = sum([list(qv) for qv in qarg.flatten()], [])
else:
intended_measurement_qubits = list(qarg)
compiled_qc = qarg.qs.compile(intended_measurements=intended_measurement_qubits)
qarg.qs.data = temp
return compiled_qc, gamma + beta
def optimization_routine(self, qarg_prep, depth, mes_kwargs, init_type, init_point, optimizer, options):
"""Wrapper subroutine for the optimization method used in QAOA. The initial values are set and the optimization via ``COBYLA`` is conducted here.
Parameters
----------
qarg_prep : callable
A function returning a :ref:`QuantumVariable` or :ref:`QuantumArray` to which the QAOA circuit is applied.
depth : int
The amont of QAOA layers.
symbols : list
The list of symbols used in the quantum circuit.
mes_kwargs : dict, optional
The keyword arguments for the measurement function. Default is an empty dictionary, as defined in previous functions.
init_type : string, optional
Specifies the way the initial optimization parameters are chosen. Available are ``random`` and ``tqa``. The default is ``random``.
init_point : ndarray, shape (n,), optional
Specifies the initial optimization parameters.
optimizer : str, optional
Specifies the `SciPy optimization routine <https://docs.scipy.org/doc/scipy/reference/generated/scipy.optimize.minimize.html>`_.
Available are, e.g., ``COBYLA``, ``COBYQA``, ``Nelder-Mead``. The Default is ``COBYLA``.
In tracing mode (i.e. Jasp) Jax-traceable :ref:`optimization routines <optimization_tools>` must be utilized.
Available are ``SPSA``. The Default is ``SPSA``.
options : dict
A dictionary of solver options.
Returns
-------
ndarray, shape (n,)
The optimized parameters of the problem instance.
float or jax.Array
The expectation value of the classical cost function for the optimized parameters.
"""
if check_for_tracing_mode():
# Define optimization wrapper function to be minimized using QAOA
def optimization_wrapper(theta, state_prep, mes_kwargs):
"""Wrapper function for the optimization method used in QAOA.
This function calculates the value of the classical cost function.
Parameters
----------
theta : jax.Array
The array of angle parameters gamma and beta for the QAOA circuit.
state_prep : callable
A function returning a :ref:`QuantumVariable` or :ref:`QuantumArray`.
The expectation of the classical cost function from the state of this QuantumVariable will be measured.
The state preparation function can only take classical values as arguments.
This is because a quantum value would need to be copied for each sampling iteration, which is prohibited by the no-cloning theorem.
mes_kwargs : dict
The keyword arguments for the sample function.
Returns
-------
float
The expected value of the classical cost function.
"""
res_sample = sample(state_prep, shots=mes_kwargs["shots"])(theta)
cl_cost = self.cl_cost_function(res_sample)
return cl_cost
def tqa_angles(p, state_prep, mes_kwargs, steps=10):
"""Compute the optimal parameters for the Trotterized Quantum Annealing (`TQA <https://quantum-journal.org/papers/q-2021-07-01-491/>`_) algorithm.
The function first creates a linspace array `dt` from 0.1 to 1 with `steps` steps.
Then for each `dt_` in `dt`, it computes the parameters `x` using the `computeParams`
function and calculates the energy `energy_` using the `optimization_wrapper` function.
The energy values are stored in the `energy` list. The `dt_max` corresponding to the
minimum energy is found and used to compute the optimal parameters which are returned.
Parameters
----------
p : int
The number of partitions for the time interval.
state_prep : callable
A function returning a :ref:`QuantumVariable` or :ref:`QuantumArray`.
mes_kwargs : dict
The measurement keyword arguments.
steps : int, optional
The number of steps for the linspace function, default is 10.
Returns
-------
jax.Array
A concatenated jax.numpy array of optimal gamma and beta values.
"""
dt = jnp.linspace(0.1, 1, steps)
energy = jnp.array([0.0] * steps)
for i in range(steps):
theta = self.computeParams(p, dt[i])
energy_ = optimization_wrapper(theta, state_prep, mes_kwargs)
energy = energy.at[i].set(energy_)
idx = jnp.argmin(energy)
dt_max = dt[idx]
return self.computeParams(p, dt_max)
else:
# Define optimization wrapper function to be minimized using QAOA
def optimization_wrapper(theta, qarg, qc, symbols, mes_kwargs):
"""Wrapper function for the optimization method used in QAOA.
This function calculates the value of the classical cost function.
Parameters
----------
theta : list
The list of angle parameters gamma and beta for the QAOA circuit.
qarg : :ref:`QuantumVariable` or :ref:`QuantumArray`
The quantum argument to which the quantum circuit is applied.
qc : :ref:`QuantumCircuit`
The compiled quantum circuit.
symbols : list
The list of symbols used in the quantum circuit.
mes_kwargs : dict
The keyword arguments for the measurement function.
Returns
-------
float
The expected value of the classical cost function.
"""
subs_dic = {symbols[i]: theta[i] for i in range(len(symbols))}
res_dic = qarg.get_measurement(subs_dic=subs_dic, precompiled_qc=qc, **mes_kwargs)
cl_cost = self.cl_cost_function(res_dic)
if self.callback:
self.optimization_costs.append(cl_cost)
return cl_cost
def tqa_angles(p, qarg, qc, symbols, mes_kwargs, steps=10):
"""Compute the optimal parameters for the Trotterized Quantum Annealing (`TQA <https://quantum-journal.org/papers/q-2021-07-01-491/>`_) algorithm.
The function first creates a linspace array `dt` from 0.1 to 1 with `steps` steps.
Then for each `dt_` in `dt`, it computes the parameters `x` using the `computeParams`
function and calculates the energy `energy_` using the `optimization_wrapper` function.
The energy values are stored in the `energy` list. The `dt_max` corresponding to the
minimum energy is found and used to compute the optimal parameters which are returned.
Parameters
----------
p : int
The number of partitions for the time interval.
qarg : :ref:`QuantumVariable` or :ref:`QuantumArray`
The quantum argument to which the quantum circuit is applied.
qc : :ref:`QuantumCircuit`
The quantum circuit for the specific problem instance.
symbols : list
The list of symbols in the quantum circuit.
mes_kwargs : dict
The measurement keyword arguments.
steps : int, optional
The number of steps for the linspace function, default is 10.
Returns
-------
np.array
A concatenated numpy array of optimal gamma and beta values.
"""
dt = np.linspace(0.1, 1, steps)
energy = []
for dt_ in dt:
theta = self.computeParams(p, dt_)
energy_ = optimization_wrapper(theta, qarg, qc, symbols, mes_kwargs)
energy.append(energy_)
idx = np.argmin(energy)
dt_max = dt[idx]
return self.computeParams(p, dt_max)
if check_for_tracing_mode():
def state_prep(theta):
qarg = qarg_prep()
# Prepare initial state - if no init_function is specified, prepare uniform superposition
if self.init_function is not None:
self.init_function(qarg)
elif init_type == "tqa": # Prepare the ground state (eigenvalue -1) of the X mixer
x(qarg)
h(qarg)
else:
h(qarg)
# Apply p layers of phase separators and mixers
for i in jrange(depth):
self.cost_operator(qarg, theta[i])
self.mixer(qarg, theta[i + depth])
return qarg
else:
qarg = qarg_prep()
compiled_qc, symbols = self.compile_circuit(qarg, depth, init_type)
# Initialization for optimization parameters
if init_point is None:
if init_type == "random":
# Random initialization
if check_for_tracing_mode():
key = jax.random.key(11)
init_point = jax.random.uniform(key=key, shape=(2 * depth,)) * jnp.pi / 2
else:
init_point = np.random.rand(2 * depth) * np.pi / 2
elif init_type == "tqa":
# TQA initialization
if check_for_tracing_mode():
init_point = tqa_angles(depth, state_prep, mes_kwargs)
else:
init_point = tqa_angles(depth, qarg, compiled_qc, symbols, mes_kwargs)
else:
raise Exception(f"Parameter initialization method {init_type} is not available.")
if check_for_tracing_mode():
res_sample = jasp_minimize(
optimization_wrapper,
init_point,
method=optimizer,
options=options,
args=(
state_prep,
mes_kwargs,
),
)
return res_sample.x, res_sample.fun
else:
res_sample = minimize(
optimization_wrapper,
init_point,
method=optimizer,
options=options,
args=(qarg, compiled_qc, symbols, mes_kwargs),
)
return res_sample.x, res_sample.fun
[docs]
def run(
self,
qarg,
depth,
mes_kwargs={},
max_iter=50,
init_type="random",
init_point=None,
optimizer="COBYLA",
options={},
):
r"""Run the specific QAOA problem instance.
Takes the quantum argument, the depth of the QAOA circuit, the measurement
keyword arguments (mes_kwargs) and the maximum number of iterations for the
optimization (max_iter).
Parameters
----------
qarg : :ref:`QuantumVariable` or :ref:`QuantumArray` or QuantumVariableTemplate or callable
The argument to which the QAOA circuit is applied, or a
QuantumVariableTemplate or function returning a :ref:`QuantumVariable`
or :ref:`QuantumArray` to which the QAOA circuit is applied.
depth : int
The amount of QAOA layers.
mes_kwargs : dict, optional
The keyword arguments for the measurement function. Default is an empty dictionary.
max_iter : int, optional
The maximum number of iterations for the optimization method. Default is 50.
init_type : string, optional
Specifies the way the initial optimization parameters are chosen. Available are ``random`` and ``tqa``. The default is ``random``:
The parameters are initialized uniformly at random in the interval $[0,\pi/2]$.
For ``tqa``, the parameters are chosen based on the `Trotterized Quantum Annealing <https://quantum-journal.org/papers/q-2021-07-01-491/>`_ protocol.
If ``tqa`` is chosen, and no ``init_function`` for the :ref:`QAOAProblem` is specified, the $\ket{-}^n$ state is prepared (the ground state for the X mixer).
init_point : ndarray, shape (n,), optional
Specifies the initial optimization parameters.
optimizer : str, optional
Specifies the `SciPy optimization routine <https://docs.scipy.org/doc/scipy/reference/generated/scipy.optimize.minimize.html>`_.
Available are, e.g., ``COBYLA``, ``COBYQA``, ``Nelder-Mead``. The Default is ``COBYLA``.
In tracing mode (i.e. Jasp) Jax-traceable :ref:`optimization routines <optimization_tools>` must be utilized.
Available are ``COBYLA``, ``SPSA``.
options : dict
A dictionary of solver options.
Returns
-------
dict or jax.Array
The optimal result after running QAOA problem for a specific problem instance. It contains the measurement results after applying the optimal QAOA circuit to the quantum argument.
"""
if callable(qarg):
qarg_prep = qarg
elif isinstance(qarg, QuantumArray):
template = qarg.qtype.template()
shape = qarg.shape
def qarg_prep():
return QuantumArray(qtype=template.construct(), shape=shape)
elif isinstance(qarg, QuantumVariableTemplate):
def qarg_prep():
return qarg.construct()
else:
template = qarg.template()
def qarg_prep():
return template.construct()
# Set default options
options["maxiter"] = max_iter
if "shots" not in mes_kwargs:
mes_kwargs["shots"] = 5000
# Delete callback
self.optimization_params = []
self.optimization_costs = []
opt_theta, opt_res = self.optimization_routine(
qarg_prep, depth, mes_kwargs, init_type, init_point, optimizer, options
)
def state_prep(theta):
qarg = qarg_prep()
# Prepare initial state - if no init_function is specified, prepare uniform superposition
if self.init_function is not None:
self.init_function(qarg)
elif init_type == "tqa": # Prepare the ground state (eigenvalue -1) of the X mixer
x(qarg)
h(qarg)
else:
h(qarg)
# Apply p layers of phase separators and mixers
for i in jrange(depth):
self.cost_operator(qarg, theta[i])
self.mixer(qarg, theta[i + depth])
return qarg
if check_for_tracing_mode():
res_sample = sample(state_prep, shots=mes_kwargs["shots"])(opt_theta)
else:
qarg = state_prep(opt_theta)
res_sample = qarg.get_measurement(**mes_kwargs)
return res_sample
[docs]
def train_function(
self,
qarg,
depth,
mes_kwargs={},
max_iter=50,
init_type="random",
init_point=None,
optimizer="COBYLA",
options={},
):
r"""Train a circuit with a given ``QAOAProblem`` instance.
Returns a function that can be applied to a ``QuantumVariable``, such that
it represents a solution to the problem instance. When applied to a
``QuantumVariable``, the function therefore prepares the state
.. math::
\ket{\psi_p}=U_M(B,\beta_p)U_P(C,\gamma_p)\dotsb U_M(B,\beta_1)U_P(C,\gamma_1)\ket{\psi_0}
with optimized parameters $\gamma, \beta$.
Parameters
----------
qarg : :ref:`QuantumVariable` or :ref:`QuantumArray` or QuantumVariableTemplate or callable
The argument to which the QAOA circuit is applied, or a
QuantumVariableTemplate or function returning a :ref:`QuantumVariable`
or :ref:`QuantumArray` to which the QAOA circuit is applied.
depth : int
The amount of QAOA layers.
mes_kwargs : dict, optional
The keyword arguments for the measurement function. Default is an empty dictionary.
max_iter : int, optional
The maximum number of iterations for the optimization method. Default is 50.
init_type : string, optional
Specifies the way the initial optimization parameters are chosen. Available are ``random`` and ``tqa``. The default is ``random``:
The parameters are initialized uniformly at random in the interval $[0,\pi/2]$.
For ``tqa``, the parameters are chosen based on the `Trotterized Quantum Annealing <https://quantum-journal.org/papers/q-2021-07-01-491/>`_ protocol.
If ``tqa`` is chosen, and no ``init_function`` for the :ref:`QAOAProblem` is specified, the $\ket{-}^n$ state is prepared (the ground state for the X mixer).
init_point : ndarray, shape (n,), optional
Specifies the initial optimization parameters.
optimizer : str, optional
Specifies the `SciPy optimization routine <https://docs.scipy.org/doc/scipy/reference/generated/scipy.optimize.minimize.html>`_.
Available are, e.g., ``COBYLA``, ``COBYQA``, ``Nelder-Mead``. The Default is ``COBYLA``.
In tracing mode (i.e. Jasp) Jax-traceable :ref:`optimization routines <optimization_tools>` must be utilized.
Available are ``COBYLA``, ``SPSA``.
options : dict
A dictionary of solver options.
Returns
-------
circuit_generator : function
A function that can be applied to a ``QuantumVariable`` , with optimized parameters for the problem instance. The ``QuantumVariable`` then represents a solution of the problem.
Examples
--------
We create a :ref:`MaxIndepSet <maxIndepSetQAOA>` instance and train a ciruit with the :ref:`QAOAProblem` instance.
::
from qrisp import QuantumVariable
from qrisp.qaoa import QAOAProblem, RZ_mixer, create_max_indep_set_cl_cost_function, create_max_indep_set_mixer, max_indep_set_init_function
import networkx as nx
import matplotlib.pyplot as plt
G = nx.erdos_renyi_graph(9, 0.5, seed = 133)
qaoa_instance = QAOAProblem(cost_operator=RZ_mixer,
mixer=create_max_indep_set_mixer(G),
cl_cost_function=create_max_indep_set_cl_cost_function(G),
init_function=max_indep_set_init_function)
# create a blueprint-qv to train the circuit with the problem instance
qarg = QuantumVariable(G.number_of_nodes())
training_func = qaoa_instance.train_function(qarg, depth=5)
# apply the trained function to a new qv
qarg_trained = QuantumVariable(G.number_of_nodes())
training_func(qarg_trained)
# get the measurement results
opt_res = qarg_trained.get_measurement()
cl_cost = create_max_indep_set_cl_cost_function(G)
print("5 most likely solutions")
max_five = sorted(opt_res.items(), key=lambda item: item[1], reverse=True)[:5]
for res, prob in max_five:
print([index for index, value in enumerate(res) if value == '1'], prob, cl_cost({res : 1}))
"""
if callable(qarg):
qarg_prep = qarg
elif isinstance(qarg, QuantumArray):
template = qarg.qtype.template()
shape = qarg.shape
def qarg_prep():
return QuantumArray(qtype=template.construct(), shape=shape)
elif isinstance(qarg, QuantumVariableTemplate):
def qarg_prep():
return qarg.construct()
else:
template = qarg.template()
def qarg_prep():
return template.construct()
# Set default options
options["maxiter"] = max_iter
if "shots" not in mes_kwargs:
mes_kwargs["shots"] = 5000
opt_theta, opt_res = self.optimization_routine(
qarg_prep, depth, mes_kwargs, init_type, init_point, optimizer, options
)
def circuit_generator(qarg_gen):
# Prepare initial state - if no init_function is specified, prepare uniform superposition
if self.init_function is not None:
self.init_function(qarg_gen)
elif init_type == "tqa": # Prepare the ground state (eigenvalue -1) of the X mixer
x(qarg_gen)
h(qarg_gen)
else:
h(qarg_gen)
for i in jrange(depth):
self.cost_operator(qarg_gen, opt_theta[i])
self.mixer(qarg_gen, opt_theta[i + depth])
return circuit_generator
[docs]
def benchmark(
self,
qarg,
depth_range,
shot_range,
iter_range,
optimal_solution,
repetitions=1,
mes_kwargs={},
init_type="random",
optimizer="COBYLA",
options={},
):
r"""This method enables convenient data collection regarding performance of the implementation.
Parameters
----------
qarg : :ref:`QuantumVariable` or :ref:`QuantumArray` or QuantumVariableTemplate or callable
The argument to which the QAOA circuit is applied, or a
QuantumVariableTemplate or function returning a :ref:`QuantumVariable`
or :ref:`QuantumArray` to which the QAOA circuit is applied.
Compare to the :meth:`.run <qrisp.qaoa.QAOAProblem.run>` method.
depth_range : list[int]
A list of integers indicating, which depth parameters should be explored. Depth means the amount of QAOA layers.
shot_range : list[int]
A list of integers indicating, which shots parameters should be explored. Shots means the amount of repetitions, the backend performs per iteration.
iter_range : list[int]
A list of integers indicating, what iterations parameter should be explored. Iterations means the amount of backend calls, the optimizer is allowed to do.
optimal_solution : -
The optimal solution to the problem. Should have the same type as the keys of the result of ``qarg.get_measurement()``.
repetitions : int, optional
The amount of repetitions, each parameter constellation should go though. Can be used to get a better statistical significance. The default is 1.
mes_kwargs : dict, optional
The keyword arguments, that are used for the ``qarg.get_measurement``. The default is {}.
init_type : string, optional
Specifies the way the initial optimization parameters are chosen. Available are ``random`` and ``tqa``. The default is ``random``:
The parameters are initialized uniformly at random in the interval $[0,\pi/2]$.
For ``tqa``, the parameters are chosen based on the `Trotterized Quantum Annealing <https://quantum-journal.org/papers/q-2021-07-01-491/>`_ protocol.
If ``tqa`` is chosen, and no ``init_function`` for the :ref:`QAOAProblem` is specified, the $\ket{-}^n$ state is prepared (the ground state for the X mixer).
optimizer : str, optional
Specifies the `SciPy optimization routine <https://docs.scipy.org/doc/scipy/reference/generated/scipy.optimize.minimize.html>`_.
Available are, e.g., ``COBYLA``, ``COBYQA``, ``Nelder-Mead``. The Default is ``COBYLA``.
options : dict
A dictionary of solver options.
Returns
-------
:ref:`QAOABenchmark`
The results of the benchmark.
Examples
--------
We create a MaxCut instance and benchmark several parameters
::
from qrisp import *
from networkx import Graph
G = Graph()
G.add_edges_from([[0,3],[0,4],[1,3],[1,4],[2,3],[2,4]])
from qrisp.qaoa import maxcut_problem
max_cut_instance = maxcut_problem(G)
benchmark_data = max_cut_instance.benchmark(QuantumVariable(5),
depth_range = [3,4,5],
shot_range = [5000, 10000],
iter_range = [25, 50],
optimal_solution = "11100",
repetitions = 2
)
We can investigate the data by calling ``visualize``:
::
benchmark_data.visualize()
.. image:: benchmark_plot.png
The :ref:`QAOABenchmark` class contains a variety of methods to help
you drawing conclusions from the collected data. Make sure to check them out!
"""
if callable(qarg):
qarg_prep = qarg
elif isinstance(qarg, QuantumArray):
template = qarg.qtype.template()
shape = qarg.shape
def qarg_prep():
return QuantumArray(qtype=template.construct(), shape=shape)
elif isinstance(qarg, QuantumVariableTemplate):
def qarg_prep():
return qarg.construct()
else:
template = qarg.template()
def qarg_prep():
return template.construct()
data_dict = {
"layer_depth": [],
"circuit_depth": [],
"qubit_amount": [],
"shots": [],
"iterations": [],
"counts": [],
"runtime": [],
"cl_cost": [],
}
for p in depth_range:
for s in shot_range:
for it in iter_range:
for k in range(repetitions):
start_time = time.time()
temp_mes_kwargs = dict(mes_kwargs)
temp_mes_kwargs["shots"] = s
counts = self.run(
qarg_prep,
depth=p,
max_iter=it,
mes_kwargs=temp_mes_kwargs,
init_type=init_type,
optimizer=optimizer,
options=options,
)
final_time = time.time() - start_time
compiled_qc, _ = self.compile_circuit(qarg_prep(), depth=p)
data_dict["layer_depth"].append(p)
data_dict["circuit_depth"].append(compiled_qc.depth())
data_dict["qubit_amount"].append(compiled_qc.num_qubits())
data_dict["shots"].append(s)
data_dict["iterations"].append(it)
data_dict["counts"].append(counts)
data_dict["runtime"].append(final_time)
return QAOABenchmark(data_dict, optimal_solution, self.cl_cost_function)
[docs]
def visualize_cost(self):
"""Visualizes the cost during the optimization process. Can only be used if ``callback=True``."""
import matplotlib.pyplot as plt
if not self.callback:
raise Exception("Visualization can only be performed for a QAOA instance with callback=True")
x = list(range(len(self.optimization_costs)))
y = self.optimization_costs
plt.scatter(
x,
y,
color="#20306f",
marker="o",
linestyle="solid",
linewidth=1,
label="QAOA cost",
)
plt.xlabel("Iterations", fontsize=15, color="#444444")
plt.ylabel("Cost", fontsize=15, color="#444444")
plt.tick_params(axis="both", labelsize=12)
plt.legend(fontsize=12, labelcolor="#444444")
plt.grid()
plt.show()