Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow callables as optimizers in VQE and QAOA #7191

Merged
merged 17 commits into from
Mar 15, 2022
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions qiskit/algorithms/minimum_eigen_solvers/qaoa.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
from qiskit.utils.quantum_instance import QuantumInstance
from qiskit.utils.validation import validate_min
from qiskit.circuit.library.n_local.qaoa_ansatz import QAOAAnsatz
from qiskit.algorithms.minimum_eigen_solvers.vqe import VQE
from qiskit.algorithms.minimum_eigen_solvers.vqe import VQE, MINIMIZER


class QAOA(VQE):
Expand Down Expand Up @@ -55,7 +55,7 @@ class QAOA(VQE):

def __init__(
self,
optimizer: Optimizer = None,
optimizer: Optional[Union[Optimizer, MINIMIZER]] = None,
reps: int = 1,
initial_state: Optional[QuantumCircuit] = None,
mixer: Union[QuantumCircuit, OperatorBase] = None,
Expand All @@ -69,7 +69,8 @@ def __init__(
) -> None:
"""
Args:
optimizer: A classical optimizer.
optimizer: A classical optimizer, see also :class:`~qiskit.algorithms.VQE` for
more details on the possible types.
reps: the integer parameter :math:`p` as specified in https://arxiv.org/abs/1411.4028,
Has a minimum valid value of 1.
initial_state: An optional initial state to prepend the QAOA circuit with
Expand Down
108 changes: 85 additions & 23 deletions qiskit/algorithms/minimum_eigen_solvers/vqe.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import warnings
from time import time
import numpy as np
import scipy

from qiskit.circuit import QuantumCircuit, Parameter
from qiskit.circuit.library import RealAmplitudes
Expand All @@ -40,14 +41,30 @@
from qiskit.utils.backend_utils import is_aer_provider
from qiskit.utils import QuantumInstance, algorithm_globals
from ..list_or_dict import ListOrDict
from ..optimizers import Optimizer, SLSQP
from ..optimizers import Optimizer, SLSQP, OptimizerResult
from ..variational_algorithm import VariationalAlgorithm, VariationalResult
from .minimum_eigen_solver import MinimumEigensolver, MinimumEigensolverResult
from ..exceptions import AlgorithmError

logger = logging.getLogger(__name__)


OBJECTIVE = Callable[[np.ndarray], float]
GRADIENT = Callable[[np.ndarray], np.ndarray]
RESULT = Union[scipy.optimize.OptimizeResult, OptimizerResult]
BOUNDS = List[Tuple[float, float]]

MINIMIZER = Callable[
[
OBJECTIVE, # the objective function to minimize (the energy in the case of the VQE)
np.ndarray, # the initial point for the optimization
Optional[GRADIENT], # the gradient of the objective function
Optional[BOUNDS], # parameters bounds for the optimization
],
RESULT, # a result object (either SciPy's or Qiskit's)
]

Comment on lines +52 to +66
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this type hint actually correct for any SciPy minimiser? It seems to imply that the first four arguments are positional, but the first four positional arguments of their minimisers are (objective, x0, args, jac).

In an ideal world, I think we'd put this in __init__.py, and write some documentation in the docstring about the types. It can have a Sphinx cross-ref (.. _algorithms_minimum_eigensolvers_minimizer:, then you reference with :ref:algorithms_minimum_eigensolvers_minimizer`), then the various class docstrings could all link to it. That said, I don't think you need to worry much about it right now.

Copy link
Contributor Author

@Cryoris Cryoris Feb 23, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In fact the callable must have the named arguments fun, x0, jac, bounds but I didn't quite know how to put that in a type hint using only what's available now 🤔 I updated the class docs to highlight this

Copy link
Contributor

@ikkoham ikkoham Feb 25, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

how about using Callback Protocol? PEP544

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We tried that but it's only supported from Python 3.8 onwards, see the discussion above: #7191 (comment)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Protocol is back ported by typing-extensions. https://pypi.org/project/typing-extensions/ so you can use it.
(I'm sorry I overlooked...)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah but that would create an additional dependency to a new package, so we didn't add it (yet) 😄


class VQE(VariationalAlgorithm, MinimumEigensolver):
r"""The Variational Quantum Eigensolver algorithm.

Expand Down Expand Up @@ -81,12 +98,46 @@ class VQE(VariationalAlgorithm, MinimumEigensolver):
will default it to :math:`-2\pi`; similarly, if the ansatz returns ``None``
as the upper bound, the default value will be :math:`2\pi`.

The optimizer can either be one of Qiskit's optimizers, such as
:class:`~qiskit.algorithms.optimizers.SPSA` or a callable with the following signature:

.. note::

The callable _must_ have the argument names ``fun, x0, jac, bounds`` as indicated
in the following code block.

.. code-block::python

from qiskit.algorithms.optimizers import OptimizerResult

def my_minimizer(fun, x0, jac=None, bounds=None) -> OptimizerResult:
# Note that the callable *must* have these argument names!
# Args:
# fun (callable): the function to minimize
# x0 (np.ndarray): the initial point for the optimization
# jac (callable, optional): the gradient of the objective function
# bounds (list, optional): a list of tuples specifying the parameter bounds

result = OptimizerResult()
result.x = # optimal parameters
result.fun = # optimal function value
return result

The above signature also allows to directly pass any SciPy minimizer, for instance as

.. code-block::python

from functools import partial
from scipy.optimize import minimize

optimizer = partial(minimize, method="L-BFGS-B")

"""

def __init__(
self,
ansatz: Optional[QuantumCircuit] = None,
optimizer: Optional[Optimizer] = None,
optimizer: Optional[Union[Optimizer, MINIMIZER]] = None,
initial_point: Optional[np.ndarray] = None,
gradient: Optional[Union[GradientBase, Callable]] = None,
expectation: Optional[ExpectationBase] = None,
Expand All @@ -99,7 +150,8 @@ def __init__(

Args:
ansatz: A parameterized circuit used as Ansatz for the wave function.
optimizer: A classical optimizer.
optimizer: A classical optimizer. Can either be a Qiskit optimizer or a callable
that takes an array as input and returns a Qiskit or SciPy optimization result.
initial_point: An optional initial point (i.e. initial parameter values)
for the optimizer. If ``None`` then VQE will look to the ansatz for a preferred
point and if not will simply compute a random one.
Expand Down Expand Up @@ -298,7 +350,9 @@ def optimizer(self, optimizer: Optional[Optimizer]):
if optimizer is None:
optimizer = SLSQP()

optimizer.set_max_evals_grouped(self.max_evals_grouped)
if isinstance(optimizer, Optimizer):
optimizer.set_max_evals_grouped(self.max_evals_grouped)

self._optimizer = optimizer

@property
Expand Down Expand Up @@ -333,7 +387,10 @@ def print_settings(self):
else:
ret += "ansatz has not been set"
ret += "===============================================================\n"
ret += f"{self._optimizer.setting}"
if callable(self.optimizer):
ret += "Optimizer is custom callable\n"
else:
ret += f"{self._optimizer.setting}"
ret += "===============================================================\n"
return ret

Expand Down Expand Up @@ -534,26 +591,31 @@ def compute_minimum_eigenvalue(

start_time = time()

# keep this until Optimizer.optimize is removed
try:
opt_result = self.optimizer.minimize(
if callable(self.optimizer):
opt_result = self.optimizer( # pylint: disable=not-callable
fun=energy_evaluation, x0=initial_point, jac=gradient, bounds=bounds
)
Cryoris marked this conversation as resolved.
Show resolved Hide resolved
except AttributeError:
# self.optimizer is an optimizer with the deprecated interface that uses
# ``optimize`` instead of ``minimize```
warnings.warn(
"Using an optimizer that is run with the ``optimize`` method is "
"deprecated as of Qiskit Terra 0.19.0 and will be unsupported no "
"sooner than 3 months after the release date. Instead use an optimizer "
"providing ``minimize`` (see qiskit.algorithms.optimizers.Optimizer).",
DeprecationWarning,
stacklevel=2,
)

opt_result = self.optimizer.optimize(
len(initial_point), energy_evaluation, gradient, bounds, initial_point
)
else:
# keep this until Optimizer.optimize is removed
try:
opt_result = self.optimizer.minimize(
fun=energy_evaluation, x0=initial_point, jac=gradient, bounds=bounds
)
except AttributeError:
# self.optimizer is an optimizer with the deprecated interface that uses
# ``optimize`` instead of ``minimize```
warnings.warn(
"Using an optimizer that is run with the ``optimize`` method is "
"deprecated as of Qiskit Terra 0.19.0 and will be unsupported no "
"sooner than 3 months after the release date. Instead use an optimizer "
"providing ``minimize`` (see qiskit.algorithms.optimizers.Optimizer).",
DeprecationWarning,
stacklevel=2,
)

opt_result = self.optimizer.optimize(
len(initial_point), energy_evaluation, gradient, bounds, initial_point
)

eval_time = time() - start_time

Expand Down
31 changes: 31 additions & 0 deletions releasenotes/notes/vqe-optimizer-callables-1aa14d78c855d383.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
---
features:
- |
Allow callables as optimizers in :class:`~qiskit.algorithms.VQE` and
:class:`~qiskit.algorithms.QAOA`. Now, the optimizer can either be one of Qiskit's optimizers,
such as :class:`~qiskit.algorithms.optimizers.SPSA` or a callable with the following signature:

.. code-block::python
Cryoris marked this conversation as resolved.
Show resolved Hide resolved

from qiskit.algorithms.optimizers import OptimizerResult

def my_optimizer(fun, x0, jac=None, bounds=None) -> OptimizerResult:
# Args:
# fun (callable): the function to minimize
# x0 (np.ndarray): the initial point for the optimization
# jac (callable, optional): the gradient of the objective function
# bounds (list, optional): a list of tuples specifying the parameter bounds

result = OptimizerResult()
result.x = # optimal parameters
result.fun = # optimal function value
Comment on lines +19 to +21
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Kind of weird that you don't construct the whole class in one go at initialisation time, but I gave up that fight in Python long ago.

return result

The above signature also allows to directly pass any SciPy minimizer, for instance as
Cryoris marked this conversation as resolved.
Show resolved Hide resolved

.. code-block::python
Cryoris marked this conversation as resolved.
Show resolved Hide resolved

from functools import partial
from scipy.optimize import minimize

optimizer = partial(minimize, method="L-BFGS-B)
Cryoris marked this conversation as resolved.
Show resolved Hide resolved
13 changes: 12 additions & 1 deletion test/python/algorithms/test_qaoa.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,12 @@
import unittest
from test.python.algorithms import QiskitAlgorithmsTestCase

from functools import partial
import math
import numpy as np
import retworkx as rx
from scipy.optimize import minimize as scipy_minimize
from ddt import ddt, idata, unpack
import retworkx as rx

from qiskit.algorithms import QAOA
from qiskit.algorithms.optimizers import COBYLA, NELDER_MEAD
Expand Down Expand Up @@ -309,6 +311,15 @@ def test_qaoa_construct_circuit_update(self):
circ4 = qaoa.construct_circuit([0, 0], I ^ Z)[0]
self.assertEqual(circ4, ref)

def test_optimizer_scipy_callable(self):
"""Test passing a SciPy optimizer directly as callable."""
qaoa = QAOA(
optimizer=partial(scipy_minimize, method="Nelder-Mead", options={"maxiter": 2}),
quantum_instance=self.statevector_simulator,
)
result = qaoa.compute_minimum_eigenvalue(Z)
self.assertEqual(result.cost_function_evals, 4)

def _get_operator(self, weight_matrix):
"""Generate Hamiltonian for the max-cut problem of a graph.

Expand Down
30 changes: 30 additions & 0 deletions test/python/algorithms/test_vqe.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@

from functools import partial
import numpy as np
from scipy.optimize import minimize as scipy_minimize
from ddt import data, ddt, unpack

from qiskit import BasicAer, QuantumCircuit
Expand All @@ -31,6 +32,7 @@
SLSQP,
SPSA,
TNC,
OptimizerResult,
)
from qiskit.circuit.library import EfficientSU2, RealAmplitudes, TwoLocal
from qiskit.exceptions import MissingOptionalLibraryError
Expand Down Expand Up @@ -69,6 +71,16 @@ def run(self, dag):
logging.getLogger(logger).info(self.message)


