# This code is part of Qiskit.
#
# (C) Copyright IBM 2018, 2021.
#
# This code is licensed under the Apache License, Version 2.0. You may
# obtain a copy of this license in the LICENSE.txt file in the root directory
# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.
#
# Any modifications or derivative works of this code must retain this
# copyright notice, and modified files need to carry a notice indicating
# that they have been altered from the originals.
"""A generalized QAOA quantum circuit with a support of custom initial states and mixers."""
# pylint: disable=cyclic-import
from __future__ import annotations
import numpy as np
from qiskit.circuit.library.evolved_operator_ansatz import EvolvedOperatorAnsatz, _is_pauli_identity
from qiskit.circuit.parametervector import ParameterVector
from qiskit.circuit.quantumcircuit import QuantumCircuit
from qiskit.circuit.quantumregister import QuantumRegister
from qiskit.quantum_info import SparsePauliOp
[docs]class QAOAAnsatz(EvolvedOperatorAnsatz):
"""A generalized QAOA quantum circuit with a support of custom initial states and mixers.
References:
[1]: Farhi et al., A Quantum Approximate Optimization Algorithm.
`arXiv:1411.4028 <https://arxiv.org/pdf/1411.4028>`_
"""
def __init__(
self,
cost_operator=None,
reps: int = 1,
initial_state: QuantumCircuit | None = None,
mixer_operator=None,
name: str = "QAOA",
flatten: bool | None = None,
):
r"""
Args:
cost_operator (BaseOperator or OperatorBase, optional): The operator
representing the cost of the optimization problem, denoted as :math:`U(C, \gamma)`
in the original paper. Must be set either in the constructor or via property setter.
reps (int): The integer parameter p, which determines the depth of the circuit,
as specified in the original paper, default is 1.
initial_state (QuantumCircuit, optional): An optional initial state to use.
If `None` is passed then a set of Hadamard gates is applied as an initial state
to all qubits.
mixer_operator (BaseOperator or OperatorBase or QuantumCircuit, optional): An optional
custom mixer to use instead of the global X-rotations, denoted as :math:`U(B, \beta)`
in the original paper. Can be an operator or an optionally parameterized quantum
circuit.
name (str): A name of the circuit, default 'qaoa'
flatten: Set this to ``True`` to output a flat circuit instead of nesting it inside multiple
layers of gate objects. By default currently the contents of
the output circuit will be wrapped in nested objects for
cleaner visualization. However, if you're using this circuit
for anything besides visualization its **strongly** recommended
to set this flag to ``True`` to avoid a large performance
overhead for parameter binding.
"""
super().__init__(reps=reps, name=name, flatten=flatten)
self._cost_operator = None
self._reps = reps
self._initial_state: QuantumCircuit | None = initial_state
self._mixer = mixer_operator
# set this circuit as a not-built circuit
self._bounds: list[tuple[float | None, float | None]] | None = None
# store cost operator and set the registers if the operator is not None
self.cost_operator = cost_operator
def _check_configuration(self, raise_on_failure: bool = True) -> bool:
"""Check if the current configuration is valid."""
valid = True
if not super()._check_configuration(raise_on_failure):
return False
if self.cost_operator is None:
valid = False
if raise_on_failure:
raise ValueError(
"The operator representing the cost of the optimization problem is not set"
)
if self.initial_state is not None and self.initial_state.num_qubits != self.num_qubits:
valid = False
if raise_on_failure:
raise ValueError(
"The number of qubits of the initial state {} does not match "
"the number of qubits of the cost operator {}".format(
self.initial_state.num_qubits, self.num_qubits
)
)
if self.mixer_operator is not None and self.mixer_operator.num_qubits != self.num_qubits:
valid = False
if raise_on_failure:
raise ValueError(
"The number of qubits of the mixer {} does not match "
"the number of qubits of the cost operator {}".format(
self.mixer_operator.num_qubits, self.num_qubits
)
)
return valid
@property
def parameter_bounds(self) -> list[tuple[float | None, float | None]] | None:
"""The parameter bounds for the unbound parameters in the circuit.
Returns:
A list of pairs indicating the bounds, as (lower, upper). None indicates an unbounded
parameter in the corresponding direction. If None is returned, problem is fully
unbounded.
"""
if self._bounds is not None:
return self._bounds
# if the mixer is a circuit, we set no bounds
if isinstance(self.mixer_operator, QuantumCircuit):
return None
# default bounds: None for gamma (cost operator), [0, 2pi] for gamma (mixer operator)
beta_bounds = (0, 2 * np.pi)
gamma_bounds = (None, None)
bounds: list[tuple[float | None, float | None]] = []
if not _is_pauli_identity(self.mixer_operator):
bounds += self.reps * [beta_bounds]
if not _is_pauli_identity(self.cost_operator):
bounds += self.reps * [gamma_bounds]
return bounds
@parameter_bounds.setter
def parameter_bounds(self, bounds: list[tuple[float | None, float | None]] | None) -> None:
"""Set the parameter bounds.
Args:
bounds: The new parameter bounds.
"""
self._bounds = bounds
@property
def operators(self) -> list:
"""The operators that are evolved in this circuit.
Returns:
List[Union[BaseOperator, OperatorBase, QuantumCircuit]]: The operators to be evolved
(and circuits) in this ansatz.
"""
return [self.cost_operator, self.mixer_operator]
@property
def cost_operator(self):
"""Returns an operator representing the cost of the optimization problem.
Returns:
BaseOperator or OperatorBase: cost operator.
"""
return self._cost_operator
@cost_operator.setter
def cost_operator(self, cost_operator) -> None:
"""Sets cost operator.
Args:
cost_operator (BaseOperator or OperatorBase, optional): cost operator to set.
"""
self._cost_operator = cost_operator
self.qregs = [QuantumRegister(self.num_qubits, name="q")]
self._invalidate()
@property
def reps(self) -> int:
"""Returns the `reps` parameter, which determines the depth of the circuit."""
return self._reps
@reps.setter
def reps(self, reps: int) -> None:
"""Sets the `reps` parameter."""
self._reps = reps
self._invalidate()
@property
def initial_state(self) -> QuantumCircuit | None:
"""Returns an optional initial state as a circuit"""
if self._initial_state is not None:
return self._initial_state
# if no initial state is passed and we know the number of qubits, then initialize it.
if self.num_qubits > 0:
initial_state = QuantumCircuit(self.num_qubits)
initial_state.h(range(self.num_qubits))
return initial_state
# otherwise we cannot provide a default
return None
@initial_state.setter
def initial_state(self, initial_state: QuantumCircuit | None) -> None:
"""Sets initial state."""
self._initial_state = initial_state
self._invalidate()
# we can't directly specify OperatorBase as a return type, it causes a circular import
# and pylint objects if return type is not documented
@property
def mixer_operator(self):
"""Returns an optional mixer operator expressed as an operator or a quantum circuit.
Returns:
BaseOperator or OperatorBase or QuantumCircuit, optional: mixer operator or circuit.
"""
if self._mixer is not None:
return self._mixer
# if no mixer is passed and we know the number of qubits, then initialize it.
if self.cost_operator is not None:
# local imports to avoid circular imports
num_qubits = self.cost_operator.num_qubits
# Mixer is just a sum of single qubit X's on each qubit. Evolving by this operator
# will simply produce rx's on each qubit.
mixer_terms = [
("I" * left + "X" + "I" * (num_qubits - left - 1), 1) for left in range(num_qubits)
]
mixer = SparsePauliOp.from_list(mixer_terms)
return mixer
# otherwise we cannot provide a default
return None
@mixer_operator.setter
def mixer_operator(self, mixer_operator) -> None:
"""Sets mixer operator.
Args:
mixer_operator (BaseOperator or OperatorBase or QuantumCircuit, optional): mixer
operator or circuit to set.
"""
self._mixer = mixer_operator
self._invalidate()
@property
def num_qubits(self) -> int:
if self._cost_operator is None:
return 0
return self._cost_operator.num_qubits
def _build(self):
"""If not already built, build the circuit."""
if self._is_built:
return
super()._build()
# keep old parameter order: first cost operator, then mixer operators
num_cost = 0 if _is_pauli_identity(self.cost_operator) else 1
if isinstance(self.mixer_operator, QuantumCircuit):
num_mixer = self.mixer_operator.num_parameters
else:
num_mixer = 0 if _is_pauli_identity(self.mixer_operator) else 1
betas = ParameterVector("β", self.reps * num_mixer)
gammas = ParameterVector("γ", self.reps * num_cost)
# Create a permutation to take us from (cost_1, mixer_1, cost_2, mixer_2, ...)
# to (cost_1, cost_2, ..., mixer_1, mixer_2, ...), or if the mixer is a circuit
# with more than 1 parameters, from (cost_1, mixer_1a, mixer_1b, cost_2, ...)
# to (cost_1, cost_2, ..., mixer_1a, mixer_1b, mixer_2a, mixer_2b, ...)
reordered = []
for rep in range(self.reps):
reordered.extend(gammas[rep * num_cost : (rep + 1) * num_cost])
reordered.extend(betas[rep * num_mixer : (rep + 1) * num_mixer])
self.assign_parameters(dict(zip(self.ordered_parameters, reordered)), inplace=True)