# pylint: disable=invalid-name, unused-argument
def _mock_optimizer(fun, x0, jac=None, bounds=None) -> OptimizerResult:
"""A mock of a callable that can be used as minimizer in the VQE."""
result = OptimizerResult()
result.x = np.zeros_like(x0)
result.fun = fun(result.x)
result.nit = 0
return result


@ddt
class TestVQE(QiskitAlgorithmsTestCase):
"""Test VQE"""
Expand Down Expand Up @@ -489,6 +501,24 @@ def test_set_optimizer_to_none(self):
vqe.optimizer = None
self.assertIsInstance(vqe.optimizer, SLSQP)

def test_optimizer_scipy_callable(self):
"""Test passing a SciPy optimizer directly as callable."""
vqe = VQE(
optimizer=partial(scipy_minimize, method="L-BFGS-B", options={"maxiter": 2}),
quantum_instance=self.statevector_simulator,
)
result = vqe.compute_minimum_eigenvalue(Z)
self.assertEqual(result.cost_function_evals, 20)

def test_optimizer_callable(self):
"""Test passing a optimizer directly as callable."""
ansatz = RealAmplitudes(1, reps=1)
vqe = VQE(
ansatz=ansatz, optimizer=_mock_optimizer, quantum_instance=self.statevector_simulator
)
result = vqe.compute_minimum_eigenvalue(Z)
self.assertTrue(np.all(result.optimal_point == np.zeros(ansatz.num_parameters)))

def test_aux_operators_list(self):
"""Test list-based aux_operators."""
wavefunction = self.ry_wavefunction
Expand Down