From 0a35c1a281837eab9a403343a1d797ec3d1ed847 Mon Sep 17 00:00:00 2001 From: Declan Millar Date: Wed, 31 Aug 2022 17:13:04 +0100 Subject: [PATCH 001/100] Add minimally working VQE with estimator primitive implementation. No gradients, no tests etc. --- .../algorithms/minimum_eigensolvers/__init__ | 23 ++ .../minimum_eigensolver.py | 69 +++++ .../numpy_minimum_eigensolver.py | 87 ++++++ qiskit/algorithms/minimum_eigensolvers/vqe.py | 275 ++++++++++++++++++ 4 files changed, 454 insertions(+) create mode 100644 qiskit/algorithms/minimum_eigensolvers/__init__ create mode 100644 qiskit/algorithms/minimum_eigensolvers/minimum_eigensolver.py create mode 100644 qiskit/algorithms/minimum_eigensolvers/numpy_minimum_eigensolver.py create mode 100644 qiskit/algorithms/minimum_eigensolvers/vqe.py diff --git a/qiskit/algorithms/minimum_eigensolvers/__init__ b/qiskit/algorithms/minimum_eigensolvers/__init__ new file mode 100644 index 000000000000..2d48dddaa422 --- /dev/null +++ b/qiskit/algorithms/minimum_eigensolvers/__init__ @@ -0,0 +1,23 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2022. +# +# 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. + +"""The minimum eigensolvers package.""" + +from .minimum_eigensolver import MinimumEigensolver, MinimumEigensolverResult +from .vqe import VQE, VQEResult + +__all__ = [ + "MinimumEigensolver", + "MinimumEigensolverResult", + "VQE", + "VQEResult", +] diff --git a/qiskit/algorithms/minimum_eigensolvers/minimum_eigensolver.py b/qiskit/algorithms/minimum_eigensolvers/minimum_eigensolver.py new file mode 100644 index 000000000000..50532a541d80 --- /dev/null +++ b/qiskit/algorithms/minimum_eigensolvers/minimum_eigensolver.py @@ -0,0 +1,69 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2022. +# +# 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. + +"""The minimum eigensolver interface and result.""" + +from abc import ABC, abstractmethod +from dataclasses import dataclass + +from qiskit.opflow import OperatorBase +from ..algorithm_result import AlgorithmResult +from ..list_or_dict import ListOrDict + + +class MinimumEigensolver(ABC): + """The minimum eigensolver interface. + + Algorithms that can compute a minimum eigenvalue for an operator may implement this interface to + allow different algorithms to be used interchangeably. + """ + + @abstractmethod + def compute_minimum_eigenvalue( + self, operator: OperatorBase, aux_operators: ListOrDict[OperatorBase] | None = None + ) -> "MinimumEigensolverResult": + """ + Computes minimum eigenvalue. Operator and aux_operators can be supplied here and + if not None will override any already set into algorithm so it can be reused with + different operators. While an operator is required by algorithms, aux_operators + are optional. + + Args: + operator: Qubit operator of the observable. + aux_operators: Optional list of auxiliary operators to be evaluated with the + eigenstate of the minimum eigenvalue main result and their expectation values + returned. For instance in chemistry these can be dipole operators and total particle + count operators, so we can get values for these at the ground state. + + Returns: + A minimum eigensolver result. + """ + return MinimumEigensolverResult() + + @classmethod + def supports_aux_operators(cls) -> bool: + """Whether computing the expectation value of auxiliary operators is supported. + + If the minimum eigensolver computes an eigenstate of the main operator then it can compute + the expectation value of the aux_operators for that state. Otherwise they will be ignored. + + Returns: + True if aux_operator expectations can be evaluated, False otherwise + """ + return False + + +@dataclass(frozen=True) +class MinimumEigensolverResult(AlgorithmResult): + """Minimum eigensolver result.""" + eigenvalue: float + aux_operator_eigenvalues: list \ No newline at end of file diff --git a/qiskit/algorithms/minimum_eigensolvers/numpy_minimum_eigensolver.py b/qiskit/algorithms/minimum_eigensolvers/numpy_minimum_eigensolver.py new file mode 100644 index 000000000000..33702568941e --- /dev/null +++ b/qiskit/algorithms/minimum_eigensolvers/numpy_minimum_eigensolver.py @@ -0,0 +1,87 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2022. +# +# 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. + +"""The NumPy minimum eigensolver algorithm.""" + +from typing import List, Optional, Union, Callable +import logging +import numpy as np + +from qiskit.opflow import OperatorBase + +# TODO this path will need updating +from ..eigen_solvers.numpy_eigen_solver import NumPyEigensolver +from .minimum_eigensolver import MinimumEigensolver, MinimumEigensolverResult +from ..list_or_dict import ListOrDict + +logger = logging.getLogger(__name__) + + +class NumPyMinimumEigensolver(MinimumEigensolver): + """ + The NumPy minimum eigensolver algorithm. + """ + + def __init__( + self, + filter_criterion: Callable[ + [Union[List, np.ndarray], float, Optional[ListOrDict[float]]], bool + ] = None, + ) -> None: + """ + Args: + filter_criterion: callable that allows to filter eigenvalues/eigenstates. The minimum + eigensolver is only searching over feasible states and returns an eigenstate that + has the smallest eigenvalue among feasible states. The callable has the signature + `filter(eigenstate, eigenvalue, aux_values)` and must return a boolean to indicate + whether to consider this value or not. If there is no + feasible element, the result can even be empty. + """ + self._ces = NumPyEigensolver(filter_criterion=filter_criterion) + self._ret = MinimumEigensolverResult() + + @property + def filter_criterion( + self, + ) -> Optional[Callable[[Union[List, np.ndarray], float, Optional[ListOrDict[float]]], bool]]: + """returns the filter criterion if set""" + return self._ces.filter_criterion + + @filter_criterion.setter + def filter_criterion( + self, + filter_criterion: Optional[ + Callable[[Union[List, np.ndarray], float, Optional[ListOrDict[float]]], bool] + ], + ) -> None: + """set the filter criterion""" + self._ces.filter_criterion = filter_criterion + + @classmethod + def supports_aux_operators(cls) -> bool: + return NumPyEigensolver.supports_aux_operators() + + def compute_minimum_eigenvalue( + self, operator: OperatorBase, aux_operators: Optional[ListOrDict[OperatorBase]] = None + ) -> MinimumEigensolverResult: + super().compute_minimum_eigenvalue(operator, aux_operators) + result_ces = self._ces.compute_eigenvalues(operator, aux_operators) + self._ret = MinimumEigensolverResult() + if result_ces.eigenvalues is not None and len(result_ces.eigenvalues) > 0: + self._ret.eigenvalue = result_ces.eigenvalues[0] + self._ret.eigenstate = result_ces.eigenstates[0] + if result_ces.aux_operator_eigenvalues: + self._ret.aux_operator_eigenvalues = result_ces.aux_operator_eigenvalues[0] + + logger.debug(f"MinimumEigensolver:\n{self._ret}") + + return self._ret diff --git a/qiskit/algorithms/minimum_eigensolvers/vqe.py b/qiskit/algorithms/minimum_eigensolvers/vqe.py new file mode 100644 index 000000000000..f823cc0f709f --- /dev/null +++ b/qiskit/algorithms/minimum_eigensolvers/vqe.py @@ -0,0 +1,275 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2022. +# +# 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. + +"""The variational quantum eigensolver algorithm.""" + +from __future__ import annotations +from dataclasses import dataclass +import logging +from time import time + +import numpy as np + +from qiskit.circuit import QuantumCircuit, Parameter +from qiskit.circuit.library import RealAmplitudes +from qiskit.opflow import ( + OperatorBase, +) +from qiskit.primitives import BaseEstimator +from qiskit.utils.validation import validate_min + +from ..exceptions import AlgorithmError +from ..optimizers import Optimizer, Minimizer, SLSQP +from .minimum_eigensolver import MinimumEigensolver, MinimumEigensolverResult + +logger = logging.getLogger(__name__) + + +class VQE(MinimumEigensolver): + r"""The variational quantum eigensolver (VQE) algorithm. + + VQE is a quantum algorithm that uses a variational technique to find the minimum eigenvalue of + the Hamiltonian :math:`H` of a given system [1]. + + An instance of VQE requires defining two algorithmic sub-components: a trial state (a.k.a. + ansatz) which is a :class:`QuantumCircuit`, and one of the classical + :mod:`~qiskit.algorithms.optimizers`. + + The ansatz is varied, via its set of parameters, by the optimizer, such that it works towards a + state, as determined by the parameters applied to the ansatz, that will result in the minimum + expectation value being measured of the input operator (Hamiltonian). + + 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") + + Attributes: + estimator: The estimator primitive to compute the expectation value of the circuits. + ansatz: A parameterized circuit, preparing the ansatz for the wave function. If not + provided, this defaults to a :class:`.RealAmplitudes` circuit. + optimizer: A classical optimizer to find the minimum energy. This can either be a + Qiskit :class:`.Optimizer` or a callable implementing the :class:`.Minimizer` protocol. + Defaults to :class:`.SLSQP`. + initial_point: An optional initial point (i.e. initial parameter values) for the optimizer. + If not provided, a random initial point with values in the interval :math:`[0, 2\pi]` + is used. + max_evals_grouped: Specifies how many parameter sets can be evaluated simultaneously. + This information is forwarded to the optimizer, which can use it for batch evaluation. + + References: + [1] Peruzzo et al, "A variational eigenvalue solver on a quantum processor" + `arXiv:1304.3061 https://arxiv.org/abs/1304.3061>`_ + """ + + def __init__( + self, + estimator: BaseEstimator, + ansatz: QuantumCircuit | None = None, + optimizer: Optimizer | Minimizer | None = None, + gradient=None, + initial_point: np.ndarray | None = None, + max_evals_grouped: int = 1, + # TODO Attach callback to optimizer instead. + callback=None, + ) -> None: + """ + Args: + estimator: The estimator primitive to compute the expectation value of the circuits. + ansatz: The parameterized circuit used as ansatz for the wave function. + optimizer: The 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. + gradient: An optional gradient function or operator for optimizer. + 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. + max_evals_grouped: Max number of evaluations performed simultaneously. Signals the + given optimizer that more than one set of parameters can be supplied so that + potentially the expectation values can be computed in parallel. Typically this is + possible when a finite difference gradient is used by the optimizer such that + multiple points to compute the gradient can be passed and if computed in parallel + improve overall execution time. Deprecated if a gradient operator or function is + given. + """ + super().__init__() + + validate_min("max_evals_grouped", max_evals_grouped, 1) + + self.estimator = estimator + self.ansatz = ansatz + self.optimizer = optimizer + self.gradient = gradient + self.initial_point = initial_point + self.max_evals_grouped = max_evals_grouped + self.callback = callback + + # TODO remove this + self._eval_count = 0 + + def compute_minimum_eigenvalue(self, operator: OperatorBase, aux_operators=None): + # Set defaults + if self.ansatz is None: + ansatz = RealAmplitudes(num_qubits=operator.num_qubits) + else: + ansatz = self.ansatz.copy() + _check_operator_ansatz(operator, ansatz) + + if self.optimizer is None: + optimizer = SLSQP() + else: + optimizer = self.optimizer + + if isinstance(aux_operators, dict): + aux_ops = list(aux_operators.values()) + elif aux_operators: + # Not None and not empty list + non_nones = [i for i, x in enumerate(aux_operators) if x is not None] + aux_ops = [x for x in aux_operators if x is not None] + else: + aux_ops = None + + operators = [operator] + ([] if aux_ops is None else aux_ops) + + self._eval_count = 0 + + def energy(point): + job = self.estimator.run([ansatz], [operator], [point]) + return job.result().values[0] + + # def gradient(point): + # job = self.gradient.run([self.ansatz], [operator], [point]) + # return job.result() + + def expectation(point): + value = energy(point) + self._eval_count += 1 + if self.callback is not None: + self.callback(self._eval_count, point, value, 0) + return value + + initial_point = self.initial_point + if not initial_point: + initial_point = np.random.random(ansatz.num_parameters) + + start_time = time() + + # Perform optimization + if callable(optimizer): + opt_result = optimizer( # pylint: disable=not-callable + fun=expectation, x0=initial_point # , jac=gradient, bounds=bounds + ) + else: + opt_result = optimizer.minimize( + fun=expectation, x0=initial_point # , jac=gradient, bounds=bounds + ) + + eval_time = time() - start_time + + optimal_point = opt_result.x + logger.info( + f"Optimization complete in {eval_time} seconds.\nFound opt_params {optimal_point}." + ) + + # Compute auxiliary operator eigenvalues + aux_values = None + if aux_ops: + num_aux_ops = len(aux_ops) + aux_job = self.estimator.run( + [ansatz] * num_aux_ops, operators[1:], [optimal_point] * num_aux_ops + ) + aux_eigs = aux_job.result().values + aux_eigs = list(zip(aux_eigs, [0] * len(aux_eigs))) + if isinstance(aux_operators, dict): + aux_values = dict(zip(aux_operators.keys(), aux_eigs)) + else: + aux_values = [None] * len(aux_operators) + for i, x in enumerate(non_nones): + aux_values[x] = aux_eigs[i] + + result = VQEResult( + eigenvalue=opt_result.fun + 0j, + cost_function_evals=opt_result.nfev, + optimal_point=optimal_point, + optimal_parameters=dict(zip(self.ansatz.parameters, optimal_point)), + optimal_value=opt_result.fun, + optimizer_time=eval_time, + aux_operator_eigenvalues=aux_values, + # TODO Add variances for the eigenvalues. + ) + + return result + + @classmethod + def supports_aux_operators(cls) -> bool: + return True + + +def _check_operator_ansatz(operator: OperatorBase, ansatz: QuantumCircuit) -> QuantumCircuit: + """Check that the number of qubits of operator and ansatz match and that the ansatz is + parameterized.""" + if operator.num_qubits != ansatz.num_qubits: + # Try to set the number of qubits on the ansatz. + try: + logger.info( + f"Trying to resize ansatz to match operator on {operator.num_qubits} qubits." + ) + ansatz.num_qubits = operator.num_qubits + except AttributeError as ex: + raise AlgorithmError( + "The number of qubits of the ansatz does not match the " + "operator, and the ansatz does not allow setting the " + "number of qubits using `num_qubits`." + ) from ex + + if ansatz.num_parameters == 0: + raise AlgorithmError("The ansatz must be parameterized, but has no free parameters.") + + return ansatz + + +@dataclass(frozen=True) +class VQEResult(MinimumEigensolverResult): + "Variational quantum eigensolver result." + cost_function_evals: int + optimal_point: np.ndarray + optimal_parameters: dict[Parameter, float] + optimal_value: float + optimizer_time: float From ac8841244145f737b79f5a562b3b7f018d027776 Mon Sep 17 00:00:00 2001 From: Declan Millar Date: Thu, 1 Sep 2022 12:41:12 +0100 Subject: [PATCH 002/100] Revert from dataclass results to original result classes. --- .../minimum_eigensolver.py | 36 +++++++++++--- qiskit/algorithms/minimum_eigensolvers/vqe.py | 48 +++++++++++-------- 2 files changed, 59 insertions(+), 25 deletions(-) diff --git a/qiskit/algorithms/minimum_eigensolvers/minimum_eigensolver.py b/qiskit/algorithms/minimum_eigensolvers/minimum_eigensolver.py index 50532a541d80..9fbaa21e084f 100644 --- a/qiskit/algorithms/minimum_eigensolvers/minimum_eigensolver.py +++ b/qiskit/algorithms/minimum_eigensolvers/minimum_eigensolver.py @@ -13,7 +13,6 @@ """The minimum eigensolver interface and result.""" from abc import ABC, abstractmethod -from dataclasses import dataclass from qiskit.opflow import OperatorBase from ..algorithm_result import AlgorithmResult @@ -40,7 +39,7 @@ def compute_minimum_eigenvalue( Args: operator: Qubit operator of the observable. aux_operators: Optional list of auxiliary operators to be evaluated with the - eigenstate of the minimum eigenvalue main result and their expectation values + parameters of the minimum eigenvalue main result and their expectation values returned. For instance in chemistry these can be dipole operators and total particle count operators, so we can get values for these at the ground state. @@ -53,7 +52,7 @@ def compute_minimum_eigenvalue( def supports_aux_operators(cls) -> bool: """Whether computing the expectation value of auxiliary operators is supported. - If the minimum eigensolver computes an eigenstate of the main operator then it can compute + If the minimum eigensolver computes an eigenvalue of the main operator then it can compute the expectation value of the aux_operators for that state. Otherwise they will be ignored. Returns: @@ -62,8 +61,33 @@ def supports_aux_operators(cls) -> bool: return False -@dataclass(frozen=True) class MinimumEigensolverResult(AlgorithmResult): """Minimum eigensolver result.""" - eigenvalue: float - aux_operator_eigenvalues: list \ No newline at end of file + + def __init__(self) -> None: + super().__init__() + self._eigenvalue = None + self._aux_operator_eigenvalues = None + + @property + def eigenvalue(self) -> complex | None: + """returns eigen value""" + return self._eigenvalue + + @eigenvalue.setter + def eigenvalue(self, value: complex) -> None: + """set eigen value""" + self._eigenvalue = value + + @property + def aux_operator_eigenvalues(self) -> ListOrDict[tuple[complex, complex]] | None: + """Return aux operator expectation values. + + These values are in fact tuples formatted as (mean, standard deviation). + """ + return self._aux_operator_eigenvalues + + @aux_operator_eigenvalues.setter + def aux_operator_eigenvalues(self, value: ListOrDict[tuple[complex, complex]]) -> None: + """set aux operator eigen values""" + self._aux_operator_eigenvalues = value diff --git a/qiskit/algorithms/minimum_eigensolvers/vqe.py b/qiskit/algorithms/minimum_eigensolvers/vqe.py index f823cc0f709f..5e0439e3ac59 100644 --- a/qiskit/algorithms/minimum_eigensolvers/vqe.py +++ b/qiskit/algorithms/minimum_eigensolvers/vqe.py @@ -27,8 +27,10 @@ from qiskit.primitives import BaseEstimator from qiskit.utils.validation import validate_min + from ..exceptions import AlgorithmError from ..optimizers import Optimizer, Minimizer, SLSQP +from ..variational_algorithm import VariationalResult from .minimum_eigensolver import MinimumEigensolver, MinimumEigensolverResult logger = logging.getLogger(__name__) @@ -89,6 +91,7 @@ def my_minimizer(fun, x0, jac=None, bounds=None) -> OptimizerResult: optimizer: A classical optimizer to find the minimum energy. This can either be a Qiskit :class:`.Optimizer` or a callable implementing the :class:`.Minimizer` protocol. Defaults to :class:`.SLSQP`. + gradient: An optional gradient function or operator for the optimizer. initial_point: An optional initial point (i.e. initial parameter values) for the optimizer. If not provided, a random initial point with values in the interval :math:`[0, 2\pi]` is used. @@ -117,7 +120,7 @@ def __init__( ansatz: The parameterized circuit used as ansatz for the wave function. optimizer: The 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. - gradient: An optional gradient function or operator for optimizer. + gradient: An optional gradient function or operator for the optimizer. 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. @@ -224,16 +227,15 @@ def expectation(point): for i, x in enumerate(non_nones): aux_values[x] = aux_eigs[i] - result = VQEResult( - eigenvalue=opt_result.fun + 0j, - cost_function_evals=opt_result.nfev, - optimal_point=optimal_point, - optimal_parameters=dict(zip(self.ansatz.parameters, optimal_point)), - optimal_value=opt_result.fun, - optimizer_time=eval_time, - aux_operator_eigenvalues=aux_values, - # TODO Add variances for the eigenvalues. - ) + result = VQEResult() + result.eigenvalue = opt_result.fun + 0j + result.aux_operator_eigenvalues = aux_values + result.cost_function_evals = opt_result.nfev + result.optimal_point = optimal_point + result.optimal_parameters = dict(zip(self.ansatz.parameters, optimal_point)) + result.optimal_value = opt_result.fun + result.optimizer_time = eval_time + # TODO Add variances for the eigenvalues. return result @@ -265,11 +267,19 @@ def _check_operator_ansatz(operator: OperatorBase, ansatz: QuantumCircuit) -> Qu return ansatz -@dataclass(frozen=True) -class VQEResult(MinimumEigensolverResult): - "Variational quantum eigensolver result." - cost_function_evals: int - optimal_point: np.ndarray - optimal_parameters: dict[Parameter, float] - optimal_value: float - optimizer_time: float +class VQEResult(VariationalResult, MinimumEigensolverResult): + """Variational quantum eigensolver result.""" + + def __init__(self) -> None: + super().__init__() + self._cost_function_evals = None + + @property + def cost_function_evals(self) -> int | None: + """Returns number of cost optimizer evaluations""" + return self._cost_function_evals + + @cost_function_evals.setter + def cost_function_evals(self, value: int) -> None: + """Sets number of cost function evaluations""" + self._cost_function_evals = value From 3fd8f3c483ff8f09a18c8ebf2f21cb15949c2852 Mon Sep 17 00:00:00 2001 From: Declan Millar Date: Fri, 2 Sep 2022 09:26:32 +0100 Subject: [PATCH 003/100] Enforce positional and keyword VQE arguments. --- qiskit/algorithms/minimum_eigensolvers/vqe.py | 84 +++++++++---------- 1 file changed, 38 insertions(+), 46 deletions(-) diff --git a/qiskit/algorithms/minimum_eigensolvers/vqe.py b/qiskit/algorithms/minimum_eigensolvers/vqe.py index 5e0439e3ac59..d199cce2c31a 100644 --- a/qiskit/algorithms/minimum_eigensolvers/vqe.py +++ b/qiskit/algorithms/minimum_eigensolvers/vqe.py @@ -13,23 +13,20 @@ """The variational quantum eigensolver algorithm.""" from __future__ import annotations -from dataclasses import dataclass import logging from time import time import numpy as np from qiskit.circuit import QuantumCircuit, Parameter -from qiskit.circuit.library import RealAmplitudes from qiskit.opflow import ( OperatorBase, ) from qiskit.primitives import BaseEstimator from qiskit.utils.validation import validate_min - from ..exceptions import AlgorithmError -from ..optimizers import Optimizer, Minimizer, SLSQP +from ..optimizers import Optimizer, Minimizer from ..variational_algorithm import VariationalResult from .minimum_eigensolver import MinimumEigensolver, MinimumEigensolverResult @@ -85,12 +82,12 @@ def my_minimizer(fun, x0, jac=None, bounds=None) -> OptimizerResult: optimizer = partial(minimize, method="L-BFGS-B") Attributes: - estimator: The estimator primitive to compute the expectation value of the circuits. ansatz: A parameterized circuit, preparing the ansatz for the wave function. If not provided, this defaults to a :class:`.RealAmplitudes` circuit. optimizer: A classical optimizer to find the minimum energy. This can either be a Qiskit :class:`.Optimizer` or a callable implementing the :class:`.Minimizer` protocol. Defaults to :class:`.SLSQP`. + estimator: The estimator primitive to compute the expectation value of the circuits. gradient: An optional gradient function or operator for the optimizer. initial_point: An optional initial point (i.e. initial parameter values) for the optimizer. If not provided, a random initial point with values in the interval :math:`[0, 2\pi]` @@ -105,9 +102,10 @@ def my_minimizer(fun, x0, jac=None, bounds=None) -> OptimizerResult: def __init__( self, + ansatz: QuantumCircuit, + optimizer: Optimizer | Minimizer, estimator: BaseEstimator, - ansatz: QuantumCircuit | None = None, - optimizer: Optimizer | Minimizer | None = None, + *, gradient=None, initial_point: np.ndarray | None = None, max_evals_grouped: int = 1, @@ -116,10 +114,10 @@ def __init__( ) -> None: """ Args: - estimator: The estimator primitive to compute the expectation value of the circuits. ansatz: The parameterized circuit used as ansatz for the wave function. optimizer: The 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. + estimator: The estimator primitive to compute the expectation value of the circuits. gradient: An optional gradient function or operator for the optimizer. 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 @@ -136,9 +134,9 @@ def __init__( validate_min("max_evals_grouped", max_evals_grouped, 1) - self.estimator = estimator self.ansatz = ansatz self.optimizer = optimizer + self.estimator = estimator self.gradient = gradient self.initial_point = initial_point self.max_evals_grouped = max_evals_grouped @@ -148,17 +146,11 @@ def __init__( self._eval_count = 0 def compute_minimum_eigenvalue(self, operator: OperatorBase, aux_operators=None): - # Set defaults - if self.ansatz is None: - ansatz = RealAmplitudes(num_qubits=operator.num_qubits) - else: - ansatz = self.ansatz.copy() - _check_operator_ansatz(operator, ansatz) + ansatz = self._check_operator_ansatz(operator) - if self.optimizer is None: - optimizer = SLSQP() - else: - optimizer = self.optimizer + if isinstance(self.optimizer, Optimizer): + # note that this changes the optimizer instance -- should we reset after the VQE run? + self.optimizer.set_max_evals_grouped(self.max_evals_grouped) if isinstance(aux_operators, dict): aux_ops = list(aux_operators.values()) @@ -195,12 +187,12 @@ def expectation(point): start_time = time() # Perform optimization - if callable(optimizer): - opt_result = optimizer( # pylint: disable=not-callable + if callable(self.optimizer): + opt_result = self.optimizer( # pylint: disable=not-callable fun=expectation, x0=initial_point # , jac=gradient, bounds=bounds ) else: - opt_result = optimizer.minimize( + opt_result = self.optimizer.minimize( fun=expectation, x0=initial_point # , jac=gradient, bounds=bounds ) @@ -243,28 +235,28 @@ def expectation(point): def supports_aux_operators(cls) -> bool: return True - -def _check_operator_ansatz(operator: OperatorBase, ansatz: QuantumCircuit) -> QuantumCircuit: - """Check that the number of qubits of operator and ansatz match and that the ansatz is - parameterized.""" - if operator.num_qubits != ansatz.num_qubits: - # Try to set the number of qubits on the ansatz. - try: - logger.info( - f"Trying to resize ansatz to match operator on {operator.num_qubits} qubits." - ) - ansatz.num_qubits = operator.num_qubits - except AttributeError as ex: - raise AlgorithmError( - "The number of qubits of the ansatz does not match the " - "operator, and the ansatz does not allow setting the " - "number of qubits using `num_qubits`." - ) from ex - - if ansatz.num_parameters == 0: - raise AlgorithmError("The ansatz must be parameterized, but has no free parameters.") - - return ansatz + def _check_operator_ansatz(self, operator: OperatorBase) -> QuantumCircuit: + """Check that the number of qubits of operator and ansatz match and that the ansatz is + parameterized. + """ + ansatz = self.ansatz.copy() + if operator.num_qubits != ansatz.num_qubits: + try: + logger.info( + f"Trying to resize ansatz to match operator on {operator.num_qubits} qubits." + ) + ansatz.num_qubits = operator.num_qubits + except AttributeError as error: + raise AlgorithmError( + "The number of qubits of the ansatz does not match the " + "operator, and the ansatz does not allow setting the " + "number of qubits using `num_qubits`." + ) from error + + if ansatz.num_parameters == 0: + raise AlgorithmError("The ansatz must be parameterized, but has no free parameters.") + + return ansatz class VQEResult(VariationalResult, MinimumEigensolverResult): @@ -276,10 +268,10 @@ def __init__(self) -> None: @property def cost_function_evals(self) -> int | None: - """Returns number of cost optimizer evaluations""" + """Returns number of cost optimizer evaluations.""" return self._cost_function_evals @cost_function_evals.setter def cost_function_evals(self, value: int) -> None: - """Sets number of cost function evaluations""" + """Sets number of cost function evaluations.""" self._cost_function_evals = value From 9b987cc6f57bd145d91c54cc8302f1795a7c3f81 Mon Sep 17 00:00:00 2001 From: Declan Millar Date: Fri, 2 Sep 2022 14:52:10 +0100 Subject: [PATCH 004/100] Move aux op eval logic to function --- qiskit/algorithms/minimum_eigensolvers/vqe.py | 79 +++++++++++-------- 1 file changed, 45 insertions(+), 34 deletions(-) diff --git a/qiskit/algorithms/minimum_eigensolvers/vqe.py b/qiskit/algorithms/minimum_eigensolvers/vqe.py index d199cce2c31a..3b2a4b6895e7 100644 --- a/qiskit/algorithms/minimum_eigensolvers/vqe.py +++ b/qiskit/algorithms/minimum_eigensolvers/vqe.py @@ -18,18 +18,19 @@ import numpy as np -from qiskit.circuit import QuantumCircuit, Parameter -from qiskit.opflow import ( - OperatorBase, -) +from qiskit.circuit import QuantumCircuit +from qiskit.quantum_info.operators.base_operator import BaseOperator +from qiskit.opflow import PauliSumOp from qiskit.primitives import BaseEstimator from qiskit.utils.validation import validate_min from ..exceptions import AlgorithmError +from ..list_or_dict import ListOrDict from ..optimizers import Optimizer, Minimizer from ..variational_algorithm import VariationalResult from .minimum_eigensolver import MinimumEigensolver, MinimumEigensolverResult + logger = logging.getLogger(__name__) @@ -145,24 +146,17 @@ def __init__( # TODO remove this self._eval_count = 0 - def compute_minimum_eigenvalue(self, operator: OperatorBase, aux_operators=None): + def compute_minimum_eigenvalue( + self, + operator: BaseOperator | PauliSumOp, + aux_operators: ListOrDict[BaseOperator | PauliSumOp] | None = None, + ) -> VQEResult: ansatz = self._check_operator_ansatz(operator) if isinstance(self.optimizer, Optimizer): # note that this changes the optimizer instance -- should we reset after the VQE run? self.optimizer.set_max_evals_grouped(self.max_evals_grouped) - if isinstance(aux_operators, dict): - aux_ops = list(aux_operators.values()) - elif aux_operators: - # Not None and not empty list - non_nones = [i for i, x in enumerate(aux_operators) if x is not None] - aux_ops = [x for x in aux_operators if x is not None] - else: - aux_ops = None - - operators = [operator] + ([] if aux_ops is None else aux_ops) - self._eval_count = 0 def energy(point): @@ -181,8 +175,13 @@ def expectation(point): return value initial_point = self.initial_point - if not initial_point: + if initial_point is None: initial_point = np.random.random(ansatz.num_parameters) + elif len(initial_point) != ansatz.num_parameters: + raise ValueError( + f"The dimension of the initial point ({len(self.initial_point)}) does not match " + f"the number of parameters in the circuit ({ansatz.num_parameters})." + ) start_time = time() @@ -203,21 +202,10 @@ def expectation(point): f"Optimization complete in {eval_time} seconds.\nFound opt_params {optimal_point}." ) - # Compute auxiliary operator eigenvalues aux_values = None - if aux_ops: - num_aux_ops = len(aux_ops) - aux_job = self.estimator.run( - [ansatz] * num_aux_ops, operators[1:], [optimal_point] * num_aux_ops - ) - aux_eigs = aux_job.result().values - aux_eigs = list(zip(aux_eigs, [0] * len(aux_eigs))) - if isinstance(aux_operators, dict): - aux_values = dict(zip(aux_operators.keys(), aux_eigs)) - else: - aux_values = [None] * len(aux_operators) - for i, x in enumerate(non_nones): - aux_values[x] = aux_eigs[i] + if aux_operators: + # Not None and not empty list + aux_values = self._eval_aux_ops(ansatz, optimal_point, aux_operators) result = VQEResult() result.eigenvalue = opt_result.fun + 0j @@ -227,15 +215,13 @@ def expectation(point): result.optimal_parameters = dict(zip(self.ansatz.parameters, optimal_point)) result.optimal_value = opt_result.fun result.optimizer_time = eval_time - # TODO Add variances for the eigenvalues. - return result @classmethod def supports_aux_operators(cls) -> bool: return True - def _check_operator_ansatz(self, operator: OperatorBase) -> QuantumCircuit: + def _check_operator_ansatz(self, operator: BaseOperator | PauliSumOp) -> QuantumCircuit: """Check that the number of qubits of operator and ansatz match and that the ansatz is parameterized. """ @@ -258,6 +244,31 @@ def _check_operator_ansatz(self, operator: OperatorBase) -> QuantumCircuit: return ansatz + def _eval_aux_ops(self, ansatz, parameters, aux_operators): + """Compute auxiliary operator eigenvalues.""" + if isinstance(aux_operators, dict): + aux_ops = list(aux_operators.values()) + else: + non_nones = [i for i, x in enumerate(aux_operators) if x is not None] + aux_ops = [x for x in aux_operators if x is not None] + + aux_values = None + if aux_ops: + num_aux_ops = len(aux_ops) + aux_job = self.estimator.run( + [ansatz] * num_aux_ops, aux_ops, [parameters] * num_aux_ops + ) + aux_eigs = aux_job.result().values + aux_eigs = list(zip(aux_eigs, [0] * len(aux_eigs))) + if isinstance(aux_operators, dict): + aux_values = dict(zip(aux_operators.keys(), aux_eigs)) + else: + aux_values = [None] * len(aux_operators) + for i, x in enumerate(non_nones): + aux_values[x] = aux_eigs[i] + + return aux_values + class VQEResult(VariationalResult, MinimumEigensolverResult): """Variational quantum eigensolver result.""" From 378aac879b9ca309c6d4b40b3246df2c2ccdc31b Mon Sep 17 00:00:00 2001 From: Declan Millar Date: Fri, 2 Sep 2022 14:57:46 +0100 Subject: [PATCH 005/100] Update docstring. --- qiskit/algorithms/minimum_eigensolvers/vqe.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/qiskit/algorithms/minimum_eigensolvers/vqe.py b/qiskit/algorithms/minimum_eigensolvers/vqe.py index 3b2a4b6895e7..fc3882d40dd7 100644 --- a/qiskit/algorithms/minimum_eigensolvers/vqe.py +++ b/qiskit/algorithms/minimum_eigensolvers/vqe.py @@ -73,7 +73,7 @@ def my_minimizer(fun, x0, jac=None, bounds=None) -> OptimizerResult: result.fun = # optimal function value return result - The above signature also allows to directly pass any SciPy minimizer, for instance as + The above signature also allows one to directly pass any SciPy minimizer, for instance as .. code-block:: python @@ -83,11 +83,9 @@ def my_minimizer(fun, x0, jac=None, bounds=None) -> OptimizerResult: optimizer = partial(minimize, method="L-BFGS-B") Attributes: - ansatz: A parameterized circuit, preparing the ansatz for the wave function. If not - provided, this defaults to a :class:`.RealAmplitudes` circuit. + ansatz: The parameterized circuit used as an ansatz for the wave function. optimizer: A classical optimizer to find the minimum energy. This can either be a Qiskit :class:`.Optimizer` or a callable implementing the :class:`.Minimizer` protocol. - Defaults to :class:`.SLSQP`. estimator: The estimator primitive to compute the expectation value of the circuits. gradient: An optional gradient function or operator for the optimizer. initial_point: An optional initial point (i.e. initial parameter values) for the optimizer. From 629e6de4017a42dd99b60e436b635fa360283819 Mon Sep 17 00:00:00 2001 From: Declan Millar Date: Mon, 5 Sep 2022 09:49:22 +0100 Subject: [PATCH 006/100] Remove max_evals_grouped. Force to set directly on optimizer. --- qiskit/algorithms/minimum_eigensolvers/vqe.py | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/qiskit/algorithms/minimum_eigensolvers/vqe.py b/qiskit/algorithms/minimum_eigensolvers/vqe.py index fc3882d40dd7..f3dda334505e 100644 --- a/qiskit/algorithms/minimum_eigensolvers/vqe.py +++ b/qiskit/algorithms/minimum_eigensolvers/vqe.py @@ -91,8 +91,6 @@ def my_minimizer(fun, x0, jac=None, bounds=None) -> OptimizerResult: initial_point: An optional initial point (i.e. initial parameter values) for the optimizer. If not provided, a random initial point with values in the interval :math:`[0, 2\pi]` is used. - max_evals_grouped: Specifies how many parameter sets can be evaluated simultaneously. - This information is forwarded to the optimizer, which can use it for batch evaluation. References: [1] Peruzzo et al, "A variational eigenvalue solver on a quantum processor" @@ -107,7 +105,6 @@ def __init__( *, gradient=None, initial_point: np.ndarray | None = None, - max_evals_grouped: int = 1, # TODO Attach callback to optimizer instead. callback=None, ) -> None: @@ -121,24 +118,14 @@ def __init__( 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. - max_evals_grouped: Max number of evaluations performed simultaneously. Signals the - given optimizer that more than one set of parameters can be supplied so that - potentially the expectation values can be computed in parallel. Typically this is - possible when a finite difference gradient is used by the optimizer such that - multiple points to compute the gradient can be passed and if computed in parallel - improve overall execution time. Deprecated if a gradient operator or function is - given. """ super().__init__() - validate_min("max_evals_grouped", max_evals_grouped, 1) - self.ansatz = ansatz self.optimizer = optimizer self.estimator = estimator self.gradient = gradient self.initial_point = initial_point - self.max_evals_grouped = max_evals_grouped self.callback = callback # TODO remove this @@ -151,10 +138,6 @@ def compute_minimum_eigenvalue( ) -> VQEResult: ansatz = self._check_operator_ansatz(operator) - if isinstance(self.optimizer, Optimizer): - # note that this changes the optimizer instance -- should we reset after the VQE run? - self.optimizer.set_max_evals_grouped(self.max_evals_grouped) - self._eval_count = 0 def energy(point): From 499c35ea4765e550f8150a26f18f9247fe0643ed Mon Sep 17 00:00:00 2001 From: Declan Millar Date: Mon, 5 Sep 2022 10:33:16 +0100 Subject: [PATCH 007/100] Remove validate min import. --- qiskit/algorithms/minimum_eigensolvers/vqe.py | 1 - 1 file changed, 1 deletion(-) diff --git a/qiskit/algorithms/minimum_eigensolvers/vqe.py b/qiskit/algorithms/minimum_eigensolvers/vqe.py index f3dda334505e..cdb485843d7f 100644 --- a/qiskit/algorithms/minimum_eigensolvers/vqe.py +++ b/qiskit/algorithms/minimum_eigensolvers/vqe.py @@ -22,7 +22,6 @@ from qiskit.quantum_info.operators.base_operator import BaseOperator from qiskit.opflow import PauliSumOp from qiskit.primitives import BaseEstimator -from qiskit.utils.validation import validate_min from ..exceptions import AlgorithmError from ..list_or_dict import ListOrDict From 689e2c87669800cd117f778a8b9583c61387f065 Mon Sep 17 00:00:00 2001 From: Declan Millar Date: Mon, 5 Sep 2022 15:04:37 +0100 Subject: [PATCH 008/100] Make note that eval_observables will be used to eval aux ops. --- qiskit/algorithms/minimum_eigensolvers/vqe.py | 21 +++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/qiskit/algorithms/minimum_eigensolvers/vqe.py b/qiskit/algorithms/minimum_eigensolvers/vqe.py index cdb485843d7f..ff2fa752d4b0 100644 --- a/qiskit/algorithms/minimum_eigensolvers/vqe.py +++ b/qiskit/algorithms/minimum_eigensolvers/vqe.py @@ -139,20 +139,20 @@ def compute_minimum_eigenvalue( self._eval_count = 0 - def energy(point): + def energy_evaluation(point): job = self.estimator.run([ansatz], [operator], [point]) return job.result().values[0] - # def gradient(point): + # def gradient_evaluation(point): # job = self.gradient.run([self.ansatz], [operator], [point]) # return job.result() def expectation(point): - value = energy(point) + energy = energy_evaluation(point) self._eval_count += 1 if self.callback is not None: - self.callback(self._eval_count, point, value, 0) - return value + self.callback(self._eval_count, point, energy, 0) + return energy initial_point = self.initial_point if initial_point is None: @@ -185,6 +185,7 @@ def expectation(point): aux_values = None if aux_operators: # Not None and not empty list + # TODO use eval_operators aux_values = self._eval_aux_ops(ansatz, optimal_point, aux_operators) result = VQEResult() @@ -224,8 +225,16 @@ def _check_operator_ansatz(self, operator: BaseOperator | PauliSumOp) -> Quantum return ansatz - def _eval_aux_ops(self, ansatz, parameters, aux_operators): + def _eval_aux_ops( + self, + ansatz: QuantumCircuit, + parameters: np.ndarray, + aux_operators: ListOrDict[BaseOperator | PauliSumOp], + ) -> ListOrDict[tuple(complex, complex)]: """Compute auxiliary operator eigenvalues.""" + + # TODO This is going to be replaced by eval_observables. See PR #8683. + if isinstance(aux_operators, dict): aux_ops = list(aux_operators.values()) else: From 1a434604fce2e090f320ec645c5fa05033665d25 Mon Sep 17 00:00:00 2001 From: Declan Millar Date: Mon, 5 Sep 2022 15:06:01 +0100 Subject: [PATCH 009/100] Add initial vqe tests. --- .../minimum_eigensolvers/test_vqe.py | 582 ++++++++++++++++++ 1 file changed, 582 insertions(+) create mode 100644 test/python/algorithms/minimum_eigensolvers/test_vqe.py diff --git a/test/python/algorithms/minimum_eigensolvers/test_vqe.py b/test/python/algorithms/minimum_eigensolvers/test_vqe.py new file mode 100644 index 000000000000..cee17d0cdb2a --- /dev/null +++ b/test/python/algorithms/minimum_eigensolvers/test_vqe.py @@ -0,0 +1,582 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2022. +# +# 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. + +"""Test the variational quantum eigensolver algorithm.""" + +import logging +import unittest +from test.python.algorithms import QiskitAlgorithmsTestCase +from test.python.transpiler._dummy_passes import DummyAP + +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 +from qiskit.algorithms import AlgorithmError +from qiskit.algorithms.minimum_eigensolvers.vqe import VQE +from qiskit.algorithms.optimizers import ( + CG, + COBYLA, + L_BFGS_B, + P_BFGS, + QNSPSA, + SLSQP, + SPSA, + TNC, + OptimizerResult, +) +from qiskit.circuit.library import EfficientSU2, RealAmplitudes, TwoLocal +from qiskit.exceptions import MissingOptionalLibraryError +from qiskit.opflow import ( + AerPauliExpectation, + Gradient, + I, + MatrixExpectation, + PauliExpectation, + PauliSumOp, + PrimitiveOp, + TwoQubitReduction, + X, + Z, +) +from qiskit.quantum_info import SparsePauliOp, Operator +from qiskit.primitives import Estimator +from qiskit.quantum_info import Statevector +from qiskit.transpiler import PassManager, PassManagerConfig +from qiskit.transpiler.preset_passmanagers import level_1_pass_manager +from qiskit.utils import QuantumInstance, algorithm_globals, has_aer + +if has_aer(): + from qiskit import Aer + +logger = "LocalLogger" + + +class LogPass(DummyAP): + """A dummy analysis pass that logs when executed""" + + def __init__(self, message): + super().__init__() + self.message = message + + 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""" + + def setUp(self): + super().setUp() + self.seed = 50 + algorithm_globals.random_seed = self.seed + self.h2_op = ( + -1.052373245772859 * (I ^ I) + + 0.39793742484318045 * (I ^ Z) + - 0.39793742484318045 * (Z ^ I) + - 0.01128010425623538 * (Z ^ Z) + + 0.18093119978423156 * (X ^ X) + ) + self.h2_energy = -1.85727503 + + self.ryrz_wavefunction = TwoLocal(rotation_blocks=["ry", "rz"], entanglement_blocks="cz") + self.ry_wavefunction = TwoLocal(rotation_blocks="ry", entanglement_blocks="cz") + + def test_basic_aer_statevector(self): + """Test VQE using reference Estimator.""" + vqe = VQE(self.ryrz_wavefunction, L_BFGS_B(), Estimator()) + + result = vqe.compute_minimum_eigenvalue(operator=self.h2_op) + + with self.subTest(msg="test eigenvalue"): + self.assertAlmostEqual(result.eigenvalue.real, self.h2_energy) + + with self.subTest(msg="test optimal_value"): + self.assertAlmostEqual(result.optimal_value, self.h2_energy) + + with self.subTest(msg="test dimension of optimal point"): + self.assertEqual(len(result.optimal_point), 16) + + with self.subTest(msg="assert cost_function_evals is set"): + self.assertIsNotNone(result.cost_function_evals) + + with self.subTest(msg="assert optimizer_time is set"): + self.assertIsNotNone(result.optimizer_time) + + def test_circuit_input(self): + """Test running the VQE on a plain QuantumCircuit object.""" + wavefunction = QuantumCircuit(2).compose(EfficientSU2(2)) + optimizer = SLSQP(maxiter=50) + vqe = VQE(wavefunction, optimizer, Estimator()) + result = vqe.compute_minimum_eigenvalue(self.h2_op) + self.assertAlmostEqual(result.eigenvalue.real, self.h2_energy, places=5) + + def test_invalid_initial_point(self): + """Test the proper error is raised when the initial point has the wrong size.""" + ansatz = self.ryrz_wavefunction + initial_point = np.array([1]) + + vqe = VQE( + ansatz, + SLSQP(), + Estimator(), + initial_point=initial_point, + ) + + with self.assertRaises(ValueError): + _ = vqe.compute_minimum_eigenvalue(operator=self.h2_op) + + def test_ansatz_resize(self): + """Test the ansatz is properly resized if it's a blueprint circuit.""" + ansatz = RealAmplitudes(1, reps=1) + vqe = VQE(ansatz, SLSQP(), Estimator()) + result = vqe.compute_minimum_eigenvalue(self.h2_op) + self.assertAlmostEqual(result.eigenvalue.real, self.h2_energy, places=5) + + def test_invalid_ansatz_size(self): + """Test an error is raised if the ansatz has the wrong number of qubits.""" + ansatz = QuantumCircuit(1) + ansatz.compose(RealAmplitudes(1, reps=2)) + vqe = VQE(ansatz, SLSQP(), Estimator()) + + with self.assertRaises(AlgorithmError): + _ = vqe.compute_minimum_eigenvalue(operator=self.h2_op) + + def test_missing_ansatz_params(self): + """Test specifying an ansatz with no parameters raises an error.""" + ansatz = QuantumCircuit(self.h2_op.num_qubits) + vqe = VQE(ansatz, SLSQP(), Estimator()) + with self.assertRaises(AlgorithmError): + vqe.compute_minimum_eigenvalue(operator=self.h2_op) + + # @data( + # (SLSQP(maxiter=50, max_evals_grouped=5), 4), + # (SPSA(maxiter=150, max_evals_grouped=2), 2), + # ) + # @unpack + # def test_max_evals_grouped(self, optimizer, places): + # """VQE Optimizers test""" + # vqe = VQE( + # self.ryrz_wavefunction, + # optimizer, + # estimator=Estimator(), + # ) + # result = vqe.compute_minimum_eigenvalue(operator=self.h2_op) + # self.assertAlmostEqual(result.eigenvalue.real, self.h2_energy, places=places) + + # @unittest.skipUnless(has_aer(), "qiskit-aer doesn't appear to be installed.") + # @data( + # CG(maxiter=1), + # L_BFGS_B(maxfun=1), + # P_BFGS(maxfun=1, max_processes=0), + # SLSQP(maxiter=1), + # TNC(maxiter=1), + # ) + # def test_with_gradient(self, optimizer): + # """Test VQE using Gradient().""" + # quantum_instance = QuantumInstance( + # backend=Aer.get_backend("qasm_simulator"), + # shots=1, + # seed_simulator=algorithm_globals.random_seed, + # seed_transpiler=algorithm_globals.random_seed, + # ) + # vqe = VQE( + # ansatz=self.ry_wavefunction, + # optimizer=optimizer, + # gradient=Gradient(), + # expectation=AerPauliExpectation(), + # quantum_instance=quantum_instance, + # max_evals_grouped=1000, + # ) + # vqe.compute_minimum_eigenvalue(operator=self.h2_op) + + def test_with_two_qubit_reduction(self): + """Test the VQE using TwoQubitReduction.""" + qubit_op = PauliSumOp.from_list( + [ + ("IIII", -0.8105479805373266), + ("IIIZ", 0.17218393261915552), + ("IIZZ", -0.22575349222402472), + ("IZZI", 0.1721839326191556), + ("ZZII", -0.22575349222402466), + ("IIZI", 0.1209126326177663), + ("IZZZ", 0.16892753870087912), + ("IXZX", -0.045232799946057854), + ("ZXIX", 0.045232799946057854), + ("IXIX", 0.045232799946057854), + ("ZXZX", -0.045232799946057854), + ("ZZIZ", 0.16614543256382414), + ("IZIZ", 0.16614543256382414), + ("ZZZZ", 0.17464343068300453), + ("ZIZI", 0.1209126326177663), + ] + ) + tapered_qubit_op = TwoQubitReduction(num_particles=2).convert(qubit_op) + vqe = VQE( + self.ry_wavefunction, + SPSA(maxiter=300, last_avg=5), + estimator=Estimator(), + ) + result = vqe.compute_minimum_eigenvalue(tapered_qubit_op) + self.assertAlmostEqual(result.eigenvalue.real, self.h2_energy, places=2) + + # def test_callback(self): + # """Test the callback on VQE.""" + # history = {"eval_count": [], "parameters": [], "mean": [], "std": []} + + # def store_intermediate_result(eval_count, parameters, mean, std): + # history["eval_count"].append(eval_count) + # history["parameters"].append(parameters) + # history["mean"].append(mean) + # history["std"].append(std) + + # optimizer = COBYLA(maxiter=3) + # wavefunction = self.ry_wavefunction + + # vqe = VQE( + # ansatz=wavefunction, + # optimizer=optimizer, + # callback=store_intermediate_result, + # quantum_instance=self.qasm_simulator, + # ) + # vqe.compute_minimum_eigenvalue(operator=self.h2_op) + + # self.assertTrue(all(isinstance(count, int) for count in history["eval_count"])) + # self.assertTrue(all(isinstance(mean, float) for mean in history["mean"])) + # self.assertTrue(all(isinstance(std, float) for std in history["std"])) + # for params in history["parameters"]: + # self.assertTrue(all(isinstance(param, float) for param in params)) + + def test_reuse(self): + """Test re-using a VQE algorithm instance.""" + ansatz = TwoLocal(rotation_blocks=["ry", "rz"], entanglement_blocks="cz") + optimizer = SLSQP() + estimator = Estimator() + vqe = VQE(ansatz, optimizer, estimator) + with self.subTest(msg="assert VQE works once all info is available"): + result = vqe.compute_minimum_eigenvalue(operator=self.h2_op) + self.assertAlmostEqual(result.eigenvalue.real, self.h2_energy, places=5) + + operator = Operator(np.array([[1, 0, 0, 0], [0, -1, 0, 0], [0, 0, 2, 0], [0, 0, 0, 3]])) + + with self.subTest(msg="assert vqe works on re-use."): + result = vqe.compute_minimum_eigenvalue(operator=operator) + self.assertAlmostEqual(result.eigenvalue.real, -1.0, places=5) + + def test_vqe_optimizer(self): + """Test running same VQE twice to re-use optimizer, then switch optimizer""" + vqe = VQE( + ansatz=self.ryrz_wavefunction, + optimizer=SLSQP(), + estimator=Estimator(), + ) + + def run_check(): + result = vqe.compute_minimum_eigenvalue(operator=self.h2_op) + self.assertAlmostEqual(result.eigenvalue.real, self.h2_energy, places=5) + + run_check() + + with self.subTest("Optimizer re-use."): + run_check() + + with self.subTest("Optimizer replace."): + vqe.optimizer = L_BFGS_B() + run_check() + + # def test_batch_evaluate_with_qnspsa(self): + # """Test batch evaluating with QNSPSA works.""" + # ansatz = TwoLocal(2, rotation_blocks=["ry", "rz"], entanglement_blocks="cz") + + # wrapped_backend = BasicAer.get_backend("qasm_simulator") + # inner_backend = BasicAer.get_backend("statevector_simulator") + + # callcount = {"count": 0} + + # def wrapped_run(circuits, **kwargs): + # kwargs["callcount"]["count"] += 1 + # return inner_backend.run(circuits) + + # wrapped_backend.run = partial(wrapped_run, callcount=callcount) + + # fidelity = QNSPSA.get_fidelity(ansatz, backend=wrapped_backend) + # qnspsa = QNSPSA(fidelity, maxiter=5) + + # vqe = VQE( + # ansatz=ansatz, + # optimizer=qnspsa, + # max_evals_grouped=100, + # quantum_instance=wrapped_backend, + # ) + # _ = vqe.compute_minimum_eigenvalue(Z ^ Z) + + # # 1 calibration + 1 stddev estimation + 1 initial blocking + # # + 5 (1 loss + 1 fidelity + 1 blocking) + 1 return loss + 1 VQE eval + # expected = 1 + 1 + 1 + 5 * 3 + 1 + 1 + + # self.assertEqual(callcount["count"], expected) + + def test_optimizer_scipy_callable(self): + """Test passing a SciPy optimizer directly as callable.""" + vqe = VQE( + ansatz=self.ryrz_wavefunction, + optimizer=partial(scipy_minimize, method="L-BFGS-B", options={"maxiter": 10}), + estimator=Estimator(), + ) + result = vqe.compute_minimum_eigenvalue(self.h2_op) + self.assertAlmostEqual(result.eigenvalue.real, self.h2_energy, places=2) + + def test_optimizer_callable(self): + """Test passing a optimizer directly as callable.""" + ansatz = RealAmplitudes(1, reps=1) + vqe = VQE(ansatz, _mock_optimizer, estimator=Estimator()) + result = vqe.compute_minimum_eigenvalue(SparsePauliOp("Z")) + self.assertTrue(np.all(result.optimal_point == np.zeros(ansatz.num_parameters))) + + # def test_aux_operators_list(self): + # """Test list-based aux_operators.""" + # TODO waiting for eval_operators to be ported. + # vqe = VQE(self.ry_wavefunction, SLSQP(), Estimator()) + + # # Start with an empty list + # result = vqe.compute_minimum_eigenvalue(self.h2_op, aux_operators=[]) + # self.assertAlmostEqual(result.eigenvalue.real, self.h2_energy, places=6) + # self.assertIsNone(result.aux_operator_eigenvalues) + + # # Go again with two auxiliary operators + # aux_op1 = PauliSumOp.from_list([("II", 2.0)]) + # aux_op2 = PauliSumOp.from_list([("II", 0.5), ("ZZ", 0.5), ("YY", 0.5), ("XX", -0.5)]) + # aux_ops = [aux_op1, aux_op2] + # result = vqe.compute_minimum_eigenvalue(self.h2_op, aux_operators=aux_ops) + # self.assertAlmostEqual(result.eigenvalue.real, self.h2_energy, places=6) + # self.assertEqual(len(result.aux_operator_eigenvalues), 2) + # # expectation values + # self.assertAlmostEqual(result.aux_operator_eigenvalues[0][0], 2, places=6) + # self.assertAlmostEqual(result.aux_operator_eigenvalues[1][0], 0, places=6) + # # standard deviations + # self.assertAlmostEqual(result.aux_operator_eigenvalues[0][1], 0.0) + # self.assertAlmostEqual(result.aux_operator_eigenvalues[1][1], 0.0) + + # # Go again with additional None and zero operators + # extra_ops = [*aux_ops, None, 0] + # result = vqe.compute_minimum_eigenvalue(self.h2_op, aux_operators=extra_ops) + # self.assertAlmostEqual(result.eigenvalue.real, self.h2_energy, places=6) + # self.assertEqual(len(result.aux_operator_eigenvalues), 4) + # # expectation values + # self.assertAlmostEqual(result.aux_operator_eigenvalues[0][0], 2, places=6) + # self.assertAlmostEqual(result.aux_operator_eigenvalues[1][0], 0, places=6) + # self.assertEqual(result.aux_operator_eigenvalues[2][0], 0.0) + # self.assertEqual(result.aux_operator_eigenvalues[3][0], 0.0) + # # standard deviations + # self.assertAlmostEqual(result.aux_operator_eigenvalues[0][1], 0.0) + # self.assertAlmostEqual(result.aux_operator_eigenvalues[1][1], 0.0) + # self.assertAlmostEqual(result.aux_operator_eigenvalues[2][1], 0.0) + # self.assertAlmostEqual(result.aux_operator_eigenvalues[3][1], 0.0) + + # def test_aux_operators_dict(self): + # """Test dictionary compatibility of aux_operators""" + # vqe = VQE(self.ry_wavefunction, SLSQP(), Estimator()) + + # # Start with an empty dictionary + # result = vqe.compute_minimum_eigenvalue(self.h2_op, aux_operators={}) + # self.assertAlmostEqual(result.eigenvalue.real, self.h2_energy, places=6) + # self.assertIsNone(result.aux_operator_eigenvalues) + + # # Go again with two auxiliary operators + # aux_op1 = PauliSumOp.from_list([("II", 2.0)]) + # aux_op2 = PauliSumOp.from_list([("II", 0.5), ("ZZ", 0.5), ("YY", 0.5), ("XX", -0.5)]) + # aux_ops = {"aux_op1": aux_op1, "aux_op2": aux_op2} + # result = vqe.compute_minimum_eigenvalue(self.h2_op, aux_operators=aux_ops) + # self.assertAlmostEqual(result.eigenvalue.real, self.h2_energy, places=6) + # self.assertEqual(len(result.aux_operator_eigenvalues), 2) + # # expectation values + # self.assertAlmostEqual(result.aux_operator_eigenvalues["aux_op1"][0], 2, places=6) + # self.assertAlmostEqual(result.aux_operator_eigenvalues["aux_op2"][0], 0, places=6) + # # standard deviations + # self.assertAlmostEqual(result.aux_operator_eigenvalues["aux_op1"][1], 0.0) + # self.assertAlmostEqual(result.aux_operator_eigenvalues["aux_op2"][1], 0.0) + + # # Go again with additional None and zero operators + # extra_ops = {**aux_ops, "None_operator": None, "zero_operator": 0} + # result = vqe.compute_minimum_eigenvalue(self.h2_op, aux_operators=extra_ops) + # self.assertAlmostEqual(result.eigenvalue.real, self.h2_energy, places=6) + # self.assertEqual(len(result.aux_operator_eigenvalues), 3) + # # expectation values + # self.assertAlmostEqual(result.aux_operator_eigenvalues["aux_op1"][0], 2, places=6) + # self.assertAlmostEqual(result.aux_operator_eigenvalues["aux_op2"][0], 0, places=6) + # self.assertEqual(result.aux_operator_eigenvalues["zero_operator"][0], 0.0) + # self.assertTrue("None_operator" not in result.aux_operator_eigenvalues.keys()) + # # standard deviations + # self.assertAlmostEqual(result.aux_operator_eigenvalues["aux_op1"][1], 0.0) + # self.assertAlmostEqual(result.aux_operator_eigenvalues["aux_op2"][1], 0.0) + # self.assertAlmostEqual(result.aux_operator_eigenvalues["zero_operator"][1], 0.0) + + # def test_aux_operator_std_dev_pauli(self): + # """Test non-zero standard deviations of aux operators with PauliExpectation.""" + # wavefunction = self.ry_wavefunction + # vqe = VQE( + # ansatz=wavefunction, + # expectation=PauliExpectation(), + # optimizer=COBYLA(maxiter=0), + # quantum_instance=self.qasm_simulator, + # ) + + # # Go again with two auxiliary operators + # aux_op1 = PauliSumOp.from_list([("II", 2.0)]) + # aux_op2 = PauliSumOp.from_list([("II", 0.5), ("ZZ", 0.5), ("YY", 0.5), ("XX", -0.5)]) + # aux_ops = [aux_op1, aux_op2] + # result = vqe.compute_minimum_eigenvalue(self.h2_op, aux_operators=aux_ops) + # self.assertEqual(len(result.aux_operator_eigenvalues), 2) + # # expectation values + # self.assertAlmostEqual(result.aux_operator_eigenvalues[0][0], 2.0, places=6) + # self.assertAlmostEqual(result.aux_operator_eigenvalues[1][0], 0.6796875, places=6) + # # standard deviations + # self.assertAlmostEqual(result.aux_operator_eigenvalues[0][1], 0.0) + # self.assertAlmostEqual(result.aux_operator_eigenvalues[1][1], 0.02534712219145965, places=6) + + # # Go again with additional None and zero operators + # aux_ops = [*aux_ops, None, 0] + # result = vqe.compute_minimum_eigenvalue(self.h2_op, aux_operators=aux_ops) + # self.assertEqual(len(result.aux_operator_eigenvalues), 4) + # # expectation values + # self.assertAlmostEqual(result.aux_operator_eigenvalues[0][0], 2.0, places=6) + # self.assertAlmostEqual(result.aux_operator_eigenvalues[1][0], 0.57421875, places=6) + # self.assertEqual(result.aux_operator_eigenvalues[2][0], 0.0) + # self.assertEqual(result.aux_operator_eigenvalues[3][0], 0.0) + # # # standard deviations + # self.assertAlmostEqual(result.aux_operator_eigenvalues[0][1], 0.0) + # self.assertAlmostEqual( + # result.aux_operator_eigenvalues[1][1], 0.026562146577166837, places=6 + # ) + # self.assertAlmostEqual(result.aux_operator_eigenvalues[2][1], 0.0) + # self.assertAlmostEqual(result.aux_operator_eigenvalues[3][1], 0.0) + + # @unittest.skipUnless(has_aer(), "qiskit-aer doesn't appear to be installed.") + # def test_aux_operator_std_dev_aer_pauli(self): + # """Test non-zero standard deviations of aux operators with AerPauliExpectation.""" + # wavefunction = self.ry_wavefunction + # vqe = VQE( + # ansatz=wavefunction, + # expectation=AerPauliExpectation(), + # optimizer=COBYLA(maxiter=0), + # quantum_instance=QuantumInstance( + # backend=Aer.get_backend("qasm_simulator"), + # shots=1, + # seed_simulator=algorithm_globals.random_seed, + # seed_transpiler=algorithm_globals.random_seed, + # ), + # ) + + # # Go again with two auxiliary operators + # aux_op1 = PauliSumOp.from_list([("II", 2.0)]) + # aux_op2 = PauliSumOp.from_list([("II", 0.5), ("ZZ", 0.5), ("YY", 0.5), ("XX", -0.5)]) + # aux_ops = [aux_op1, aux_op2] + # result = vqe.compute_minimum_eigenvalue(self.h2_op, aux_operators=aux_ops) + # self.assertEqual(len(result.aux_operator_eigenvalues), 2) + # # expectation values + # self.assertAlmostEqual(result.aux_operator_eigenvalues[0][0], 2.0, places=6) + # self.assertAlmostEqual(result.aux_operator_eigenvalues[1][0], 0.6698863565455391, places=6) + # # standard deviations + # self.assertAlmostEqual(result.aux_operator_eigenvalues[0][1], 0.0) + # self.assertAlmostEqual(result.aux_operator_eigenvalues[1][1], 0.0, places=6) + + # # Go again with additional None and zero operators + # aux_ops = [*aux_ops, None, 0] + # result = vqe.compute_minimum_eigenvalue(self.h2_op, aux_operators=aux_ops) + # self.assertEqual(len(result.aux_operator_eigenvalues), 4) + # # expectation values + # self.assertAlmostEqual(result.aux_operator_eigenvalues[0][0], 2.0, places=6) + # self.assertAlmostEqual(result.aux_operator_eigenvalues[1][0], 0.6036400943063891, places=6) + # self.assertEqual(result.aux_operator_eigenvalues[2][0], 0.0) + # self.assertEqual(result.aux_operator_eigenvalues[3][0], 0.0) + # # standard deviations + # self.assertAlmostEqual(result.aux_operator_eigenvalues[0][1], 0.0) + # self.assertAlmostEqual(result.aux_operator_eigenvalues[1][1], 0.0, places=6) + # self.assertAlmostEqual(result.aux_operator_eigenvalues[2][1], 0.0) + # self.assertAlmostEqual(result.aux_operator_eigenvalues[3][1], 0.0) + + # def test_2step_transpile(self): + # """Test the two-step transpiler pass.""" + # # count how often the pass for parameterized circuits is called + # pre_counter = LogPass("pre_passmanager") + # pre_pass = PassManager(pre_counter) + # config = PassManagerConfig(basis_gates=["u3", "cx"]) + # pre_pass += level_1_pass_manager(config) + + # # ... and the pass for bound circuits + # bound_counter = LogPass("bound_pass_manager") + # bound_pass = PassManager(bound_counter) + + # quantum_instance = QuantumInstance( + # backend=BasicAer.get_backend("statevector_simulator"), + # basis_gates=["u3", "cx"], + # pass_manager=pre_pass, + # bound_pass_manager=bound_pass, + # ) + + # optimizer = SPSA(maxiter=5, learning_rate=0.01, perturbation=0.01) + + # vqe = VQE(optimizer=optimizer, quantum_instance=quantum_instance) + # _ = vqe.compute_minimum_eigenvalue(Z) + + # with self.assertLogs(logger, level="INFO") as cm: + # _ = vqe.compute_minimum_eigenvalue(Z) + + # expected = [ + # "pre_passmanager", + # "bound_pass_manager", + # "bound_pass_manager", + # "bound_pass_manager", + # "bound_pass_manager", + # "bound_pass_manager", + # "bound_pass_manager", + # "bound_pass_manager", + # "bound_pass_manager", + # "bound_pass_manager", + # "bound_pass_manager", + # "bound_pass_manager", + # "pre_passmanager", + # "bound_pass_manager", + # ] + # self.assertEqual([record.message for record in cm.records], expected) + + # def test_construct_eigenstate_from_optpoint(self): + # """Test constructing the eigenstate from the optimal point, if the default ansatz is used.""" + + # # use Hamiltonian yielding more than 11 parameters in the default ansatz + # hamiltonian = Z ^ Z ^ Z + # optimizer = SPSA(maxiter=1, learning_rate=0.01, perturbation=0.01) + # quantum_instance = QuantumInstance( + # backend=BasicAer.get_backend("statevector_simulator"), basis_gates=["u3", "cx"] + # ) + # vqe = VQE(optimizer=optimizer, quantum_instance=quantum_instance) + # result = vqe.compute_minimum_eigenvalue(hamiltonian) + + # optimal_circuit = vqe.ansatz.bind_parameters(result.optimal_point) + # self.assertTrue(Statevector(result.eigenstate).equiv(optimal_circuit)) + + +if __name__ == "__main__": + unittest.main() From 8d6bd1cb4a40602b19261596754c9fb4e8494a32 Mon Sep 17 00:00:00 2001 From: Declan Millar Date: Mon, 5 Sep 2022 16:14:42 +0100 Subject: [PATCH 010/100] Have VQE inherit from VariationalAlgorithm. --- qiskit/algorithms/minimum_eigensolvers/vqe.py | 35 +++++++++++++------ 1 file changed, 25 insertions(+), 10 deletions(-) diff --git a/qiskit/algorithms/minimum_eigensolvers/vqe.py b/qiskit/algorithms/minimum_eigensolvers/vqe.py index ff2fa752d4b0..284b6ca1f56d 100644 --- a/qiskit/algorithms/minimum_eigensolvers/vqe.py +++ b/qiskit/algorithms/minimum_eigensolvers/vqe.py @@ -13,8 +13,10 @@ """The variational quantum eigensolver algorithm.""" from __future__ import annotations + import logging from time import time +from collections.abc import Sequence import numpy as np @@ -26,14 +28,14 @@ from ..exceptions import AlgorithmError from ..list_or_dict import ListOrDict from ..optimizers import Optimizer, Minimizer -from ..variational_algorithm import VariationalResult +from ..variational_algorithm import VariationalAlgorithm, VariationalResult from .minimum_eigensolver import MinimumEigensolver, MinimumEigensolverResult logger = logging.getLogger(__name__) -class VQE(MinimumEigensolver): +class VQE(VariationalAlgorithm, MinimumEigensolver): r"""The variational quantum eigensolver (VQE) algorithm. VQE is a quantum algorithm that uses a variational technique to find the minimum eigenvalue of @@ -103,7 +105,7 @@ def __init__( estimator: BaseEstimator, *, gradient=None, - initial_point: np.ndarray | None = None, + initial_point: Sequence[float] | None = None, # TODO Attach callback to optimizer instead. callback=None, ) -> None: @@ -124,20 +126,31 @@ def __init__( self.optimizer = optimizer self.estimator = estimator self.gradient = gradient - self.initial_point = initial_point + + # this has to go via getters and setters due to the VariationalAlgorithm interface + self._initial_point = initial_point + + # TODO Remove this self.callback = callback - # TODO remove this - self._eval_count = 0 + @property + def initial_point(self) -> Sequence[float] | None: + """Return the initial point.""" + return self._initial_point + + @initial_point.setter + def initial_point(self, value: Sequence[float] | None) -> None: + """Set the initial point.""" + self._initial_point = value def compute_minimum_eigenvalue( self, operator: BaseOperator | PauliSumOp, aux_operators: ListOrDict[BaseOperator | PauliSumOp] | None = None, ) -> VQEResult: - ansatz = self._check_operator_ansatz(operator) + iter_count = 0 - self._eval_count = 0 + ansatz = self._check_operator_ansatz(operator) def energy_evaluation(point): job = self.estimator.run([ansatz], [operator], [point]) @@ -148,10 +161,12 @@ def energy_evaluation(point): # return job.result() def expectation(point): + nonlocal iter_count energy = energy_evaluation(point) - self._eval_count += 1 + iter_count += 1 + # TODO remove this if self.callback is not None: - self.callback(self._eval_count, point, energy, 0) + self.callback(iter_count, point, energy, 0) return energy initial_point = self.initial_point From f1ffb54409024e8cb7012f4599c3e7d92a2e8209 Mon Sep 17 00:00:00 2001 From: Declan Millar Date: Mon, 5 Sep 2022 16:38:00 +0100 Subject: [PATCH 011/100] Move energy evaluation to unnested function. --- qiskit/algorithms/minimum_eigensolvers/vqe.py | 49 +++++++++++-------- 1 file changed, 29 insertions(+), 20 deletions(-) diff --git a/qiskit/algorithms/minimum_eigensolvers/vqe.py b/qiskit/algorithms/minimum_eigensolvers/vqe.py index 284b6ca1f56d..1d171eb4717e 100644 --- a/qiskit/algorithms/minimum_eigensolvers/vqe.py +++ b/qiskit/algorithms/minimum_eigensolvers/vqe.py @@ -16,7 +16,7 @@ import logging from time import time -from collections.abc import Sequence +from collections.abc import Callable, Sequence import numpy as np @@ -148,27 +148,8 @@ def compute_minimum_eigenvalue( operator: BaseOperator | PauliSumOp, aux_operators: ListOrDict[BaseOperator | PauliSumOp] | None = None, ) -> VQEResult: - iter_count = 0 - ansatz = self._check_operator_ansatz(operator) - def energy_evaluation(point): - job = self.estimator.run([ansatz], [operator], [point]) - return job.result().values[0] - - # def gradient_evaluation(point): - # job = self.gradient.run([self.ansatz], [operator], [point]) - # return job.result() - - def expectation(point): - nonlocal iter_count - energy = energy_evaluation(point) - iter_count += 1 - # TODO remove this - if self.callback is not None: - self.callback(iter_count, point, energy, 0) - return energy - initial_point = self.initial_point if initial_point is None: initial_point = np.random.random(ansatz.num_parameters) @@ -180,6 +161,8 @@ def expectation(point): start_time = time() + expectation = self.get_energy_evaluation(operator, ansatz) + # Perform optimization if callable(self.optimizer): opt_result = self.optimizer( # pylint: disable=not-callable @@ -217,6 +200,32 @@ def expectation(point): def supports_aux_operators(cls) -> bool: return True + def get_energy_evaluation( + self, + operator: BaseOperator | PauliSumOp, + ansatz: QuantumCircuit, + ) -> tuple[Callable[[np.ndarray], float | list[float]], dict]: + """Returns a function handle to evaluates the energy at given parameters for the ansatz. + This is the objective function to be passed to the optimizer that is used for evaluation. + Args: + operator: The operator whose energy to evaluate. + ansatz: The ansatz preparing the quantum state. + Returns: + Energy of the hamiltonian of each parameter. + """ + iter_count = 0 + + def energy_evaluation(point): + nonlocal iter_count + job = self.estimator.run([ansatz], [operator], [point]) + energy = job.result().values[0] + iter_count += 1 + if self.callback is not None: + self.callback(iter_count, point, energy, 0) + return energy + + return energy_evaluation + def _check_operator_ansatz(self, operator: BaseOperator | PauliSumOp) -> QuantumCircuit: """Check that the number of qubits of operator and ansatz match and that the ansatz is parameterized. From fcb28469b24ae36209d3265339c4af2abf736ec6 Mon Sep 17 00:00:00 2001 From: Declan Millar Date: Mon, 5 Sep 2022 17:20:12 +0100 Subject: [PATCH 012/100] Construct h2_op using SparsePauliOp --- qiskit/algorithms/minimum_eigensolvers/vqe.py | 10 +-- .../minimum_eigensolvers/test_vqe.py | 62 +++++++------------ 2 files changed, 29 insertions(+), 43 deletions(-) diff --git a/qiskit/algorithms/minimum_eigensolvers/vqe.py b/qiskit/algorithms/minimum_eigensolvers/vqe.py index 1d171eb4717e..53ee58e609d2 100644 --- a/qiskit/algorithms/minimum_eigensolvers/vqe.py +++ b/qiskit/algorithms/minimum_eigensolvers/vqe.py @@ -20,10 +20,12 @@ import numpy as np +from qiskit.algorithms.gradients import BaseEstimatorGradient from qiskit.circuit import QuantumCircuit -from qiskit.quantum_info.operators.base_operator import BaseOperator from qiskit.opflow import PauliSumOp from qiskit.primitives import BaseEstimator +from qiskit.quantum_info.operators.base_operator import BaseOperator + from ..exceptions import AlgorithmError from ..list_or_dict import ListOrDict @@ -104,7 +106,7 @@ def __init__( optimizer: Optimizer | Minimizer, estimator: BaseEstimator, *, - gradient=None, + gradient=BaseEstimatorGradient, initial_point: Sequence[float] | None = None, # TODO Attach callback to optimizer instead. callback=None, @@ -166,11 +168,11 @@ def compute_minimum_eigenvalue( # Perform optimization if callable(self.optimizer): opt_result = self.optimizer( # pylint: disable=not-callable - fun=expectation, x0=initial_point # , jac=gradient, bounds=bounds + fun=expectation, x0=initial_point,# jac=self.gradient, # bounds=bounds ) else: opt_result = self.optimizer.minimize( - fun=expectation, x0=initial_point # , jac=gradient, bounds=bounds + fun=expectation, x0=initial_point, #jac=self.gradient, # bounds=bounds ) eval_time = time() - start_time diff --git a/test/python/algorithms/minimum_eigensolvers/test_vqe.py b/test/python/algorithms/minimum_eigensolvers/test_vqe.py index cee17d0cdb2a..ab76a6ca7d86 100644 --- a/test/python/algorithms/minimum_eigensolvers/test_vqe.py +++ b/test/python/algorithms/minimum_eigensolvers/test_vqe.py @@ -22,7 +22,7 @@ from scipy.optimize import minimize as scipy_minimize from ddt import data, ddt, unpack -from qiskit import BasicAer, QuantumCircuit +from qiskit import QuantumCircuit from qiskit.algorithms import AlgorithmError from qiskit.algorithms.minimum_eigensolvers.vqe import VQE from qiskit.algorithms.optimizers import ( @@ -37,28 +37,11 @@ OptimizerResult, ) from qiskit.circuit.library import EfficientSU2, RealAmplitudes, TwoLocal -from qiskit.exceptions import MissingOptionalLibraryError -from qiskit.opflow import ( - AerPauliExpectation, - Gradient, - I, - MatrixExpectation, - PauliExpectation, - PauliSumOp, - PrimitiveOp, - TwoQubitReduction, - X, - Z, -) +from qiskit.opflow import PauliSumOp, TwoQubitReduction from qiskit.quantum_info import SparsePauliOp, Operator from qiskit.primitives import Estimator -from qiskit.quantum_info import Statevector -from qiskit.transpiler import PassManager, PassManagerConfig -from qiskit.transpiler.preset_passmanagers import level_1_pass_manager -from qiskit.utils import QuantumInstance, algorithm_globals, has_aer +from qiskit.utils import algorithm_globals -if has_aer(): - from qiskit import Aer logger = "LocalLogger" @@ -92,12 +75,22 @@ def setUp(self): super().setUp() self.seed = 50 algorithm_globals.random_seed = self.seed - self.h2_op = ( - -1.052373245772859 * (I ^ I) - + 0.39793742484318045 * (I ^ Z) - - 0.39793742484318045 * (Z ^ I) - - 0.01128010425623538 * (Z ^ Z) - + 0.18093119978423156 * (X ^ X) + # self.h2_op = ( + # -1.052373245772859 * (I ^ I) + # + 0.39793742484318045 * (I ^ Z) + # - 0.39793742484318045 * (Z ^ I) + # - 0.01128010425623538 * (Z ^ Z) + # + 0.18093119978423156 * (X ^ X) + # ) + self.h2_op = SparsePauliOp( + ["II", "IZ", "ZI", "ZZ", "XX"], + coeffs=[ + -1.052373245772859, + 0.39793742484318045, + -0.39793742484318045, + -0.01128010425623538, + 0.18093119978423156, + ], ) self.h2_energy = -1.85727503 @@ -186,7 +179,6 @@ def test_missing_ansatz_params(self): # result = vqe.compute_minimum_eigenvalue(operator=self.h2_op) # self.assertAlmostEqual(result.eigenvalue.real, self.h2_energy, places=places) - # @unittest.skipUnless(has_aer(), "qiskit-aer doesn't appear to be installed.") # @data( # CG(maxiter=1), # L_BFGS_B(maxfun=1), @@ -195,20 +187,12 @@ def test_missing_ansatz_params(self): # TNC(maxiter=1), # ) # def test_with_gradient(self, optimizer): - # """Test VQE using Gradient().""" - # quantum_instance = QuantumInstance( - # backend=Aer.get_backend("qasm_simulator"), - # shots=1, - # seed_simulator=algorithm_globals.random_seed, - # seed_transpiler=algorithm_globals.random_seed, - # ) + # """Test VQE using gradient primitive.""" # vqe = VQE( - # ansatz=self.ry_wavefunction, - # optimizer=optimizer, + # self.ry_wavefunction, + # optimizer, + # Estimator(), # gradient=Gradient(), - # expectation=AerPauliExpectation(), - # quantum_instance=quantum_instance, - # max_evals_grouped=1000, # ) # vqe.compute_minimum_eigenvalue(operator=self.h2_op) From 903614c3a6eaa540ccde320a8ae79192cc2a444b Mon Sep 17 00:00:00 2001 From: Declan Millar Date: Tue, 6 Sep 2022 11:51:55 +0100 Subject: [PATCH 013/100] Add gradient with primitives support. --- qiskit/algorithms/minimum_eigensolvers/vqe.py | 41 +++- .../minimum_eigensolvers/test_vqe.py | 215 +++++++----------- 2 files changed, 105 insertions(+), 151 deletions(-) diff --git a/qiskit/algorithms/minimum_eigensolvers/vqe.py b/qiskit/algorithms/minimum_eigensolvers/vqe.py index 53ee58e609d2..31ccc21d00f7 100644 --- a/qiskit/algorithms/minimum_eigensolvers/vqe.py +++ b/qiskit/algorithms/minimum_eigensolvers/vqe.py @@ -106,10 +106,10 @@ def __init__( optimizer: Optimizer | Minimizer, estimator: BaseEstimator, *, - gradient=BaseEstimatorGradient, + gradient: BaseEstimatorGradient | None = None, initial_point: Sequence[float] | None = None, # TODO Attach callback to optimizer instead. - callback=None, + callback: Callable[[int, np.ndarray, float, float], None] | None = None, ) -> None: """ Args: @@ -163,16 +163,19 @@ def compute_minimum_eigenvalue( start_time = time() - expectation = self.get_energy_evaluation(operator, ansatz) + eval_energy = self.get_eval_energy(ansatz, operator) + + if self.gradient is not None: + eval_gradient = self.get_eval_gradient(ansatz, operator) + else: + eval_gradient = None # Perform optimization if callable(self.optimizer): - opt_result = self.optimizer( # pylint: disable=not-callable - fun=expectation, x0=initial_point,# jac=self.gradient, # bounds=bounds - ) + opt_result = self.optimizer(fun=eval_energy, x0=initial_point, jac=eval_gradient) else: opt_result = self.optimizer.minimize( - fun=expectation, x0=initial_point, #jac=self.gradient, # bounds=bounds + fun=eval_energy, x0=initial_point, jac=eval_gradient ) eval_time = time() - start_time @@ -202,10 +205,10 @@ def compute_minimum_eigenvalue( def supports_aux_operators(cls) -> bool: return True - def get_energy_evaluation( + def get_eval_energy( self, - operator: BaseOperator | PauliSumOp, ansatz: QuantumCircuit, + operator: BaseOperator | PauliSumOp, ) -> tuple[Callable[[np.ndarray], float | list[float]], dict]: """Returns a function handle to evaluates the energy at given parameters for the ansatz. This is the objective function to be passed to the optimizer that is used for evaluation. @@ -217,16 +220,30 @@ def get_energy_evaluation( """ iter_count = 0 - def energy_evaluation(point): + def eval_energy(point): nonlocal iter_count job = self.estimator.run([ansatz], [operator], [point]) energy = job.result().values[0] iter_count += 1 if self.callback is not None: - self.callback(iter_count, point, energy, 0) + self.callback(iter_count, point, energy, 0.0) return energy - return energy_evaluation + return eval_energy + + def get_eval_gradient( + self, + ansatz: QuantumCircuit, + operator: BaseOperator | PauliSumOp, + ) -> tuple[Callable[[np.ndarray], np.ndarray]]: + """Returns a function handle to evaluate the gradient for a given point.""" + + def eval_gradient(parameters): + # broadcasting not required for the estimator gradients + result = self.gradient.run([ansatz], [operator], [parameters]).result() + return result.gradients[0] + + return eval_gradient def _check_operator_ansatz(self, operator: BaseOperator | PauliSumOp) -> QuantumCircuit: """Check that the number of qubits of operator and ansatz match and that the ansatz is diff --git a/test/python/algorithms/minimum_eigensolvers/test_vqe.py b/test/python/algorithms/minimum_eigensolvers/test_vqe.py index ab76a6ca7d86..bce08c832202 100644 --- a/test/python/algorithms/minimum_eigensolvers/test_vqe.py +++ b/test/python/algorithms/minimum_eigensolvers/test_vqe.py @@ -24,6 +24,7 @@ from qiskit import QuantumCircuit from qiskit.algorithms import AlgorithmError +from qiskit.algorithms.gradients import ParamShiftEstimatorGradient from qiskit.algorithms.minimum_eigensolvers.vqe import VQE from qiskit.algorithms.optimizers import ( CG, @@ -34,12 +35,14 @@ SLSQP, SPSA, TNC, + GradientDescent, OptimizerResult, ) from qiskit.circuit.library import EfficientSU2, RealAmplitudes, TwoLocal from qiskit.opflow import PauliSumOp, TwoQubitReduction from qiskit.quantum_info import SparsePauliOp, Operator from qiskit.primitives import Estimator +from qiskit.test.decorators import slow_test from qiskit.utils import algorithm_globals @@ -58,12 +61,15 @@ def run(self, dag): # pylint: disable=invalid-name, unused-argument -def _mock_optimizer(fun, x0, jac=None, bounds=None) -> OptimizerResult: +def _mock_optimizer(fun, x0, jac=None, bounds=None, inputs=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 + + if inputs is not None: + inputs.update({"fun": fun, "x0": x0, "jac": jac, "bounds": bounds}) return result @@ -75,13 +81,6 @@ def setUp(self): super().setUp() self.seed = 50 algorithm_globals.random_seed = self.seed - # self.h2_op = ( - # -1.052373245772859 * (I ^ I) - # + 0.39793742484318045 * (I ^ Z) - # - 0.39793742484318045 * (Z ^ I) - # - 0.01128010425623538 * (Z ^ Z) - # + 0.18093119978423156 * (X ^ X) - # ) self.h2_op = SparsePauliOp( ["II", "IZ", "ZI", "ZZ", "XX"], coeffs=[ @@ -164,6 +163,7 @@ def test_missing_ansatz_params(self): with self.assertRaises(AlgorithmError): vqe.compute_minimum_eigenvalue(operator=self.h2_op) + # TODO setting max evals grouped causes an error. # @data( # (SLSQP(maxiter=50, max_evals_grouped=5), 4), # (SPSA(maxiter=150, max_evals_grouped=2), 2), @@ -179,22 +179,51 @@ def test_missing_ansatz_params(self): # result = vqe.compute_minimum_eigenvalue(operator=self.h2_op) # self.assertAlmostEqual(result.eigenvalue.real, self.h2_energy, places=places) - # @data( - # CG(maxiter=1), - # L_BFGS_B(maxfun=1), - # P_BFGS(maxfun=1, max_processes=0), - # SLSQP(maxiter=1), - # TNC(maxiter=1), - # ) - # def test_with_gradient(self, optimizer): - # """Test VQE using gradient primitive.""" - # vqe = VQE( - # self.ry_wavefunction, - # optimizer, - # Estimator(), - # gradient=Gradient(), - # ) - # vqe.compute_minimum_eigenvalue(operator=self.h2_op) + @data( + CG(), + L_BFGS_B(), + P_BFGS(), + SLSQP(), + TNC(), + ) + def test_with_gradient(self, optimizer): + """Test VQE using gradient primitive.""" + estimator = Estimator() + vqe = VQE( + self.ry_wavefunction, + optimizer, + estimator, + gradient=ParamShiftEstimatorGradient(estimator), + ) + result = vqe.compute_minimum_eigenvalue(operator=self.h2_op) + self.assertAlmostEqual(result.eigenvalue.real, self.h2_energy, places=5) + + def test_gradient_passed(self): + """Test the gradient is properly passed into the optimizer.""" + inputs = {} + estimator = Estimator() + vqe = VQE( + RealAmplitudes(), + partial(_mock_optimizer, inputs=inputs), + estimator, + gradient=ParamShiftEstimatorGradient(estimator), + ) + _ = vqe.compute_minimum_eigenvalue(operator=self.h2_op) + + self.assertIsNotNone(inputs["jac"]) + + # @slow_test + def test_gradient_run(self): + """Test using the gradient to calculate the minimum.""" + estimator = Estimator() + vqe = VQE( + RealAmplitudes(), + GradientDescent(maxiter=200, learning_rate=0.1), + estimator, + gradient=ParamShiftEstimatorGradient(estimator), + ) + result = vqe.compute_minimum_eigenvalue(operator=self.h2_op) + self.assertAlmostEqual(result.eigenvalue.real, self.h2_energy, places=5) def test_with_two_qubit_reduction(self): """Test the VQE using TwoQubitReduction.""" @@ -226,32 +255,32 @@ def test_with_two_qubit_reduction(self): result = vqe.compute_minimum_eigenvalue(tapered_qubit_op) self.assertAlmostEqual(result.eigenvalue.real, self.h2_energy, places=2) - # def test_callback(self): - # """Test the callback on VQE.""" - # history = {"eval_count": [], "parameters": [], "mean": [], "std": []} + def test_callback(self): + """Test the callback on VQE.""" + history = {"eval_count": [], "parameters": [], "mean": [], "std": []} - # def store_intermediate_result(eval_count, parameters, mean, std): - # history["eval_count"].append(eval_count) - # history["parameters"].append(parameters) - # history["mean"].append(mean) - # history["std"].append(std) + def store_intermediate_result(eval_count, parameters, mean, std): + history["eval_count"].append(eval_count) + history["parameters"].append(parameters) + history["mean"].append(mean) + history["std"].append(std) - # optimizer = COBYLA(maxiter=3) - # wavefunction = self.ry_wavefunction + optimizer = COBYLA(maxiter=3) + wavefunction = self.ry_wavefunction - # vqe = VQE( - # ansatz=wavefunction, - # optimizer=optimizer, - # callback=store_intermediate_result, - # quantum_instance=self.qasm_simulator, - # ) - # vqe.compute_minimum_eigenvalue(operator=self.h2_op) + vqe = VQE( + wavefunction, + optimizer, + Estimator(), + callback=store_intermediate_result, + ) + vqe.compute_minimum_eigenvalue(operator=self.h2_op) - # self.assertTrue(all(isinstance(count, int) for count in history["eval_count"])) - # self.assertTrue(all(isinstance(mean, float) for mean in history["mean"])) - # self.assertTrue(all(isinstance(std, float) for std in history["std"])) - # for params in history["parameters"]: - # self.assertTrue(all(isinstance(param, float) for param in params)) + self.assertTrue(all(isinstance(count, int) for count in history["eval_count"])) + self.assertTrue(all(isinstance(mean, float) for mean in history["mean"])) + self.assertTrue(all(isinstance(std, float) for std in history["std"])) + for params in history["parameters"]: + self.assertTrue(all(isinstance(param, float) for param in params)) def test_reuse(self): """Test re-using a VQE algorithm instance.""" @@ -290,38 +319,6 @@ def run_check(): vqe.optimizer = L_BFGS_B() run_check() - # def test_batch_evaluate_with_qnspsa(self): - # """Test batch evaluating with QNSPSA works.""" - # ansatz = TwoLocal(2, rotation_blocks=["ry", "rz"], entanglement_blocks="cz") - - # wrapped_backend = BasicAer.get_backend("qasm_simulator") - # inner_backend = BasicAer.get_backend("statevector_simulator") - - # callcount = {"count": 0} - - # def wrapped_run(circuits, **kwargs): - # kwargs["callcount"]["count"] += 1 - # return inner_backend.run(circuits) - - # wrapped_backend.run = partial(wrapped_run, callcount=callcount) - - # fidelity = QNSPSA.get_fidelity(ansatz, backend=wrapped_backend) - # qnspsa = QNSPSA(fidelity, maxiter=5) - - # vqe = VQE( - # ansatz=ansatz, - # optimizer=qnspsa, - # max_evals_grouped=100, - # quantum_instance=wrapped_backend, - # ) - # _ = vqe.compute_minimum_eigenvalue(Z ^ Z) - - # # 1 calibration + 1 stddev estimation + 1 initial blocking - # # + 5 (1 loss + 1 fidelity + 1 blocking) + 1 return loss + 1 VQE eval - # expected = 1 + 1 + 1 + 5 * 3 + 1 + 1 - - # self.assertEqual(callcount["count"], expected) - def test_optimizer_scipy_callable(self): """Test passing a SciPy optimizer directly as callable.""" vqe = VQE( @@ -339,9 +336,9 @@ def test_optimizer_callable(self): result = vqe.compute_minimum_eigenvalue(SparsePauliOp("Z")) self.assertTrue(np.all(result.optimal_point == np.zeros(ansatz.num_parameters))) + # TODO waiting for eval_operators to be ported. # def test_aux_operators_list(self): # """Test list-based aux_operators.""" - # TODO waiting for eval_operators to be ported. # vqe = VQE(self.ry_wavefunction, SLSQP(), Estimator()) # # Start with an empty list @@ -501,66 +498,6 @@ def test_optimizer_callable(self): # self.assertAlmostEqual(result.aux_operator_eigenvalues[2][1], 0.0) # self.assertAlmostEqual(result.aux_operator_eigenvalues[3][1], 0.0) - # def test_2step_transpile(self): - # """Test the two-step transpiler pass.""" - # # count how often the pass for parameterized circuits is called - # pre_counter = LogPass("pre_passmanager") - # pre_pass = PassManager(pre_counter) - # config = PassManagerConfig(basis_gates=["u3", "cx"]) - # pre_pass += level_1_pass_manager(config) - - # # ... and the pass for bound circuits - # bound_counter = LogPass("bound_pass_manager") - # bound_pass = PassManager(bound_counter) - - # quantum_instance = QuantumInstance( - # backend=BasicAer.get_backend("statevector_simulator"), - # basis_gates=["u3", "cx"], - # pass_manager=pre_pass, - # bound_pass_manager=bound_pass, - # ) - - # optimizer = SPSA(maxiter=5, learning_rate=0.01, perturbation=0.01) - - # vqe = VQE(optimizer=optimizer, quantum_instance=quantum_instance) - # _ = vqe.compute_minimum_eigenvalue(Z) - - # with self.assertLogs(logger, level="INFO") as cm: - # _ = vqe.compute_minimum_eigenvalue(Z) - - # expected = [ - # "pre_passmanager", - # "bound_pass_manager", - # "bound_pass_manager", - # "bound_pass_manager", - # "bound_pass_manager", - # "bound_pass_manager", - # "bound_pass_manager", - # "bound_pass_manager", - # "bound_pass_manager", - # "bound_pass_manager", - # "bound_pass_manager", - # "bound_pass_manager", - # "pre_passmanager", - # "bound_pass_manager", - # ] - # self.assertEqual([record.message for record in cm.records], expected) - - # def test_construct_eigenstate_from_optpoint(self): - # """Test constructing the eigenstate from the optimal point, if the default ansatz is used.""" - - # # use Hamiltonian yielding more than 11 parameters in the default ansatz - # hamiltonian = Z ^ Z ^ Z - # optimizer = SPSA(maxiter=1, learning_rate=0.01, perturbation=0.01) - # quantum_instance = QuantumInstance( - # backend=BasicAer.get_backend("statevector_simulator"), basis_gates=["u3", "cx"] - # ) - # vqe = VQE(optimizer=optimizer, quantum_instance=quantum_instance) - # result = vqe.compute_minimum_eigenvalue(hamiltonian) - - # optimal_circuit = vqe.ansatz.bind_parameters(result.optimal_point) - # self.assertTrue(Statevector(result.eigenstate).equiv(optimal_circuit)) - if __name__ == "__main__": unittest.main() From 27d02b0d6641402012e3c3400cd0928d48d9c368 Mon Sep 17 00:00:00 2001 From: Declan Millar Date: Tue, 6 Sep 2022 13:55:45 +0100 Subject: [PATCH 014/100] Update docstrings --- qiskit/algorithms/minimum_eigensolvers/vqe.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/qiskit/algorithms/minimum_eigensolvers/vqe.py b/qiskit/algorithms/minimum_eigensolvers/vqe.py index 31ccc21d00f7..54540506837b 100644 --- a/qiskit/algorithms/minimum_eigensolvers/vqe.py +++ b/qiskit/algorithms/minimum_eigensolvers/vqe.py @@ -94,6 +94,7 @@ def my_minimizer(fun, x0, jac=None, bounds=None) -> OptimizerResult: initial_point: An optional initial point (i.e. initial parameter values) for the optimizer. If not provided, a random initial point with values in the interval :math:`[0, 2\pi]` is used. + callback: An optional callback function to plot the energy at each evaluation. References: [1] Peruzzo et al, "A variational eigenvalue solver on a quantum processor" @@ -108,7 +109,7 @@ def __init__( *, gradient: BaseEstimatorGradient | None = None, initial_point: Sequence[float] | None = None, - # TODO Attach callback to optimizer instead. + # TODO Remove callback and attach to optimizer instead. callback: Callable[[int, np.ndarray, float, float], None] | None = None, ) -> None: """ @@ -121,6 +122,7 @@ def __init__( 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. + callback: An optional callback function to plot the energy at each evaluation. """ super().__init__() @@ -129,10 +131,11 @@ def __init__( self.estimator = estimator self.gradient = gradient - # this has to go via getters and setters due to the VariationalAlgorithm interface + # TODO Change Variational Algorithm interface to use a public attribute. + # This has to go via getters and setters due to the VariationalAlgorithm interface. self._initial_point = initial_point - # TODO Remove this + # TODO Remove callback and attach to optimizer instead. self.callback = callback @property @@ -170,7 +173,7 @@ def compute_minimum_eigenvalue( else: eval_gradient = None - # Perform optimization + # Perform optimization. if callable(self.optimizer): opt_result = self.optimizer(fun=eval_energy, x0=initial_point, jac=eval_gradient) else: @@ -188,7 +191,7 @@ def compute_minimum_eigenvalue( aux_values = None if aux_operators: # Not None and not empty list - # TODO use eval_operators + # TODO This is going to be replaced by eval_observables. See PR #8683. aux_values = self._eval_aux_ops(ansatz, optimal_point, aux_operators) result = VQEResult() From 813a344d681c7b4214d0cee9b330f63ede536978 Mon Sep 17 00:00:00 2001 From: Declan Millar Date: Wed, 7 Sep 2022 11:54:09 +0100 Subject: [PATCH 015/100] update broadcast handling --- qiskit/algorithms/minimum_eigensolvers/vqe.py | 30 +++-- qiskit/algorithms/optimizers/qnspsa.py | 4 +- qiskit/algorithms/optimizers/spsa.py | 25 +++- .../minimum_eigensolvers/test_vqe.py | 115 ++++++++++++------ 4 files changed, 120 insertions(+), 54 deletions(-) diff --git a/qiskit/algorithms/minimum_eigensolvers/vqe.py b/qiskit/algorithms/minimum_eigensolvers/vqe.py index 54540506837b..8dfaadfb4e38 100644 --- a/qiskit/algorithms/minimum_eigensolvers/vqe.py +++ b/qiskit/algorithms/minimum_eigensolvers/vqe.py @@ -109,7 +109,7 @@ def __init__( *, gradient: BaseEstimatorGradient | None = None, initial_point: Sequence[float] | None = None, - # TODO Remove callback and attach to optimizer instead. + # TODO remove callback and attach to optimizer instead callback: Callable[[int, np.ndarray, float, float], None] | None = None, ) -> None: """ @@ -131,11 +131,11 @@ def __init__( self.estimator = estimator self.gradient = gradient - # TODO Change Variational Algorithm interface to use a public attribute. - # This has to go via getters and setters due to the VariationalAlgorithm interface. + # TODO change VariationalAlgorithm interface to use a public attribute + # this has to go via getters and setters due to the VariationalAlgorithm interface self._initial_point = initial_point - # TODO Remove callback and attach to optimizer instead. + # TODO remove callback and attach to optimizer instead self.callback = callback @property @@ -173,7 +173,7 @@ def compute_minimum_eigenvalue( else: eval_gradient = None - # Perform optimization. + # perform optimization if callable(self.optimizer): opt_result = self.optimizer(fun=eval_energy, x0=initial_point, jac=eval_gradient) else: @@ -190,8 +190,8 @@ def compute_minimum_eigenvalue( aux_values = None if aux_operators: - # Not None and not empty list - # TODO This is going to be replaced by eval_observables. See PR #8683. + # not None and not empty list + # TODO this is going to be replaced by eval_observables (see PR #8683) aux_values = self._eval_aux_ops(ansatz, optimal_point, aux_operators) result = VQEResult() @@ -222,14 +222,20 @@ def get_eval_energy( Energy of the hamiltonian of each parameter. """ iter_count = 0 + num_parameters = ansatz.num_parameters - def eval_energy(point): + def eval_energy(parameters): nonlocal iter_count - job = self.estimator.run([ansatz], [operator], [point]) + + # handle broadcasting: ensure parameters is of shape [array, array, ...] + parameters = np.reshape(parameters, (-1, num_parameters)).tolist() + batchsize = len(parameters) + + job = self.estimator.run(batchsize * [ansatz], batchsize * [operator], parameters) energy = job.result().values[0] iter_count += 1 if self.callback is not None: - self.callback(iter_count, point, energy, 0.0) + self.callback(iter_count, parameters, energy, 0.0) return energy return eval_energy @@ -239,7 +245,7 @@ def get_eval_gradient( ansatz: QuantumCircuit, operator: BaseOperator | PauliSumOp, ) -> tuple[Callable[[np.ndarray], np.ndarray]]: - """Returns a function handle to evaluate the gradient for a given point.""" + """Returns a function handle to evaluate the gradient at given parameters for the ansatz.""" def eval_gradient(parameters): # broadcasting not required for the estimator gradients @@ -279,7 +285,7 @@ def _eval_aux_ops( ) -> ListOrDict[tuple(complex, complex)]: """Compute auxiliary operator eigenvalues.""" - # TODO This is going to be replaced by eval_observables. See PR #8683. + # TODO this is going to be replaced by eval_observables (see PR #8683) if isinstance(aux_operators, dict): aux_ops = list(aux_operators.values()) diff --git a/qiskit/algorithms/optimizers/qnspsa.py b/qiskit/algorithms/optimizers/qnspsa.py index f74f91e51f19..15a4e602803c 100644 --- a/qiskit/algorithms/optimizers/qnspsa.py +++ b/qiskit/algorithms/optimizers/qnspsa.py @@ -187,7 +187,9 @@ def _point_sample(self, loss, x, eps, delta1, delta2): self._nfev += 6 loss_values = _batch_evaluate(loss, loss_points, self._max_evals_grouped) - fidelity_values = _batch_evaluate(self.fidelity, fidelity_points, self._max_evals_grouped) + fidelity_values = _batch_evaluate( + self.fidelity, fidelity_points, self._max_evals_grouped, unpack_points=True + ) # compute the gradient approximation and additionally return the loss function evaluations gradient_estimate = (loss_values[0] - loss_values[1]) / (2 * eps) * delta1 diff --git a/qiskit/algorithms/optimizers/spsa.py b/qiskit/algorithms/optimizers/spsa.py index 10d81dfdd41c..b1caf2360816 100644 --- a/qiskit/algorithms/optimizers/spsa.py +++ b/qiskit/algorithms/optimizers/spsa.py @@ -711,7 +711,7 @@ def constant(eta=0.01): yield eta -def _batch_evaluate(function, points, max_evals_grouped): +def _batch_evaluate(function, points, max_evals_grouped, unpack_points=False): # if the function cannot handle lists of points as input, cover this case immediately if max_evals_grouped == 1: # support functions with multiple arguments where the points are given in a tuple @@ -731,11 +731,32 @@ def _batch_evaluate(function, points, max_evals_grouped): results = [] for batch in batched_points: - results += function(batch).tolist() + if unpack_points: + batch = _repack_points(batch) + results += _as_list(function(*batch)) + else: + results += _as_list(function(batch)) return results +def _as_list(obj): + """Convert a list or numpy array into a list.""" + return obj if isinstance(obj, list) else obj.tolist() + + +def _repack_points(points): + """Turn a list of tuples of points into a tuple of lists of points. + E.g. turns + [(a1, a2, a3), (b1, b2, b3)] + into + ([a1, b1], [a2, b2], [a3, b3]) + where all elements are np.ndarray. + """ + num_sets = len(points[0]) # length of (a1, a2, a3) + return ([x[i] for x in points] for i in range(num_sets)) + + def _make_spd(matrix, bias=0.01): identity = np.identity(matrix.shape[0]) psd = scipy.linalg.sqrtm(matrix.dot(matrix)) diff --git a/test/python/algorithms/minimum_eigensolvers/test_vqe.py b/test/python/algorithms/minimum_eigensolvers/test_vqe.py index bce08c832202..d7c9912c9333 100644 --- a/test/python/algorithms/minimum_eigensolvers/test_vqe.py +++ b/test/python/algorithms/minimum_eigensolvers/test_vqe.py @@ -38,10 +38,11 @@ GradientDescent, OptimizerResult, ) +from qiskit.algorithms.state_fidelities import ComputeUncompute from qiskit.circuit.library import EfficientSU2, RealAmplitudes, TwoLocal from qiskit.opflow import PauliSumOp, TwoQubitReduction -from qiskit.quantum_info import SparsePauliOp, Operator -from qiskit.primitives import Estimator +from qiskit.quantum_info import SparsePauliOp, Operator, Pauli +from qiskit.primitives import Estimator, Sampler from qiskit.test.decorators import slow_test from qiskit.utils import algorithm_globals @@ -163,21 +164,17 @@ def test_missing_ansatz_params(self): with self.assertRaises(AlgorithmError): vqe.compute_minimum_eigenvalue(operator=self.h2_op) - # TODO setting max evals grouped causes an error. - # @data( - # (SLSQP(maxiter=50, max_evals_grouped=5), 4), - # (SPSA(maxiter=150, max_evals_grouped=2), 2), - # ) - # @unpack - # def test_max_evals_grouped(self, optimizer, places): - # """VQE Optimizers test""" - # vqe = VQE( - # self.ryrz_wavefunction, - # optimizer, - # estimator=Estimator(), - # ) - # result = vqe.compute_minimum_eigenvalue(operator=self.h2_op) - # self.assertAlmostEqual(result.eigenvalue.real, self.h2_energy, places=places) + @unpack + def test_max_evals_grouped(self): + """VQE Optimizers test""" + optimizer = SLSQP(maxiter=50, max_evals_grouped=5) + vqe = VQE( + self.ryrz_wavefunction, + optimizer, + estimator=Estimator(), + ) + result = vqe.compute_minimum_eigenvalue(operator=self.h2_op) + self.assertAlmostEqual(result.eigenvalue.real, self.h2_energy, places=5) @data( CG(), @@ -255,32 +252,32 @@ def test_with_two_qubit_reduction(self): result = vqe.compute_minimum_eigenvalue(tapered_qubit_op) self.assertAlmostEqual(result.eigenvalue.real, self.h2_energy, places=2) - def test_callback(self): - """Test the callback on VQE.""" - history = {"eval_count": [], "parameters": [], "mean": [], "std": []} + # def test_callback(self): + # """Test the callback on VQE.""" + # history = {"eval_count": [], "parameters": [], "mean": [], "std": []} - def store_intermediate_result(eval_count, parameters, mean, std): - history["eval_count"].append(eval_count) - history["parameters"].append(parameters) - history["mean"].append(mean) - history["std"].append(std) + # def store_intermediate_result(eval_count, parameters, mean, std): + # history["eval_count"].append(eval_count) + # history["parameters"].append(parameters) + # history["mean"].append(mean) + # history["std"].append(std) - optimizer = COBYLA(maxiter=3) - wavefunction = self.ry_wavefunction + # optimizer = COBYLA(maxiter=3) + # wavefunction = self.ry_wavefunction - vqe = VQE( - wavefunction, - optimizer, - Estimator(), - callback=store_intermediate_result, - ) - vqe.compute_minimum_eigenvalue(operator=self.h2_op) + # vqe = VQE( + # wavefunction, + # optimizer, + # Estimator(), + # callback=store_intermediate_result, + # ) + # vqe.compute_minimum_eigenvalue(operator=self.h2_op) - self.assertTrue(all(isinstance(count, int) for count in history["eval_count"])) - self.assertTrue(all(isinstance(mean, float) for mean in history["mean"])) - self.assertTrue(all(isinstance(std, float) for std in history["std"])) - for params in history["parameters"]: - self.assertTrue(all(isinstance(param, float) for param in params)) + # self.assertTrue(all(isinstance(count, int) for count in history["eval_count"])) + # self.assertTrue(all(isinstance(mean, float) for mean in history["mean"])) + # self.assertTrue(all(isinstance(std, float) for std in history["std"])) + # for params in history["parameters"]: + # self.assertTrue(all(isinstance(param, float) for param in params)) def test_reuse(self): """Test re-using a VQE algorithm instance.""" @@ -319,6 +316,46 @@ def run_check(): vqe.optimizer = L_BFGS_B() run_check() + # def test_batch_evaluate_with_qnspsa(self): + # """Test batch evaluating with QNSPSA works.""" + # ansatz = TwoLocal(2, rotation_blocks=["ry", "rz"], entanglement_blocks="cz") + + # wrapped_sampler = Sampler() + # inner_estimator = Estimator() + + # callcount = {"count": 0} + + # def wrapped_run(*args, **kwargs): + # kwargs["callcount"]["count"] += 1 + # return inner_estimator.run(*args, **kwargs) + + # wrapped_sampler.run = partial(wrapped_run, callcount=callcount) + + # fidelity = ComputeUncompute(wrapped_sampler) + + # def fidelity_callable(left, right): + # batchsize = np.asarray(left).shape[0] + # job = fidelity.run(batchsize * [ansatz], batchsize * [ansatz], left, right) + # return job.result().fidelities + + # qnspsa = QNSPSA(fidelity_callable, maxiter=5) + # qnspsa.set_max_evals_grouped(100) + + # vqe = VQE( + # ansatz, + # qnspsa, + # wrapped_sampler, + # ) + # _ = vqe.compute_minimum_eigenvalue(Pauli("ZZ")) + + # print("COUNT", callcount["count"]) + + # # 1 calibration + 1 stddev estimation + 1 initial blocking + # # + 5 (1 loss + 1 fidelity + 1 blocking) + 1 return loss + 1 VQE eval + # expected = 1 + 1 + 1 + 5 * 3 + 1 + 1 + + # self.assertEqual(callcount["count"], expected) + def test_optimizer_scipy_callable(self): """Test passing a SciPy optimizer directly as callable.""" vqe = VQE( From 70ba38bf0feb3da4d5c8b4dd72901f6d414fec20 Mon Sep 17 00:00:00 2001 From: Declan Millar Date: Wed, 7 Sep 2022 13:06:07 +0100 Subject: [PATCH 016/100] update eval_energy output for batching --- qiskit/algorithms/minimum_eigensolvers/vqe.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/qiskit/algorithms/minimum_eigensolvers/vqe.py b/qiskit/algorithms/minimum_eigensolvers/vqe.py index 8dfaadfb4e38..ab1d68099c25 100644 --- a/qiskit/algorithms/minimum_eigensolvers/vqe.py +++ b/qiskit/algorithms/minimum_eigensolvers/vqe.py @@ -232,11 +232,11 @@ def eval_energy(parameters): batchsize = len(parameters) job = self.estimator.run(batchsize * [ansatz], batchsize * [operator], parameters) - energy = job.result().values[0] + values = job.result().values iter_count += 1 if self.callback is not None: - self.callback(iter_count, parameters, energy, 0.0) - return energy + self.callback(iter_count, parameters, values[0], 0.0) + return values[0] if len(values) == 1 else values return eval_energy From 36dfd3b881216cb625860de6252c5cd16e1219a4 Mon Sep 17 00:00:00 2001 From: Declan Millar Date: Wed, 7 Sep 2022 13:43:19 +0100 Subject: [PATCH 017/100] add incomplete QNSPA test --- .../minimum_eigensolvers/test_vqe.py | 56 +++++++++---------- 1 file changed, 27 insertions(+), 29 deletions(-) diff --git a/test/python/algorithms/minimum_eigensolvers/test_vqe.py b/test/python/algorithms/minimum_eigensolvers/test_vqe.py index d7c9912c9333..bb844081e78a 100644 --- a/test/python/algorithms/minimum_eigensolvers/test_vqe.py +++ b/test/python/algorithms/minimum_eigensolvers/test_vqe.py @@ -316,45 +316,43 @@ def run_check(): vqe.optimizer = L_BFGS_B() run_check() - # def test_batch_evaluate_with_qnspsa(self): - # """Test batch evaluating with QNSPSA works.""" - # ansatz = TwoLocal(2, rotation_blocks=["ry", "rz"], entanglement_blocks="cz") + def test_batch_evaluate_with_qnspsa(self): + """Test batch evaluating with QNSPSA works.""" + ansatz = TwoLocal(2, rotation_blocks=["ry", "rz"], entanglement_blocks="cz") - # wrapped_sampler = Sampler() - # inner_estimator = Estimator() + wrapped_sampler = Sampler() + inner_estimator = Estimator() - # callcount = {"count": 0} + callcount = {"count": 0} - # def wrapped_run(*args, **kwargs): - # kwargs["callcount"]["count"] += 1 - # return inner_estimator.run(*args, **kwargs) + def wrapped_run(*args, **kwargs): + kwargs["callcount"]["count"] += 1 + return inner_estimator.run(*args, **kwargs) - # wrapped_sampler.run = partial(wrapped_run, callcount=callcount) + wrapped_sampler.run = partial(wrapped_run, callcount=callcount) - # fidelity = ComputeUncompute(wrapped_sampler) + fidelity = ComputeUncompute(wrapped_sampler) - # def fidelity_callable(left, right): - # batchsize = np.asarray(left).shape[0] - # job = fidelity.run(batchsize * [ansatz], batchsize * [ansatz], left, right) - # return job.result().fidelities + def fidelity_callable(left, right): + batchsize = np.asarray(left).shape[0] + job = fidelity.run(batchsize * [ansatz], batchsize * [ansatz], left, right) + return job.result().fidelities - # qnspsa = QNSPSA(fidelity_callable, maxiter=5) - # qnspsa.set_max_evals_grouped(100) + qnspsa = QNSPSA(fidelity_callable, maxiter=5) + qnspsa.set_max_evals_grouped(100) - # vqe = VQE( - # ansatz, - # qnspsa, - # wrapped_sampler, - # ) - # _ = vqe.compute_minimum_eigenvalue(Pauli("ZZ")) - - # print("COUNT", callcount["count"]) + vqe = VQE( + ansatz, + qnspsa, + wrapped_sampler, + ) + _ = vqe.compute_minimum_eigenvalue(Pauli("ZZ")) - # # 1 calibration + 1 stddev estimation + 1 initial blocking - # # + 5 (1 loss + 1 fidelity + 1 blocking) + 1 return loss + 1 VQE eval - # expected = 1 + 1 + 1 + 5 * 3 + 1 + 1 + # 1 calibration + 1 stddev estimation + 1 initial blocking + # + 5 (1 loss + 1 fidelity + 1 blocking) + 1 return loss + 1 VQE eval + expected = 1 + 1 + 1 + 5 * 3 + 1 + 1 - # self.assertEqual(callcount["count"], expected) + self.assertEqual(callcount["count"], expected) def test_optimizer_scipy_callable(self): """Test passing a SciPy optimizer directly as callable.""" From 3c79faa6c5be550a4891f114e3737d4fe25c9064 Mon Sep 17 00:00:00 2001 From: Declan Millar Date: Wed, 7 Sep 2022 14:55:14 +0100 Subject: [PATCH 018/100] fix batch evaluation of QNSPSA test --- .../minimum_eigensolvers/test_vqe.py | 27 +++++++++++++------ 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/test/python/algorithms/minimum_eigensolvers/test_vqe.py b/test/python/algorithms/minimum_eigensolvers/test_vqe.py index bb844081e78a..63862529570e 100644 --- a/test/python/algorithms/minimum_eigensolvers/test_vqe.py +++ b/test/python/algorithms/minimum_eigensolvers/test_vqe.py @@ -321,15 +321,23 @@ def test_batch_evaluate_with_qnspsa(self): ansatz = TwoLocal(2, rotation_blocks=["ry", "rz"], entanglement_blocks="cz") wrapped_sampler = Sampler() + inner_sampler = Sampler() + + wrapped_estimator = Estimator() inner_estimator = Estimator() - callcount = {"count": 0} + callcount = {"sampler": 0, "estimator": 0} - def wrapped_run(*args, **kwargs): - kwargs["callcount"]["count"] += 1 + def wrapped_estimator_run(*args, **kwargs): + kwargs["callcount"]["estimator"] += 1 return inner_estimator.run(*args, **kwargs) - wrapped_sampler.run = partial(wrapped_run, callcount=callcount) + def wrapped_sampler_run(*args, **kwargs): + kwargs["callcount"]["sampler"] += 1 + return inner_sampler.run(*args, **kwargs) + + wrapped_estimator.run = partial(wrapped_estimator_run, callcount=callcount) + wrapped_sampler.run = partial(wrapped_sampler_run, callcount=callcount) fidelity = ComputeUncompute(wrapped_sampler) @@ -344,15 +352,18 @@ def fidelity_callable(left, right): vqe = VQE( ansatz, qnspsa, - wrapped_sampler, + wrapped_estimator, ) _ = vqe.compute_minimum_eigenvalue(Pauli("ZZ")) + # 5 (fidelity) + expected_sampler_runs = 5 # 1 calibration + 1 stddev estimation + 1 initial blocking - # + 5 (1 loss + 1 fidelity + 1 blocking) + 1 return loss + 1 VQE eval - expected = 1 + 1 + 1 + 5 * 3 + 1 + 1 + # + 5 (1 loss + 1 blocking) + 1 return loss + expected_estimator_runs = 1 + 1 + 1 + 5 * 2 + 1 - self.assertEqual(callcount["count"], expected) + self.assertEqual(callcount["sampler"], expected_sampler_runs) + self.assertEqual(callcount["estimator"], expected_estimator_runs) def test_optimizer_scipy_callable(self): """Test passing a SciPy optimizer directly as callable.""" From 4695cc3331b7116d5e82113bd2d5acc8d6ab1ddf Mon Sep 17 00:00:00 2001 From: Declan Millar Date: Wed, 7 Sep 2022 15:53:10 +0100 Subject: [PATCH 019/100] remove vqe callback --- qiskit/algorithms/minimum_eigensolvers/vqe.py | 13 --------- .../minimum_eigensolvers/test_vqe.py | 29 +------------------ 2 files changed, 1 insertion(+), 41 deletions(-) diff --git a/qiskit/algorithms/minimum_eigensolvers/vqe.py b/qiskit/algorithms/minimum_eigensolvers/vqe.py index ab1d68099c25..ad43954424ae 100644 --- a/qiskit/algorithms/minimum_eigensolvers/vqe.py +++ b/qiskit/algorithms/minimum_eigensolvers/vqe.py @@ -94,7 +94,6 @@ def my_minimizer(fun, x0, jac=None, bounds=None) -> OptimizerResult: initial_point: An optional initial point (i.e. initial parameter values) for the optimizer. If not provided, a random initial point with values in the interval :math:`[0, 2\pi]` is used. - callback: An optional callback function to plot the energy at each evaluation. References: [1] Peruzzo et al, "A variational eigenvalue solver on a quantum processor" @@ -109,8 +108,6 @@ def __init__( *, gradient: BaseEstimatorGradient | None = None, initial_point: Sequence[float] | None = None, - # TODO remove callback and attach to optimizer instead - callback: Callable[[int, np.ndarray, float, float], None] | None = None, ) -> None: """ Args: @@ -122,7 +119,6 @@ def __init__( 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. - callback: An optional callback function to plot the energy at each evaluation. """ super().__init__() @@ -135,9 +131,6 @@ def __init__( # this has to go via getters and setters due to the VariationalAlgorithm interface self._initial_point = initial_point - # TODO remove callback and attach to optimizer instead - self.callback = callback - @property def initial_point(self) -> Sequence[float] | None: """Return the initial point.""" @@ -221,21 +214,15 @@ def get_eval_energy( Returns: Energy of the hamiltonian of each parameter. """ - iter_count = 0 num_parameters = ansatz.num_parameters def eval_energy(parameters): - nonlocal iter_count - # handle broadcasting: ensure parameters is of shape [array, array, ...] parameters = np.reshape(parameters, (-1, num_parameters)).tolist() batchsize = len(parameters) job = self.estimator.run(batchsize * [ansatz], batchsize * [operator], parameters) values = job.result().values - iter_count += 1 - if self.callback is not None: - self.callback(iter_count, parameters, values[0], 0.0) return values[0] if len(values) == 1 else values return eval_energy diff --git a/test/python/algorithms/minimum_eigensolvers/test_vqe.py b/test/python/algorithms/minimum_eigensolvers/test_vqe.py index 63862529570e..3c8e8a33e3c9 100644 --- a/test/python/algorithms/minimum_eigensolvers/test_vqe.py +++ b/test/python/algorithms/minimum_eigensolvers/test_vqe.py @@ -252,33 +252,6 @@ def test_with_two_qubit_reduction(self): result = vqe.compute_minimum_eigenvalue(tapered_qubit_op) self.assertAlmostEqual(result.eigenvalue.real, self.h2_energy, places=2) - # def test_callback(self): - # """Test the callback on VQE.""" - # history = {"eval_count": [], "parameters": [], "mean": [], "std": []} - - # def store_intermediate_result(eval_count, parameters, mean, std): - # history["eval_count"].append(eval_count) - # history["parameters"].append(parameters) - # history["mean"].append(mean) - # history["std"].append(std) - - # optimizer = COBYLA(maxiter=3) - # wavefunction = self.ry_wavefunction - - # vqe = VQE( - # wavefunction, - # optimizer, - # Estimator(), - # callback=store_intermediate_result, - # ) - # vqe.compute_minimum_eigenvalue(operator=self.h2_op) - - # self.assertTrue(all(isinstance(count, int) for count in history["eval_count"])) - # self.assertTrue(all(isinstance(mean, float) for mean in history["mean"])) - # self.assertTrue(all(isinstance(std, float) for std in history["std"])) - # for params in history["parameters"]: - # self.assertTrue(all(isinstance(param, float) for param in params)) - def test_reuse(self): """Test re-using a VQE algorithm instance.""" ansatz = TwoLocal(rotation_blocks=["ry", "rz"], entanglement_blocks="cz") @@ -360,7 +333,7 @@ def fidelity_callable(left, right): expected_sampler_runs = 5 # 1 calibration + 1 stddev estimation + 1 initial blocking # + 5 (1 loss + 1 blocking) + 1 return loss - expected_estimator_runs = 1 + 1 + 1 + 5 * 2 + 1 + expected_estimator_runs = 1 + 1 + 1 + 5 * 2 + 1 + 1 self.assertEqual(callcount["sampler"], expected_sampler_runs) self.assertEqual(callcount["estimator"], expected_estimator_runs) From 105c5dbbca2f131b05a535d8fcd5424367e31d49 Mon Sep 17 00:00:00 2001 From: Declan Millar Date: Wed, 7 Sep 2022 16:32:06 +0100 Subject: [PATCH 020/100] move estimator to first arg --- qiskit/algorithms/minimum_eigensolvers/vqe.py | 8 +-- .../minimum_eigensolvers/test_vqe.py | 60 +++++++------------ 2 files changed, 26 insertions(+), 42 deletions(-) diff --git a/qiskit/algorithms/minimum_eigensolvers/vqe.py b/qiskit/algorithms/minimum_eigensolvers/vqe.py index ad43954424ae..44d47da29ccd 100644 --- a/qiskit/algorithms/minimum_eigensolvers/vqe.py +++ b/qiskit/algorithms/minimum_eigensolvers/vqe.py @@ -86,10 +86,10 @@ def my_minimizer(fun, x0, jac=None, bounds=None) -> OptimizerResult: optimizer = partial(minimize, method="L-BFGS-B") Attributes: + estimator: The estimator primitive to compute the expectation value of the circuits. ansatz: The parameterized circuit used as an ansatz for the wave function. optimizer: A classical optimizer to find the minimum energy. This can either be a Qiskit :class:`.Optimizer` or a callable implementing the :class:`.Minimizer` protocol. - estimator: The estimator primitive to compute the expectation value of the circuits. gradient: An optional gradient function or operator for the optimizer. initial_point: An optional initial point (i.e. initial parameter values) for the optimizer. If not provided, a random initial point with values in the interval :math:`[0, 2\pi]` @@ -102,19 +102,19 @@ def my_minimizer(fun, x0, jac=None, bounds=None) -> OptimizerResult: def __init__( self, + estimator: BaseEstimator, ansatz: QuantumCircuit, optimizer: Optimizer | Minimizer, - estimator: BaseEstimator, *, gradient: BaseEstimatorGradient | None = None, initial_point: Sequence[float] | None = None, ) -> None: """ Args: + estimator: The estimator primitive to compute the expectation value of the circuits. ansatz: The parameterized circuit used as ansatz for the wave function. optimizer: The 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. - estimator: The estimator primitive to compute the expectation value of the circuits. gradient: An optional gradient function or operator for the optimizer. 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 @@ -122,9 +122,9 @@ def __init__( """ super().__init__() + self.estimator = estimator self.ansatz = ansatz self.optimizer = optimizer - self.estimator = estimator self.gradient = gradient # TODO change VariationalAlgorithm interface to use a public attribute diff --git a/test/python/algorithms/minimum_eigensolvers/test_vqe.py b/test/python/algorithms/minimum_eigensolvers/test_vqe.py index 3c8e8a33e3c9..6bde5dd9486c 100644 --- a/test/python/algorithms/minimum_eigensolvers/test_vqe.py +++ b/test/python/algorithms/minimum_eigensolvers/test_vqe.py @@ -47,21 +47,6 @@ from qiskit.utils import algorithm_globals -logger = "LocalLogger" - - -class LogPass(DummyAP): - """A dummy analysis pass that logs when executed""" - - def __init__(self, message): - super().__init__() - self.message = message - - 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, inputs=None) -> OptimizerResult: """A mock of a callable that can be used as minimizer in the VQE.""" result = OptimizerResult() @@ -99,7 +84,7 @@ def setUp(self): def test_basic_aer_statevector(self): """Test VQE using reference Estimator.""" - vqe = VQE(self.ryrz_wavefunction, L_BFGS_B(), Estimator()) + vqe = VQE(Estimator(), self.ryrz_wavefunction, L_BFGS_B()) result = vqe.compute_minimum_eigenvalue(operator=self.h2_op) @@ -122,7 +107,7 @@ def test_circuit_input(self): """Test running the VQE on a plain QuantumCircuit object.""" wavefunction = QuantumCircuit(2).compose(EfficientSU2(2)) optimizer = SLSQP(maxiter=50) - vqe = VQE(wavefunction, optimizer, Estimator()) + vqe = VQE(Estimator(), wavefunction, optimizer) result = vqe.compute_minimum_eigenvalue(self.h2_op) self.assertAlmostEqual(result.eigenvalue.real, self.h2_energy, places=5) @@ -132,9 +117,9 @@ def test_invalid_initial_point(self): initial_point = np.array([1]) vqe = VQE( + Estimator(), ansatz, SLSQP(), - Estimator(), initial_point=initial_point, ) @@ -144,7 +129,7 @@ def test_invalid_initial_point(self): def test_ansatz_resize(self): """Test the ansatz is properly resized if it's a blueprint circuit.""" ansatz = RealAmplitudes(1, reps=1) - vqe = VQE(ansatz, SLSQP(), Estimator()) + vqe = VQE(Estimator(), ansatz, SLSQP()) result = vqe.compute_minimum_eigenvalue(self.h2_op) self.assertAlmostEqual(result.eigenvalue.real, self.h2_energy, places=5) @@ -152,7 +137,7 @@ def test_invalid_ansatz_size(self): """Test an error is raised if the ansatz has the wrong number of qubits.""" ansatz = QuantumCircuit(1) ansatz.compose(RealAmplitudes(1, reps=2)) - vqe = VQE(ansatz, SLSQP(), Estimator()) + vqe = VQE(Estimator(), ansatz, SLSQP()) with self.assertRaises(AlgorithmError): _ = vqe.compute_minimum_eigenvalue(operator=self.h2_op) @@ -160,7 +145,7 @@ def test_invalid_ansatz_size(self): def test_missing_ansatz_params(self): """Test specifying an ansatz with no parameters raises an error.""" ansatz = QuantumCircuit(self.h2_op.num_qubits) - vqe = VQE(ansatz, SLSQP(), Estimator()) + vqe = VQE(Estimator(), ansatz, SLSQP()) with self.assertRaises(AlgorithmError): vqe.compute_minimum_eigenvalue(operator=self.h2_op) @@ -169,9 +154,9 @@ def test_max_evals_grouped(self): """VQE Optimizers test""" optimizer = SLSQP(maxiter=50, max_evals_grouped=5) vqe = VQE( + Estimator(), self.ryrz_wavefunction, optimizer, - estimator=Estimator(), ) result = vqe.compute_minimum_eigenvalue(operator=self.h2_op) self.assertAlmostEqual(result.eigenvalue.real, self.h2_energy, places=5) @@ -187,9 +172,9 @@ def test_with_gradient(self, optimizer): """Test VQE using gradient primitive.""" estimator = Estimator() vqe = VQE( + estimator, self.ry_wavefunction, optimizer, - estimator, gradient=ParamShiftEstimatorGradient(estimator), ) result = vqe.compute_minimum_eigenvalue(operator=self.h2_op) @@ -200,9 +185,9 @@ def test_gradient_passed(self): inputs = {} estimator = Estimator() vqe = VQE( + estimator, RealAmplitudes(), partial(_mock_optimizer, inputs=inputs), - estimator, gradient=ParamShiftEstimatorGradient(estimator), ) _ = vqe.compute_minimum_eigenvalue(operator=self.h2_op) @@ -214,9 +199,9 @@ def test_gradient_run(self): """Test using the gradient to calculate the minimum.""" estimator = Estimator() vqe = VQE( + estimator, RealAmplitudes(), GradientDescent(maxiter=200, learning_rate=0.1), - estimator, gradient=ParamShiftEstimatorGradient(estimator), ) result = vqe.compute_minimum_eigenvalue(operator=self.h2_op) @@ -245,9 +230,9 @@ def test_with_two_qubit_reduction(self): ) tapered_qubit_op = TwoQubitReduction(num_particles=2).convert(qubit_op) vqe = VQE( + Estimator(), self.ry_wavefunction, SPSA(maxiter=300, last_avg=5), - estimator=Estimator(), ) result = vqe.compute_minimum_eigenvalue(tapered_qubit_op) self.assertAlmostEqual(result.eigenvalue.real, self.h2_energy, places=2) @@ -256,8 +241,7 @@ def test_reuse(self): """Test re-using a VQE algorithm instance.""" ansatz = TwoLocal(rotation_blocks=["ry", "rz"], entanglement_blocks="cz") optimizer = SLSQP() - estimator = Estimator() - vqe = VQE(ansatz, optimizer, estimator) + vqe = VQE(Estimator(), ansatz, SLSQP()) with self.subTest(msg="assert VQE works once all info is available"): result = vqe.compute_minimum_eigenvalue(operator=self.h2_op) self.assertAlmostEqual(result.eigenvalue.real, self.h2_energy, places=5) @@ -268,12 +252,12 @@ def test_reuse(self): result = vqe.compute_minimum_eigenvalue(operator=operator) self.assertAlmostEqual(result.eigenvalue.real, -1.0, places=5) - def test_vqe_optimizer(self): + def test_vqe_optimizer_reuse(self): """Test running same VQE twice to re-use optimizer, then switch optimizer""" vqe = VQE( - ansatz=self.ryrz_wavefunction, - optimizer=SLSQP(), - estimator=Estimator(), + Estimator(), + self.ryrz_wavefunction, + SLSQP(), ) def run_check(): @@ -323,9 +307,9 @@ def fidelity_callable(left, right): qnspsa.set_max_evals_grouped(100) vqe = VQE( + wrapped_estimator, ansatz, qnspsa, - wrapped_estimator, ) _ = vqe.compute_minimum_eigenvalue(Pauli("ZZ")) @@ -333,7 +317,7 @@ def fidelity_callable(left, right): expected_sampler_runs = 5 # 1 calibration + 1 stddev estimation + 1 initial blocking # + 5 (1 loss + 1 blocking) + 1 return loss - expected_estimator_runs = 1 + 1 + 1 + 5 * 2 + 1 + 1 + expected_estimator_runs = 1 + 1 + 1 + 5 * 2 + 1 self.assertEqual(callcount["sampler"], expected_sampler_runs) self.assertEqual(callcount["estimator"], expected_estimator_runs) @@ -341,9 +325,9 @@ def fidelity_callable(left, right): def test_optimizer_scipy_callable(self): """Test passing a SciPy optimizer directly as callable.""" vqe = VQE( - ansatz=self.ryrz_wavefunction, - optimizer=partial(scipy_minimize, method="L-BFGS-B", options={"maxiter": 10}), - estimator=Estimator(), + Estimator(), + self.ryrz_wavefunction, + partial(scipy_minimize, method="L-BFGS-B", options={"maxiter": 10}), ) result = vqe.compute_minimum_eigenvalue(self.h2_op) self.assertAlmostEqual(result.eigenvalue.real, self.h2_energy, places=2) @@ -351,7 +335,7 @@ def test_optimizer_scipy_callable(self): def test_optimizer_callable(self): """Test passing a optimizer directly as callable.""" ansatz = RealAmplitudes(1, reps=1) - vqe = VQE(ansatz, _mock_optimizer, estimator=Estimator()) + vqe = VQE(Estimator(), ansatz, _mock_optimizer) result = vqe.compute_minimum_eigenvalue(SparsePauliOp("Z")) self.assertTrue(np.all(result.optimal_point == np.zeros(ansatz.num_parameters))) From 9131190aeff0cbec05ea1fe03aa6cc30cbe28cf0 Mon Sep 17 00:00:00 2001 From: Declan Millar Date: Wed, 7 Sep 2022 17:12:29 +0100 Subject: [PATCH 021/100] remove usused imports --- .../minimum_eigensolvers/test_vqe.py | 28 ++++++------------- 1 file changed, 8 insertions(+), 20 deletions(-) diff --git a/test/python/algorithms/minimum_eigensolvers/test_vqe.py b/test/python/algorithms/minimum_eigensolvers/test_vqe.py index 6bde5dd9486c..7c660e0543cf 100644 --- a/test/python/algorithms/minimum_eigensolvers/test_vqe.py +++ b/test/python/algorithms/minimum_eigensolvers/test_vqe.py @@ -12,15 +12,13 @@ """Test the variational quantum eigensolver algorithm.""" -import logging import unittest from test.python.algorithms import QiskitAlgorithmsTestCase -from test.python.transpiler._dummy_passes import DummyAP from functools import partial import numpy as np from scipy.optimize import minimize as scipy_minimize -from ddt import data, ddt, unpack +from ddt import data, ddt from qiskit import QuantumCircuit from qiskit.algorithms import AlgorithmError @@ -29,21 +27,20 @@ from qiskit.algorithms.optimizers import ( CG, COBYLA, + GradientDescent, L_BFGS_B, + OptimizerResult, P_BFGS, QNSPSA, SLSQP, SPSA, TNC, - GradientDescent, - OptimizerResult, ) from qiskit.algorithms.state_fidelities import ComputeUncompute from qiskit.circuit.library import EfficientSU2, RealAmplitudes, TwoLocal from qiskit.opflow import PauliSumOp, TwoQubitReduction from qiskit.quantum_info import SparsePauliOp, Operator, Pauli from qiskit.primitives import Estimator, Sampler -from qiskit.test.decorators import slow_test from qiskit.utils import algorithm_globals @@ -82,14 +79,15 @@ def setUp(self): self.ryrz_wavefunction = TwoLocal(rotation_blocks=["ry", "rz"], entanglement_blocks="cz") self.ry_wavefunction = TwoLocal(rotation_blocks="ry", entanglement_blocks="cz") - def test_basic_aer_statevector(self): + @data(L_BFGS_B(), COBYLA()) + def test_basic_aer_statevector(self, estimator): """Test VQE using reference Estimator.""" - vqe = VQE(Estimator(), self.ryrz_wavefunction, L_BFGS_B()) + vqe = VQE(Estimator(), self.ryrz_wavefunction, estimator) result = vqe.compute_minimum_eigenvalue(operator=self.h2_op) with self.subTest(msg="test eigenvalue"): - self.assertAlmostEqual(result.eigenvalue.real, self.h2_energy) + self.assertAlmostEqual(result.eigenvalue.real, self.h2_energy, places=5) with self.subTest(msg="test optimal_value"): self.assertAlmostEqual(result.optimal_value, self.h2_energy) @@ -103,14 +101,6 @@ def test_basic_aer_statevector(self): with self.subTest(msg="assert optimizer_time is set"): self.assertIsNotNone(result.optimizer_time) - def test_circuit_input(self): - """Test running the VQE on a plain QuantumCircuit object.""" - wavefunction = QuantumCircuit(2).compose(EfficientSU2(2)) - optimizer = SLSQP(maxiter=50) - vqe = VQE(Estimator(), wavefunction, optimizer) - result = vqe.compute_minimum_eigenvalue(self.h2_op) - self.assertAlmostEqual(result.eigenvalue.real, self.h2_energy, places=5) - def test_invalid_initial_point(self): """Test the proper error is raised when the initial point has the wrong size.""" ansatz = self.ryrz_wavefunction @@ -149,9 +139,8 @@ def test_missing_ansatz_params(self): with self.assertRaises(AlgorithmError): vqe.compute_minimum_eigenvalue(operator=self.h2_op) - @unpack def test_max_evals_grouped(self): - """VQE Optimizers test""" + """Test with SLSQP with max_evals_grouped.""" optimizer = SLSQP(maxiter=50, max_evals_grouped=5) vqe = VQE( Estimator(), @@ -194,7 +183,6 @@ def test_gradient_passed(self): self.assertIsNotNone(inputs["jac"]) - # @slow_test def test_gradient_run(self): """Test using the gradient to calculate the minimum.""" estimator = Estimator() From c72639776b79989b3e9bf5b56fdbe82047b37a45 Mon Sep 17 00:00:00 2001 From: Declan Millar Date: Wed, 7 Sep 2022 17:20:47 +0100 Subject: [PATCH 022/100] add minimum eigensolvers test init file --- .../algorithms/minimum_eigensolvers/__init__.py | 11 +++++++++++ .../algorithms/minimum_eigensolvers/test_vqe.py | 5 ++--- 2 files changed, 13 insertions(+), 3 deletions(-) create mode 100644 test/python/algorithms/minimum_eigensolvers/__init__.py diff --git a/test/python/algorithms/minimum_eigensolvers/__init__.py b/test/python/algorithms/minimum_eigensolvers/__init__.py new file mode 100644 index 000000000000..fdb172d367f0 --- /dev/null +++ b/test/python/algorithms/minimum_eigensolvers/__init__.py @@ -0,0 +1,11 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2022. +# +# 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. diff --git a/test/python/algorithms/minimum_eigensolvers/test_vqe.py b/test/python/algorithms/minimum_eigensolvers/test_vqe.py index 7c660e0543cf..46cb83ba4755 100644 --- a/test/python/algorithms/minimum_eigensolvers/test_vqe.py +++ b/test/python/algorithms/minimum_eigensolvers/test_vqe.py @@ -37,13 +37,13 @@ TNC, ) from qiskit.algorithms.state_fidelities import ComputeUncompute -from qiskit.circuit.library import EfficientSU2, RealAmplitudes, TwoLocal +from qiskit.circuit.library import RealAmplitudes, TwoLocal from qiskit.opflow import PauliSumOp, TwoQubitReduction from qiskit.quantum_info import SparsePauliOp, Operator, Pauli from qiskit.primitives import Estimator, Sampler from qiskit.utils import algorithm_globals - +# pylint: disable=invalid-name def _mock_optimizer(fun, x0, jac=None, bounds=None, inputs=None) -> OptimizerResult: """A mock of a callable that can be used as minimizer in the VQE.""" result = OptimizerResult() @@ -228,7 +228,6 @@ def test_with_two_qubit_reduction(self): def test_reuse(self): """Test re-using a VQE algorithm instance.""" ansatz = TwoLocal(rotation_blocks=["ry", "rz"], entanglement_blocks="cz") - optimizer = SLSQP() vqe = VQE(Estimator(), ansatz, SLSQP()) with self.subTest(msg="assert VQE works once all info is available"): result = vqe.compute_minimum_eigenvalue(operator=self.h2_op) From cfdea10a7d30c42efadfe048e94e87c9aa1d5f7f Mon Sep 17 00:00:00 2001 From: Declan Millar Date: Thu, 8 Sep 2022 13:29:16 +0100 Subject: [PATCH 023/100] add aux ops tests and prepare for new eval_operators --- qiskit/algorithms/minimum_eigensolvers/vqe.py | 32 ++- .../minimum_eigensolvers/test_vqe.py | 235 ++++++------------ 2 files changed, 95 insertions(+), 172 deletions(-) diff --git a/qiskit/algorithms/minimum_eigensolvers/vqe.py b/qiskit/algorithms/minimum_eigensolvers/vqe.py index 44d47da29ccd..d9f8c446bd19 100644 --- a/qiskit/algorithms/minimum_eigensolvers/vqe.py +++ b/qiskit/algorithms/minimum_eigensolvers/vqe.py @@ -26,13 +26,13 @@ from qiskit.primitives import BaseEstimator from qiskit.quantum_info.operators.base_operator import BaseOperator - from ..exceptions import AlgorithmError from ..list_or_dict import ListOrDict from ..optimizers import Optimizer, Minimizer from ..variational_algorithm import VariationalAlgorithm, VariationalResult from .minimum_eigensolver import MinimumEigensolver, MinimumEigensolverResult +from qiskit.algorithms.observables_evaluator import eval_observables logger = logging.getLogger(__name__) @@ -184,7 +184,6 @@ def compute_minimum_eigenvalue( aux_values = None if aux_operators: # not None and not empty list - # TODO this is going to be replaced by eval_observables (see PR #8683) aux_values = self._eval_aux_ops(ansatz, optimal_point, aux_operators) result = VQEResult() @@ -272,19 +271,21 @@ def _eval_aux_ops( ) -> ListOrDict[tuple(complex, complex)]: """Compute auxiliary operator eigenvalues.""" - # TODO this is going to be replaced by eval_observables (see PR #8683) - - if isinstance(aux_operators, dict): - aux_ops = list(aux_operators.values()) - else: + if isinstance(aux_operators, list): non_nones = [i for i, x in enumerate(aux_operators) if x is not None] - aux_ops = [x for x in aux_operators if x is not None] + filtered_ops = [x for x in aux_operators if x is not None] + else: + # filtered_ops = {k: v for k, v in aux_operators.items() if v is not None} + filtered_ops = list(aux_operators.values()) + # TODO this is going to be replaced by eval_observables (see PR #8683) + # bound_ansatz = ansatz.bind_parameters(parameters) + # aux_values = eval_observables(self.estimator, bound_ansatz, filtered_ops) aux_values = None - if aux_ops: - num_aux_ops = len(aux_ops) + if filtered_ops: + num_aux_ops = len(filtered_ops) aux_job = self.estimator.run( - [ansatz] * num_aux_ops, aux_ops, [parameters] * num_aux_ops + [ansatz] * num_aux_ops, filtered_ops, [parameters] * num_aux_ops ) aux_eigs = aux_job.result().values aux_eigs = list(zip(aux_eigs, [0] * len(aux_eigs))) @@ -295,7 +296,14 @@ def _eval_aux_ops( for i, x in enumerate(non_nones): aux_values[x] = aux_eigs[i] - return aux_values + if isinstance(aux_values, list): + aux_eigs = [None] * len(aux_operators) + for i, x in enumerate(non_nones): + aux_eigs[x] = aux_values[i] + else: + aux_eigs = aux_values + + return aux_eigs class VQEResult(VariationalResult, MinimumEigensolverResult): diff --git a/test/python/algorithms/minimum_eigensolvers/test_vqe.py b/test/python/algorithms/minimum_eigensolvers/test_vqe.py index 46cb83ba4755..6fd974b35d0e 100644 --- a/test/python/algorithms/minimum_eigensolvers/test_vqe.py +++ b/test/python/algorithms/minimum_eigensolvers/test_vqe.py @@ -327,166 +327,81 @@ def test_optimizer_callable(self): self.assertTrue(np.all(result.optimal_point == np.zeros(ansatz.num_parameters))) # TODO waiting for eval_operators to be ported. - # def test_aux_operators_list(self): - # """Test list-based aux_operators.""" - # vqe = VQE(self.ry_wavefunction, SLSQP(), Estimator()) - - # # Start with an empty list - # result = vqe.compute_minimum_eigenvalue(self.h2_op, aux_operators=[]) - # self.assertAlmostEqual(result.eigenvalue.real, self.h2_energy, places=6) - # self.assertIsNone(result.aux_operator_eigenvalues) - - # # Go again with two auxiliary operators - # aux_op1 = PauliSumOp.from_list([("II", 2.0)]) - # aux_op2 = PauliSumOp.from_list([("II", 0.5), ("ZZ", 0.5), ("YY", 0.5), ("XX", -0.5)]) - # aux_ops = [aux_op1, aux_op2] - # result = vqe.compute_minimum_eigenvalue(self.h2_op, aux_operators=aux_ops) - # self.assertAlmostEqual(result.eigenvalue.real, self.h2_energy, places=6) - # self.assertEqual(len(result.aux_operator_eigenvalues), 2) - # # expectation values - # self.assertAlmostEqual(result.aux_operator_eigenvalues[0][0], 2, places=6) - # self.assertAlmostEqual(result.aux_operator_eigenvalues[1][0], 0, places=6) - # # standard deviations - # self.assertAlmostEqual(result.aux_operator_eigenvalues[0][1], 0.0) - # self.assertAlmostEqual(result.aux_operator_eigenvalues[1][1], 0.0) - - # # Go again with additional None and zero operators - # extra_ops = [*aux_ops, None, 0] - # result = vqe.compute_minimum_eigenvalue(self.h2_op, aux_operators=extra_ops) - # self.assertAlmostEqual(result.eigenvalue.real, self.h2_energy, places=6) - # self.assertEqual(len(result.aux_operator_eigenvalues), 4) - # # expectation values - # self.assertAlmostEqual(result.aux_operator_eigenvalues[0][0], 2, places=6) - # self.assertAlmostEqual(result.aux_operator_eigenvalues[1][0], 0, places=6) - # self.assertEqual(result.aux_operator_eigenvalues[2][0], 0.0) - # self.assertEqual(result.aux_operator_eigenvalues[3][0], 0.0) - # # standard deviations - # self.assertAlmostEqual(result.aux_operator_eigenvalues[0][1], 0.0) - # self.assertAlmostEqual(result.aux_operator_eigenvalues[1][1], 0.0) - # self.assertAlmostEqual(result.aux_operator_eigenvalues[2][1], 0.0) - # self.assertAlmostEqual(result.aux_operator_eigenvalues[3][1], 0.0) - - # def test_aux_operators_dict(self): - # """Test dictionary compatibility of aux_operators""" - # vqe = VQE(self.ry_wavefunction, SLSQP(), Estimator()) - - # # Start with an empty dictionary - # result = vqe.compute_minimum_eigenvalue(self.h2_op, aux_operators={}) - # self.assertAlmostEqual(result.eigenvalue.real, self.h2_energy, places=6) - # self.assertIsNone(result.aux_operator_eigenvalues) - - # # Go again with two auxiliary operators - # aux_op1 = PauliSumOp.from_list([("II", 2.0)]) - # aux_op2 = PauliSumOp.from_list([("II", 0.5), ("ZZ", 0.5), ("YY", 0.5), ("XX", -0.5)]) - # aux_ops = {"aux_op1": aux_op1, "aux_op2": aux_op2} - # result = vqe.compute_minimum_eigenvalue(self.h2_op, aux_operators=aux_ops) - # self.assertAlmostEqual(result.eigenvalue.real, self.h2_energy, places=6) - # self.assertEqual(len(result.aux_operator_eigenvalues), 2) - # # expectation values - # self.assertAlmostEqual(result.aux_operator_eigenvalues["aux_op1"][0], 2, places=6) - # self.assertAlmostEqual(result.aux_operator_eigenvalues["aux_op2"][0], 0, places=6) - # # standard deviations - # self.assertAlmostEqual(result.aux_operator_eigenvalues["aux_op1"][1], 0.0) - # self.assertAlmostEqual(result.aux_operator_eigenvalues["aux_op2"][1], 0.0) - - # # Go again with additional None and zero operators - # extra_ops = {**aux_ops, "None_operator": None, "zero_operator": 0} - # result = vqe.compute_minimum_eigenvalue(self.h2_op, aux_operators=extra_ops) - # self.assertAlmostEqual(result.eigenvalue.real, self.h2_energy, places=6) - # self.assertEqual(len(result.aux_operator_eigenvalues), 3) - # # expectation values - # self.assertAlmostEqual(result.aux_operator_eigenvalues["aux_op1"][0], 2, places=6) - # self.assertAlmostEqual(result.aux_operator_eigenvalues["aux_op2"][0], 0, places=6) - # self.assertEqual(result.aux_operator_eigenvalues["zero_operator"][0], 0.0) - # self.assertTrue("None_operator" not in result.aux_operator_eigenvalues.keys()) - # # standard deviations - # self.assertAlmostEqual(result.aux_operator_eigenvalues["aux_op1"][1], 0.0) - # self.assertAlmostEqual(result.aux_operator_eigenvalues["aux_op2"][1], 0.0) - # self.assertAlmostEqual(result.aux_operator_eigenvalues["zero_operator"][1], 0.0) - - # def test_aux_operator_std_dev_pauli(self): - # """Test non-zero standard deviations of aux operators with PauliExpectation.""" - # wavefunction = self.ry_wavefunction - # vqe = VQE( - # ansatz=wavefunction, - # expectation=PauliExpectation(), - # optimizer=COBYLA(maxiter=0), - # quantum_instance=self.qasm_simulator, - # ) - - # # Go again with two auxiliary operators - # aux_op1 = PauliSumOp.from_list([("II", 2.0)]) - # aux_op2 = PauliSumOp.from_list([("II", 0.5), ("ZZ", 0.5), ("YY", 0.5), ("XX", -0.5)]) - # aux_ops = [aux_op1, aux_op2] - # result = vqe.compute_minimum_eigenvalue(self.h2_op, aux_operators=aux_ops) - # self.assertEqual(len(result.aux_operator_eigenvalues), 2) - # # expectation values - # self.assertAlmostEqual(result.aux_operator_eigenvalues[0][0], 2.0, places=6) - # self.assertAlmostEqual(result.aux_operator_eigenvalues[1][0], 0.6796875, places=6) - # # standard deviations - # self.assertAlmostEqual(result.aux_operator_eigenvalues[0][1], 0.0) - # self.assertAlmostEqual(result.aux_operator_eigenvalues[1][1], 0.02534712219145965, places=6) - - # # Go again with additional None and zero operators - # aux_ops = [*aux_ops, None, 0] - # result = vqe.compute_minimum_eigenvalue(self.h2_op, aux_operators=aux_ops) - # self.assertEqual(len(result.aux_operator_eigenvalues), 4) - # # expectation values - # self.assertAlmostEqual(result.aux_operator_eigenvalues[0][0], 2.0, places=6) - # self.assertAlmostEqual(result.aux_operator_eigenvalues[1][0], 0.57421875, places=6) - # self.assertEqual(result.aux_operator_eigenvalues[2][0], 0.0) - # self.assertEqual(result.aux_operator_eigenvalues[3][0], 0.0) - # # # standard deviations - # self.assertAlmostEqual(result.aux_operator_eigenvalues[0][1], 0.0) - # self.assertAlmostEqual( - # result.aux_operator_eigenvalues[1][1], 0.026562146577166837, places=6 - # ) - # self.assertAlmostEqual(result.aux_operator_eigenvalues[2][1], 0.0) - # self.assertAlmostEqual(result.aux_operator_eigenvalues[3][1], 0.0) - - # @unittest.skipUnless(has_aer(), "qiskit-aer doesn't appear to be installed.") - # def test_aux_operator_std_dev_aer_pauli(self): - # """Test non-zero standard deviations of aux operators with AerPauliExpectation.""" - # wavefunction = self.ry_wavefunction - # vqe = VQE( - # ansatz=wavefunction, - # expectation=AerPauliExpectation(), - # optimizer=COBYLA(maxiter=0), - # quantum_instance=QuantumInstance( - # backend=Aer.get_backend("qasm_simulator"), - # shots=1, - # seed_simulator=algorithm_globals.random_seed, - # seed_transpiler=algorithm_globals.random_seed, - # ), - # ) - - # # Go again with two auxiliary operators - # aux_op1 = PauliSumOp.from_list([("II", 2.0)]) - # aux_op2 = PauliSumOp.from_list([("II", 0.5), ("ZZ", 0.5), ("YY", 0.5), ("XX", -0.5)]) - # aux_ops = [aux_op1, aux_op2] - # result = vqe.compute_minimum_eigenvalue(self.h2_op, aux_operators=aux_ops) - # self.assertEqual(len(result.aux_operator_eigenvalues), 2) - # # expectation values - # self.assertAlmostEqual(result.aux_operator_eigenvalues[0][0], 2.0, places=6) - # self.assertAlmostEqual(result.aux_operator_eigenvalues[1][0], 0.6698863565455391, places=6) - # # standard deviations - # self.assertAlmostEqual(result.aux_operator_eigenvalues[0][1], 0.0) - # self.assertAlmostEqual(result.aux_operator_eigenvalues[1][1], 0.0, places=6) - - # # Go again with additional None and zero operators - # aux_ops = [*aux_ops, None, 0] - # result = vqe.compute_minimum_eigenvalue(self.h2_op, aux_operators=aux_ops) - # self.assertEqual(len(result.aux_operator_eigenvalues), 4) - # # expectation values - # self.assertAlmostEqual(result.aux_operator_eigenvalues[0][0], 2.0, places=6) - # self.assertAlmostEqual(result.aux_operator_eigenvalues[1][0], 0.6036400943063891, places=6) - # self.assertEqual(result.aux_operator_eigenvalues[2][0], 0.0) - # self.assertEqual(result.aux_operator_eigenvalues[3][0], 0.0) - # # standard deviations - # self.assertAlmostEqual(result.aux_operator_eigenvalues[0][1], 0.0) - # self.assertAlmostEqual(result.aux_operator_eigenvalues[1][1], 0.0, places=6) - # self.assertAlmostEqual(result.aux_operator_eigenvalues[2][1], 0.0) - # self.assertAlmostEqual(result.aux_operator_eigenvalues[3][1], 0.0) + def test_aux_operators_list(self): + """Test list-based aux_operators.""" + vqe = VQE(Estimator(), self.ry_wavefunction, SLSQP()) + + # Start with an empty list + result = vqe.compute_minimum_eigenvalue(self.h2_op, aux_operators=[]) + self.assertAlmostEqual(result.eigenvalue.real, self.h2_energy, places=6) + self.assertIsNone(result.aux_operator_eigenvalues) + + # Go again with two auxiliary operators + aux_op1 = PauliSumOp.from_list([("II", 2.0)]) + aux_op2 = PauliSumOp.from_list([("II", 0.5), ("ZZ", 0.5), ("YY", 0.5), ("XX", -0.5)]) + aux_ops = [aux_op1, aux_op2] + result = vqe.compute_minimum_eigenvalue(self.h2_op, aux_operators=aux_ops) + self.assertAlmostEqual(result.eigenvalue.real, self.h2_energy, places=6) + self.assertEqual(len(result.aux_operator_eigenvalues), 2) + # expectation values + self.assertAlmostEqual(result.aux_operator_eigenvalues[0][0], 2, places=6) + self.assertAlmostEqual(result.aux_operator_eigenvalues[1][0], 0, places=6) + # standard deviations + self.assertAlmostEqual(result.aux_operator_eigenvalues[0][1], 0.0) + self.assertAlmostEqual(result.aux_operator_eigenvalues[1][1], 0.0) + + # # Go again with additional None and zero operators + # extra_ops = [*aux_ops, None, 0] + # result = vqe.compute_minimum_eigenvalue(self.h2_op, aux_operators=extra_ops) + # self.assertAlmostEqual(result.eigenvalue.real, self.h2_energy, places=6) + # self.assertEqual(len(result.aux_operator_eigenvalues), 4) + # # # expectation values + # self.assertAlmostEqual(result.aux_operator_eigenvalues[0][0], 2, places=6) + # self.assertAlmostEqual(result.aux_operator_eigenvalues[1][0], 0, places=6) + # self.assertEqual(result.aux_operator_eigenvalues[2], None) + # self.assertEqual(result.aux_operator_eigenvalues[3][0], 0.0) + # # # standard deviations + # self.assertAlmostEqual(result.aux_operator_eigenvalues[0][1], 0.0) + # self.assertAlmostEqual(result.aux_operator_eigenvalues[1][1], 0.0) + # self.assertAlmostEqual(result.aux_operator_eigenvalues[3][1], 0.0) + + def test_aux_operators_dict(self): + """Test dictionary compatibility of aux_operators""" + vqe = VQE(Estimator(), self.ry_wavefunction, SLSQP()) + + # Start with an empty dictionary + result = vqe.compute_minimum_eigenvalue(self.h2_op, aux_operators={}) + self.assertAlmostEqual(result.eigenvalue.real, self.h2_energy, places=6) + self.assertIsNone(result.aux_operator_eigenvalues) + + # Go again with two auxiliary operators + aux_op1 = PauliSumOp.from_list([("II", 2.0)]) + aux_op2 = PauliSumOp.from_list([("II", 0.5), ("ZZ", 0.5), ("YY", 0.5), ("XX", -0.5)]) + aux_ops = {"aux_op1": aux_op1, "aux_op2": aux_op2} + result = vqe.compute_minimum_eigenvalue(self.h2_op, aux_operators=aux_ops) + self.assertAlmostEqual(result.eigenvalue.real, self.h2_energy, places=6) + self.assertEqual(len(result.aux_operator_eigenvalues), 2) + # expectation values + self.assertAlmostEqual(result.aux_operator_eigenvalues["aux_op1"][0], 2, places=6) + self.assertAlmostEqual(result.aux_operator_eigenvalues["aux_op2"][0], 0, places=6) + # standard deviations + self.assertAlmostEqual(result.aux_operator_eigenvalues["aux_op1"][1], 0.0) + self.assertAlmostEqual(result.aux_operator_eigenvalues["aux_op2"][1], 0.0) + + # Go again with additional None and zero operators + # extra_ops = {**aux_ops, "None_operator": None, "zero_operator": 0} + # result = vqe.compute_minimum_eigenvalue(self.h2_op, aux_operators=extra_ops) + # self.assertAlmostEqual(result.eigenvalue.real, self.h2_energy, places=6) + # self.assertEqual(len(result.aux_operator_eigenvalues), 3) + # # expectation values + # self.assertAlmostEqual(result.aux_operator_eigenvalues["aux_op1"][0], 2, places=6) + # self.assertAlmostEqual(result.aux_operator_eigenvalues["aux_op2"][0], 0, places=6) + # self.assertEqual(result.aux_operator_eigenvalues["zero_operator"][0], 0.0) + # self.assertTrue("None_operator" not in result.aux_operator_eigenvalues.keys()) + # # standard deviations + # self.assertAlmostEqual(result.aux_operator_eigenvalues["aux_op1"][1], 0.0) + # self.assertAlmostEqual(result.aux_operator_eigenvalues["aux_op2"][1], 0.0) + # self.assertAlmostEqual(result.aux_operator_eigenvalues["zero_operator"][1], 0.0) if __name__ == "__main__": From 68788ca442137d600307d02900ab15ae72ed642d Mon Sep 17 00:00:00 2001 From: Declan Millar Date: Fri, 9 Sep 2022 14:59:10 +0100 Subject: [PATCH 024/100] no longer support account for Nones in aux_ops --- qiskit/algorithms/minimum_eigensolvers/vqe.py | 112 +++++++----------- .../minimum_eigensolvers/test_vqe.py | 31 +++-- 2 files changed, 61 insertions(+), 82 deletions(-) diff --git a/qiskit/algorithms/minimum_eigensolvers/vqe.py b/qiskit/algorithms/minimum_eigensolvers/vqe.py index d9f8c446bd19..b7658ed6460e 100644 --- a/qiskit/algorithms/minimum_eigensolvers/vqe.py +++ b/qiskit/algorithms/minimum_eigensolvers/vqe.py @@ -127,9 +127,8 @@ def __init__( self.optimizer = optimizer self.gradient = gradient - # TODO change VariationalAlgorithm interface to use a public attribute # this has to go via getters and setters due to the VariationalAlgorithm interface - self._initial_point = initial_point + self.initial_point = initial_point @property def initial_point(self) -> Sequence[float] | None: @@ -146,61 +145,65 @@ def compute_minimum_eigenvalue( operator: BaseOperator | PauliSumOp, aux_operators: ListOrDict[BaseOperator | PauliSumOp] | None = None, ) -> VQEResult: - ansatz = self._check_operator_ansatz(operator) + self._check_operator_ansatz(operator) initial_point = self.initial_point if initial_point is None: - initial_point = np.random.random(ansatz.num_parameters) - elif len(initial_point) != ansatz.num_parameters: + initial_point = np.random.random(self.ansatz.num_parameters) + elif len(initial_point) != self.ansatz.num_parameters: raise ValueError( f"The dimension of the initial point ({len(self.initial_point)}) does not match " - f"the number of parameters in the circuit ({ansatz.num_parameters})." + f"the number of parameters in the circuit ({self.ansatz.num_parameters})." ) start_time = time() - eval_energy = self.get_eval_energy(ansatz, operator) + energy_evaluation = self._get_energy_evaluation(self.ansatz, operator) if self.gradient is not None: - eval_gradient = self.get_eval_gradient(ansatz, operator) + gradient_evaluation = self._get_gradient_evaluation(self.ansatz, operator) else: - eval_gradient = None + gradient_evaluation = None # perform optimization if callable(self.optimizer): - opt_result = self.optimizer(fun=eval_energy, x0=initial_point, jac=eval_gradient) + opt_result = self.optimizer( + fun=energy_evaluation, x0=initial_point, jac=gradient_evaluation + ) else: opt_result = self.optimizer.minimize( - fun=eval_energy, x0=initial_point, jac=eval_gradient + fun=energy_evaluation, x0=initial_point, jac=gradient_evaluation ) eval_time = time() - start_time - optimal_point = opt_result.x - logger.info( - f"Optimization complete in {eval_time} seconds.\nFound opt_params {optimal_point}." - ) - - aux_values = None - if aux_operators: - # not None and not empty list - aux_values = self._eval_aux_ops(ansatz, optimal_point, aux_operators) - result = VQEResult() result.eigenvalue = opt_result.fun + 0j - result.aux_operator_eigenvalues = aux_values result.cost_function_evals = opt_result.nfev - result.optimal_point = optimal_point - result.optimal_parameters = dict(zip(self.ansatz.parameters, optimal_point)) + result.optimal_point = opt_result.x + result.optimal_parameters = dict(zip(self.ansatz.parameters, opt_result.x)) result.optimal_value = opt_result.fun result.optimizer_time = eval_time + + logger.info( + f"Optimization complete in {eval_time} seconds.\nFound opt_params {result.optimal_point}." + ) + + if aux_operators: + # not None and not empty list or dict + bound_ansatz = self.ansatz.bind_parameters(opt_result.x) + # aux_values = eval_observables(self.estimator, bound_ansatz, aux_operators) + # TODO remove once eval_operators have been ported. + aux_values = self._eval_aux_ops(bound_ansatz, aux_operators) + result.aux_operator_eigenvalues = aux_values + return result @classmethod def supports_aux_operators(cls) -> bool: return True - def get_eval_energy( + def _get_energy_evaluation( self, ansatz: QuantumCircuit, operator: BaseOperator | PauliSumOp, @@ -226,31 +229,30 @@ def eval_energy(parameters): return eval_energy - def get_eval_gradient( + def _get_gradient_evaluation( self, ansatz: QuantumCircuit, operator: BaseOperator | PauliSumOp, ) -> tuple[Callable[[np.ndarray], np.ndarray]]: """Returns a function handle to evaluate the gradient at given parameters for the ansatz.""" - def eval_gradient(parameters): + def gradient_evaluation(parameters): # broadcasting not required for the estimator gradients result = self.gradient.run([ansatz], [operator], [parameters]).result() return result.gradients[0] - return eval_gradient + return gradient_evaluation def _check_operator_ansatz(self, operator: BaseOperator | PauliSumOp) -> QuantumCircuit: """Check that the number of qubits of operator and ansatz match and that the ansatz is parameterized. """ - ansatz = self.ansatz.copy() - if operator.num_qubits != ansatz.num_qubits: + if operator.num_qubits != self.ansatz.num_qubits: try: logger.info( f"Trying to resize ansatz to match operator on {operator.num_qubits} qubits." ) - ansatz.num_qubits = operator.num_qubits + self.ansatz.num_qubits = operator.num_qubits except AttributeError as error: raise AlgorithmError( "The number of qubits of the ansatz does not match the " @@ -258,52 +260,30 @@ def _check_operator_ansatz(self, operator: BaseOperator | PauliSumOp) -> Quantum "number of qubits using `num_qubits`." ) from error - if ansatz.num_parameters == 0: + if self.ansatz.num_parameters == 0: raise AlgorithmError("The ansatz must be parameterized, but has no free parameters.") - return ansatz - def _eval_aux_ops( self, ansatz: QuantumCircuit, - parameters: np.ndarray, aux_operators: ListOrDict[BaseOperator | PauliSumOp], ) -> ListOrDict[tuple(complex, complex)]: """Compute auxiliary operator eigenvalues.""" - if isinstance(aux_operators, list): - non_nones = [i for i, x in enumerate(aux_operators) if x is not None] - filtered_ops = [x for x in aux_operators if x is not None] + if isinstance(aux_operators, dict): + aux_ops = list(aux_operators.values()) else: - # filtered_ops = {k: v for k, v in aux_operators.items() if v is not None} - filtered_ops = list(aux_operators.values()) - - # TODO this is going to be replaced by eval_observables (see PR #8683) - # bound_ansatz = ansatz.bind_parameters(parameters) - # aux_values = eval_observables(self.estimator, bound_ansatz, filtered_ops) - aux_values = None - if filtered_ops: - num_aux_ops = len(filtered_ops) - aux_job = self.estimator.run( - [ansatz] * num_aux_ops, filtered_ops, [parameters] * num_aux_ops - ) - aux_eigs = aux_job.result().values - aux_eigs = list(zip(aux_eigs, [0] * len(aux_eigs))) - if isinstance(aux_operators, dict): - aux_values = dict(zip(aux_operators.keys(), aux_eigs)) - else: - aux_values = [None] * len(aux_operators) - for i, x in enumerate(non_nones): - aux_values[x] = aux_eigs[i] - - if isinstance(aux_values, list): - aux_eigs = [None] * len(aux_operators) - for i, x in enumerate(non_nones): - aux_eigs[x] = aux_values[i] - else: - aux_eigs = aux_values + aux_ops = aux_operators + + num_aux_ops = len(aux_ops) + aux_job = self.estimator.run([ansatz] * num_aux_ops, aux_ops) + aux_values = aux_job.result().values + aux_values = list(zip(aux_values, [0] * len(aux_values))) + + if isinstance(aux_operators, dict): + aux_values = dict(zip(aux_operators.keys(), aux_values)) - return aux_eigs + return aux_values class VQEResult(VariationalResult, MinimumEigensolverResult): diff --git a/test/python/algorithms/minimum_eigensolvers/test_vqe.py b/test/python/algorithms/minimum_eigensolvers/test_vqe.py index 6fd974b35d0e..dd4038353f66 100644 --- a/test/python/algorithms/minimum_eigensolvers/test_vqe.py +++ b/test/python/algorithms/minimum_eigensolvers/test_vqe.py @@ -228,7 +228,7 @@ def test_with_two_qubit_reduction(self): def test_reuse(self): """Test re-using a VQE algorithm instance.""" ansatz = TwoLocal(rotation_blocks=["ry", "rz"], entanglement_blocks="cz") - vqe = VQE(Estimator(), ansatz, SLSQP()) + vqe = VQE(Estimator(), ansatz, SLSQP(maxiter=300)) with self.subTest(msg="assert VQE works once all info is available"): result = vqe.compute_minimum_eigenvalue(operator=self.h2_op) self.assertAlmostEqual(result.eigenvalue.real, self.h2_energy, places=5) @@ -326,10 +326,9 @@ def test_optimizer_callable(self): result = vqe.compute_minimum_eigenvalue(SparsePauliOp("Z")) self.assertTrue(np.all(result.optimal_point == np.zeros(ansatz.num_parameters))) - # TODO waiting for eval_operators to be ported. def test_aux_operators_list(self): """Test list-based aux_operators.""" - vqe = VQE(Estimator(), self.ry_wavefunction, SLSQP()) + vqe = VQE(Estimator(), self.ry_wavefunction, SLSQP(maxiter=300)) # Start with an empty list result = vqe.compute_minimum_eigenvalue(self.h2_op, aux_operators=[]) @@ -350,20 +349,20 @@ def test_aux_operators_list(self): self.assertAlmostEqual(result.aux_operator_eigenvalues[0][1], 0.0) self.assertAlmostEqual(result.aux_operator_eigenvalues[1][1], 0.0) - # # Go again with additional None and zero operators - # extra_ops = [*aux_ops, None, 0] + # Go again with additional zero operator + # TODO waiting for eval_operators to be ported. + # extra_ops = [*aux_ops, 0] # result = vqe.compute_minimum_eigenvalue(self.h2_op, aux_operators=extra_ops) # self.assertAlmostEqual(result.eigenvalue.real, self.h2_energy, places=6) - # self.assertEqual(len(result.aux_operator_eigenvalues), 4) + # self.assertEqual(len(result.aux_operator_eigenvalues), 3) # # # expectation values # self.assertAlmostEqual(result.aux_operator_eigenvalues[0][0], 2, places=6) # self.assertAlmostEqual(result.aux_operator_eigenvalues[1][0], 0, places=6) - # self.assertEqual(result.aux_operator_eigenvalues[2], None) - # self.assertEqual(result.aux_operator_eigenvalues[3][0], 0.0) + # self.assertEqual(result.aux_operator_eigenvalues[2][0], 0.0) # # # standard deviations # self.assertAlmostEqual(result.aux_operator_eigenvalues[0][1], 0.0) # self.assertAlmostEqual(result.aux_operator_eigenvalues[1][1], 0.0) - # self.assertAlmostEqual(result.aux_operator_eigenvalues[3][1], 0.0) + # self.assertAlmostEqual(result.aux_operator_eigenvalues[2][1], 0.0) def test_aux_operators_dict(self): """Test dictionary compatibility of aux_operators""" @@ -382,22 +381,22 @@ def test_aux_operators_dict(self): self.assertAlmostEqual(result.eigenvalue.real, self.h2_energy, places=6) self.assertEqual(len(result.aux_operator_eigenvalues), 2) # expectation values - self.assertAlmostEqual(result.aux_operator_eigenvalues["aux_op1"][0], 2, places=6) - self.assertAlmostEqual(result.aux_operator_eigenvalues["aux_op2"][0], 0, places=6) + self.assertAlmostEqual(result.aux_operator_eigenvalues["aux_op1"][0], 2, places=5) + self.assertAlmostEqual(result.aux_operator_eigenvalues["aux_op2"][0], 0, places=5) # standard deviations self.assertAlmostEqual(result.aux_operator_eigenvalues["aux_op1"][1], 0.0) self.assertAlmostEqual(result.aux_operator_eigenvalues["aux_op2"][1], 0.0) - # Go again with additional None and zero operators - # extra_ops = {**aux_ops, "None_operator": None, "zero_operator": 0} + # TODO waiting for eval_operators to be ported. + # Go again with additional zero operator + # extra_ops = {**aux_ops, "zero_operator": 0} # result = vqe.compute_minimum_eigenvalue(self.h2_op, aux_operators=extra_ops) # self.assertAlmostEqual(result.eigenvalue.real, self.h2_energy, places=6) # self.assertEqual(len(result.aux_operator_eigenvalues), 3) # # expectation values - # self.assertAlmostEqual(result.aux_operator_eigenvalues["aux_op1"][0], 2, places=6) - # self.assertAlmostEqual(result.aux_operator_eigenvalues["aux_op2"][0], 0, places=6) + # self.assertAlmostEqual(result.aux_operator_eigenvalues["aux_op1"][0], 2, places=5) + # self.assertAlmostEqual(result.aux_operator_eigenvalues["aux_op2"][0], 0, places=5) # self.assertEqual(result.aux_operator_eigenvalues["zero_operator"][0], 0.0) - # self.assertTrue("None_operator" not in result.aux_operator_eigenvalues.keys()) # # standard deviations # self.assertAlmostEqual(result.aux_operator_eigenvalues["aux_op1"][1], 0.0) # self.assertAlmostEqual(result.aux_operator_eigenvalues["aux_op2"][1], 0.0) From 7ab77e42fa1f4c4d8f6c2f1869ea1b96e9fd48b4 Mon Sep 17 00:00:00 2001 From: Declan Millar Date: Fri, 9 Sep 2022 15:17:42 +0100 Subject: [PATCH 025/100] correct typing for MinimumEigensolver --- .../minimum_eigensolver.py | 21 +++++++++++-------- .../minimum_eigensolvers/test_vqe.py | 2 +- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/qiskit/algorithms/minimum_eigensolvers/minimum_eigensolver.py b/qiskit/algorithms/minimum_eigensolvers/minimum_eigensolver.py index 9fbaa21e084f..5295b289a8bc 100644 --- a/qiskit/algorithms/minimum_eigensolvers/minimum_eigensolver.py +++ b/qiskit/algorithms/minimum_eigensolvers/minimum_eigensolver.py @@ -14,7 +14,9 @@ from abc import ABC, abstractmethod -from qiskit.opflow import OperatorBase +from qiskit.opflow import PauliSumOp +from qiskit.quantum_info.operators.base_operator import BaseOperator + from ..algorithm_result import AlgorithmResult from ..list_or_dict import ListOrDict @@ -28,13 +30,14 @@ class MinimumEigensolver(ABC): @abstractmethod def compute_minimum_eigenvalue( - self, operator: OperatorBase, aux_operators: ListOrDict[OperatorBase] | None = None + self, + operator: BaseOperator | PauliSumOp, + aux_operators: ListOrDict[BaseOperator | PauliSumOp] | None = None, ) -> "MinimumEigensolverResult": """ - Computes minimum eigenvalue. Operator and aux_operators can be supplied here and - if not None will override any already set into algorithm so it can be reused with - different operators. While an operator is required by algorithms, aux_operators - are optional. + Computes minimum eigenvalue. Operator and aux_operators can be supplied here and if not None + will override any already set into algorithm so it can be reused with different operators. + While an operator is required by algorithms, aux_operators are optional. Args: operator: Qubit operator of the observable. @@ -71,12 +74,12 @@ def __init__(self) -> None: @property def eigenvalue(self) -> complex | None: - """returns eigen value""" + """Return the eigenvalue.""" return self._eigenvalue @eigenvalue.setter def eigenvalue(self, value: complex) -> None: - """set eigen value""" + """Set the eigenvalue.""" self._eigenvalue = value @property @@ -89,5 +92,5 @@ def aux_operator_eigenvalues(self) -> ListOrDict[tuple[complex, complex]] | None @aux_operator_eigenvalues.setter def aux_operator_eigenvalues(self, value: ListOrDict[tuple[complex, complex]]) -> None: - """set aux operator eigen values""" + """set aux operator eigenvalues""" self._aux_operator_eigenvalues = value diff --git a/test/python/algorithms/minimum_eigensolvers/test_vqe.py b/test/python/algorithms/minimum_eigensolvers/test_vqe.py index dd4038353f66..453b723296df 100644 --- a/test/python/algorithms/minimum_eigensolvers/test_vqe.py +++ b/test/python/algorithms/minimum_eigensolvers/test_vqe.py @@ -366,7 +366,7 @@ def test_aux_operators_list(self): def test_aux_operators_dict(self): """Test dictionary compatibility of aux_operators""" - vqe = VQE(Estimator(), self.ry_wavefunction, SLSQP()) + vqe = VQE(Estimator(), self.ry_wavefunction, SLSQP(maxiter=300)) # Start with an empty dictionary result = vqe.compute_minimum_eigenvalue(self.h2_op, aux_operators={}) From 632b3aa7b742870dd09c65be77739e902bb5aa05 Mon Sep 17 00:00:00 2001 From: Declan Millar Date: Mon, 12 Sep 2022 13:53:45 +0100 Subject: [PATCH 026/100] Compute default initial point using ansatz bounds. --- qiskit/algorithms/minimum_eigensolvers/vqe.py | 44 ++++++++++++++----- 1 file changed, 33 insertions(+), 11 deletions(-) diff --git a/qiskit/algorithms/minimum_eigensolvers/vqe.py b/qiskit/algorithms/minimum_eigensolvers/vqe.py index b7658ed6460e..cdd4c7d23404 100644 --- a/qiskit/algorithms/minimum_eigensolvers/vqe.py +++ b/qiskit/algorithms/minimum_eigensolvers/vqe.py @@ -25,6 +25,7 @@ from qiskit.opflow import PauliSumOp from qiskit.primitives import BaseEstimator from qiskit.quantum_info.operators.base_operator import BaseOperator +from qiskit.utils import algorithm_globals from ..exceptions import AlgorithmError from ..list_or_dict import ListOrDict @@ -32,7 +33,7 @@ from ..variational_algorithm import VariationalAlgorithm, VariationalResult from .minimum_eigensolver import MinimumEigensolver, MinimumEigensolverResult -from qiskit.algorithms.observables_evaluator import eval_observables +# from qiskit.algorithms.observables_evaluator import eval_observables logger = logging.getLogger(__name__) @@ -147,14 +148,7 @@ def compute_minimum_eigenvalue( ) -> VQEResult: self._check_operator_ansatz(operator) - initial_point = self.initial_point - if initial_point is None: - initial_point = np.random.random(self.ansatz.num_parameters) - elif len(initial_point) != self.ansatz.num_parameters: - raise ValueError( - f"The dimension of the initial point ({len(self.initial_point)}) does not match " - f"the number of parameters in the circuit ({self.ansatz.num_parameters})." - ) + initial_point = _validate_initial_point(self.initial_point, self.ansatz) start_time = time() @@ -218,7 +212,7 @@ def _get_energy_evaluation( """ num_parameters = ansatz.num_parameters - def eval_energy(parameters): + def energy_evaluation(parameters): # handle broadcasting: ensure parameters is of shape [array, array, ...] parameters = np.reshape(parameters, (-1, num_parameters)).tolist() batchsize = len(parameters) @@ -227,7 +221,7 @@ def eval_energy(parameters): values = job.result().values return values[0] if len(values) == 1 else values - return eval_energy + return energy_evaluation def _get_gradient_evaluation( self, @@ -286,6 +280,34 @@ def _eval_aux_ops( return aux_values +def _validate_initial_point(point, ansatz): + expected_size = ansatz.num_parameters + + if point is None: + # get bounds if ansatz has them set, otherwise use [-2pi, 2pi] for each parameter + bounds = getattr(ansatz, "parameter_bounds", None) + if bounds is None: + bounds = [(-2 * np.pi, 2 * np.pi)] * expected_size + + # replace all Nones by [-2pi, 2pi] + lower_bounds = [] + upper_bounds = [] + for lower, upper in bounds: + lower_bounds.append(lower if lower is not None else -2 * np.pi) + upper_bounds.append(upper if upper is not None else 2 * np.pi) + + # sample from within bounds + point = algorithm_globals.random.uniform(lower_bounds, upper_bounds) + + elif len(point) != expected_size: + raise ValueError( + f"The dimension of the initial point ({len(point)}) does not match the " + f"number of parameters in the circuit ({expected_size})." + ) + + return point + + class VQEResult(VariationalResult, MinimumEigensolverResult): """Variational quantum eigensolver result.""" From 6aa537379d6e568110e8689922ddd38b218aeb48 Mon Sep 17 00:00:00 2001 From: Declan Millar Date: Mon, 12 Sep 2022 14:20:18 +0100 Subject: [PATCH 027/100] Add NumPyMinimumEigensolverResult --- .../numpy_minimum_eigensolver.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/qiskit/algorithms/minimum_eigensolvers/numpy_minimum_eigensolver.py b/qiskit/algorithms/minimum_eigensolvers/numpy_minimum_eigensolver.py index 33702568941e..e85f12e69dc2 100644 --- a/qiskit/algorithms/minimum_eigensolvers/numpy_minimum_eigensolver.py +++ b/qiskit/algorithms/minimum_eigensolvers/numpy_minimum_eigensolver.py @@ -85,3 +85,16 @@ def compute_minimum_eigenvalue( logger.debug(f"MinimumEigensolver:\n{self._ret}") return self._ret + +class NumPyMinimumEigensolverResult(MinimumEigensolverResult): + """NumPy minimum eigensolver result.""" + + @property + def eigenstate(self) -> Optional[np.ndarray]: + """Return eigenstate.""" + return self._eigenstate + + @eigenstate.setter + def eigenstate(self, value: np.ndarray) -> None: + """Set eigenstate.""" + self._eigenstate = value \ No newline at end of file From a00246751d0cc3dfaf9c174de772b79c3b3541fa Mon Sep 17 00:00:00 2001 From: Declan Millar Date: Mon, 12 Sep 2022 14:50:08 +0100 Subject: [PATCH 028/100] Fix type hints --- .../minimum_eigensolver.py | 2 ++ .../numpy_minimum_eigensolver.py | 19 +++++++++++-------- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/qiskit/algorithms/minimum_eigensolvers/minimum_eigensolver.py b/qiskit/algorithms/minimum_eigensolvers/minimum_eigensolver.py index 5295b289a8bc..03b917aa8578 100644 --- a/qiskit/algorithms/minimum_eigensolvers/minimum_eigensolver.py +++ b/qiskit/algorithms/minimum_eigensolvers/minimum_eigensolver.py @@ -12,6 +12,8 @@ """The minimum eigensolver interface and result.""" +from __future__ import annotations + from abc import ABC, abstractmethod from qiskit.opflow import PauliSumOp diff --git a/qiskit/algorithms/minimum_eigensolvers/numpy_minimum_eigensolver.py b/qiskit/algorithms/minimum_eigensolvers/numpy_minimum_eigensolver.py index e85f12e69dc2..69e4f18f71fa 100644 --- a/qiskit/algorithms/minimum_eigensolvers/numpy_minimum_eigensolver.py +++ b/qiskit/algorithms/minimum_eigensolvers/numpy_minimum_eigensolver.py @@ -12,7 +12,9 @@ """The NumPy minimum eigensolver algorithm.""" -from typing import List, Optional, Union, Callable +from __future__ import annotations + +from typing import Callable import logging import numpy as np @@ -34,7 +36,7 @@ class NumPyMinimumEigensolver(MinimumEigensolver): def __init__( self, filter_criterion: Callable[ - [Union[List, np.ndarray], float, Optional[ListOrDict[float]]], bool + [list | np.ndarray, float, ListOrDict[float] | None], bool ] = None, ) -> None: """ @@ -52,15 +54,15 @@ def __init__( @property def filter_criterion( self, - ) -> Optional[Callable[[Union[List, np.ndarray], float, Optional[ListOrDict[float]]], bool]]: + ) -> Callable[[list | np.ndarray, float, ListOrDict[float] | None], bool] | None: """returns the filter criterion if set""" return self._ces.filter_criterion @filter_criterion.setter def filter_criterion( self, - filter_criterion: Optional[ - Callable[[Union[List, np.ndarray], float, Optional[ListOrDict[float]]], bool] + filter_criterion: Callable[ + [list | np.ndarray, float, ListOrDict[float] | None], bool ], ) -> None: """set the filter criterion""" @@ -71,7 +73,7 @@ def supports_aux_operators(cls) -> bool: return NumPyEigensolver.supports_aux_operators() def compute_minimum_eigenvalue( - self, operator: OperatorBase, aux_operators: Optional[ListOrDict[OperatorBase]] = None + self, operator: OperatorBase, aux_operators: ListOrDict[OperatorBase] | None = None ) -> MinimumEigensolverResult: super().compute_minimum_eigenvalue(operator, aux_operators) result_ces = self._ces.compute_eigenvalues(operator, aux_operators) @@ -86,15 +88,16 @@ def compute_minimum_eigenvalue( return self._ret + class NumPyMinimumEigensolverResult(MinimumEigensolverResult): """NumPy minimum eigensolver result.""" @property - def eigenstate(self) -> Optional[np.ndarray]: + def eigenstate(self) -> np.ndarray | None: """Return eigenstate.""" return self._eigenstate @eigenstate.setter def eigenstate(self, value: np.ndarray) -> None: """Set eigenstate.""" - self._eigenstate = value \ No newline at end of file + self._eigenstate = value From c83c7b9212df635ba8774b68f47581e26ff8f35e Mon Sep 17 00:00:00 2001 From: Declan Millar Date: Mon, 12 Sep 2022 14:53:19 +0100 Subject: [PATCH 029/100] Fix type hints --- .../minimum_eigensolvers/numpy_minimum_eigensolver.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/qiskit/algorithms/minimum_eigensolvers/numpy_minimum_eigensolver.py b/qiskit/algorithms/minimum_eigensolvers/numpy_minimum_eigensolver.py index 69e4f18f71fa..3ec89117db23 100644 --- a/qiskit/algorithms/minimum_eigensolvers/numpy_minimum_eigensolver.py +++ b/qiskit/algorithms/minimum_eigensolvers/numpy_minimum_eigensolver.py @@ -18,7 +18,8 @@ import logging import numpy as np -from qiskit.opflow import OperatorBase +from qiskit.opflow import PauliSumOp +from qiskit.quantum_info.operators.base_operator import BaseOperator # TODO this path will need updating from ..eigen_solvers.numpy_eigen_solver import NumPyEigensolver @@ -73,7 +74,7 @@ def supports_aux_operators(cls) -> bool: return NumPyEigensolver.supports_aux_operators() def compute_minimum_eigenvalue( - self, operator: OperatorBase, aux_operators: ListOrDict[OperatorBase] | None = None + self, operator: BaseOperator | PauliSumOp, aux_operators: ListOrDict[BaseOperator | PauliSumOp] | None = None ) -> MinimumEigensolverResult: super().compute_minimum_eigenvalue(operator, aux_operators) result_ces = self._ces.compute_eigenvalues(operator, aux_operators) From 98648f06be051512331e1c4b985917d8bfa8f26a Mon Sep 17 00:00:00 2001 From: Declan Millar Date: Mon, 12 Sep 2022 15:07:22 +0100 Subject: [PATCH 030/100] Formatting --- .../minimum_eigensolvers/numpy_minimum_eigensolver.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/qiskit/algorithms/minimum_eigensolvers/numpy_minimum_eigensolver.py b/qiskit/algorithms/minimum_eigensolvers/numpy_minimum_eigensolver.py index 3ec89117db23..6bb45ae391eb 100644 --- a/qiskit/algorithms/minimum_eigensolvers/numpy_minimum_eigensolver.py +++ b/qiskit/algorithms/minimum_eigensolvers/numpy_minimum_eigensolver.py @@ -62,9 +62,7 @@ def filter_criterion( @filter_criterion.setter def filter_criterion( self, - filter_criterion: Callable[ - [list | np.ndarray, float, ListOrDict[float] | None], bool - ], + filter_criterion: Callable[[list | np.ndarray, float, ListOrDict[float] | None], bool], ) -> None: """set the filter criterion""" self._ces.filter_criterion = filter_criterion @@ -74,7 +72,9 @@ def supports_aux_operators(cls) -> bool: return NumPyEigensolver.supports_aux_operators() def compute_minimum_eigenvalue( - self, operator: BaseOperator | PauliSumOp, aux_operators: ListOrDict[BaseOperator | PauliSumOp] | None = None + self, + operator: BaseOperator | PauliSumOp, + aux_operators: ListOrDict[BaseOperator | PauliSumOp] | None = None, ) -> MinimumEigensolverResult: super().compute_minimum_eigenvalue(operator, aux_operators) result_ces = self._ces.compute_eigenvalues(operator, aux_operators) From 2ed3ff327e8c0d7494376f8ccf52a8118b175122 Mon Sep 17 00:00:00 2001 From: Declan Millar Date: Mon, 12 Sep 2022 15:11:59 +0100 Subject: [PATCH 031/100] Do not store NumPyMES result inside the algo. --- .../numpy_minimum_eigensolver.py | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/qiskit/algorithms/minimum_eigensolvers/numpy_minimum_eigensolver.py b/qiskit/algorithms/minimum_eigensolvers/numpy_minimum_eigensolver.py index 6bb45ae391eb..da0d10423542 100644 --- a/qiskit/algorithms/minimum_eigensolvers/numpy_minimum_eigensolver.py +++ b/qiskit/algorithms/minimum_eigensolvers/numpy_minimum_eigensolver.py @@ -50,7 +50,6 @@ def __init__( feasible element, the result can even be empty. """ self._ces = NumPyEigensolver(filter_criterion=filter_criterion) - self._ret = MinimumEigensolverResult() @property def filter_criterion( @@ -75,19 +74,19 @@ def compute_minimum_eigenvalue( self, operator: BaseOperator | PauliSumOp, aux_operators: ListOrDict[BaseOperator | PauliSumOp] | None = None, - ) -> MinimumEigensolverResult: + ) -> NumPyMinimumEigensolverResult: super().compute_minimum_eigenvalue(operator, aux_operators) result_ces = self._ces.compute_eigenvalues(operator, aux_operators) - self._ret = MinimumEigensolverResult() + result = NumPyMinimumEigensolverResult() if result_ces.eigenvalues is not None and len(result_ces.eigenvalues) > 0: - self._ret.eigenvalue = result_ces.eigenvalues[0] - self._ret.eigenstate = result_ces.eigenstates[0] + result.eigenvalue = result_ces.eigenvalues[0] + result.eigenstate = result_ces.eigenstates[0] if result_ces.aux_operator_eigenvalues: - self._ret.aux_operator_eigenvalues = result_ces.aux_operator_eigenvalues[0] + result.aux_operator_eigenvalues = result_ces.aux_operator_eigenvalues[0] - logger.debug(f"MinimumEigensolver:\n{self._ret}") + logger.debug(f"MinimumEigensolver:\n{result}") - return self._ret + return result class NumPyMinimumEigensolverResult(MinimumEigensolverResult): From aced280783ca2f2c53ecdf3439488cd34f18e20c Mon Sep 17 00:00:00 2001 From: Declan Millar Date: Mon, 12 Sep 2022 16:03:11 +0100 Subject: [PATCH 032/100] Provide default values for ansatz and estimator --- qiskit/algorithms/minimum_eigensolvers/vqe.py | 52 ++++++++++++------- .../minimum_eigensolvers/test_vqe.py | 6 +++ 2 files changed, 38 insertions(+), 20 deletions(-) diff --git a/qiskit/algorithms/minimum_eigensolvers/vqe.py b/qiskit/algorithms/minimum_eigensolvers/vqe.py index cdd4c7d23404..824c2376acde 100644 --- a/qiskit/algorithms/minimum_eigensolvers/vqe.py +++ b/qiskit/algorithms/minimum_eigensolvers/vqe.py @@ -22,6 +22,7 @@ from qiskit.algorithms.gradients import BaseEstimatorGradient from qiskit.circuit import QuantumCircuit +from qiskit.circuit.library import RealAmplitudes from qiskit.opflow import PauliSumOp from qiskit.primitives import BaseEstimator from qiskit.quantum_info.operators.base_operator import BaseOperator @@ -29,7 +30,7 @@ from ..exceptions import AlgorithmError from ..list_or_dict import ListOrDict -from ..optimizers import Optimizer, Minimizer +from ..optimizers import Optimizer, Minimizer, SLSQP from ..variational_algorithm import VariationalAlgorithm, VariationalResult from .minimum_eigensolver import MinimumEigensolver, MinimumEigensolverResult @@ -104,18 +105,19 @@ def my_minimizer(fun, x0, jac=None, bounds=None) -> OptimizerResult: def __init__( self, estimator: BaseEstimator, - ansatz: QuantumCircuit, - optimizer: Optimizer | Minimizer, - *, + ansatz: QuantumCircuit | None = None, + optimizer: Optimizer | Minimizer | None = None, gradient: BaseEstimatorGradient | None = None, initial_point: Sequence[float] | None = None, ) -> None: """ Args: estimator: The estimator primitive to compute the expectation value of the circuits. - ansatz: The parameterized circuit used as ansatz for the wave function. - optimizer: The 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. + ansatz: A parameterized circuit, preparing the ansatz for the wave function. If not + provided, this defaults to a :class:`.RealAmplitudes` circuit. + optimizer: A classical optimizer to find the minimum energy. This can either be a + Qiskit :class:`.Optimizer` or a callable implementing the :class:`.Minimizer` protocol. + Defaults to :class:`.SLSQP`. gradient: An optional gradient function or operator for the optimizer. 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 @@ -146,26 +148,34 @@ def compute_minimum_eigenvalue( operator: BaseOperator | PauliSumOp, aux_operators: ListOrDict[BaseOperator | PauliSumOp] | None = None, ) -> VQEResult: - self._check_operator_ansatz(operator) + # set defaults + if self.ansatz is None: + ansatz = RealAmplitudes(num_qubits=operator.num_qubits) + else: + ansatz = self.ansatz + + ansatz = self._check_operator_ansatz(operator, ansatz) - initial_point = _validate_initial_point(self.initial_point, self.ansatz) + optimizer = SLSQP() if self.optimizer is None else self.optimizer + + initial_point = _validate_initial_point(self.initial_point, ansatz) start_time = time() - energy_evaluation = self._get_energy_evaluation(self.ansatz, operator) + energy_evaluation = self._get_energy_evaluation(ansatz, operator) if self.gradient is not None: - gradient_evaluation = self._get_gradient_evaluation(self.ansatz, operator) + gradient_evaluation = self._get_gradient_evaluation(ansatz, operator) else: gradient_evaluation = None # perform optimization - if callable(self.optimizer): - opt_result = self.optimizer( + if callable(optimizer): + opt_result = optimizer( fun=energy_evaluation, x0=initial_point, jac=gradient_evaluation ) else: - opt_result = self.optimizer.minimize( + opt_result = optimizer.minimize( fun=energy_evaluation, x0=initial_point, jac=gradient_evaluation ) @@ -175,7 +185,7 @@ def compute_minimum_eigenvalue( result.eigenvalue = opt_result.fun + 0j result.cost_function_evals = opt_result.nfev result.optimal_point = opt_result.x - result.optimal_parameters = dict(zip(self.ansatz.parameters, opt_result.x)) + result.optimal_parameters = dict(zip(ansatz.parameters, opt_result.x)) result.optimal_value = opt_result.fun result.optimizer_time = eval_time @@ -185,7 +195,7 @@ def compute_minimum_eigenvalue( if aux_operators: # not None and not empty list or dict - bound_ansatz = self.ansatz.bind_parameters(opt_result.x) + bound_ansatz = ansatz.bind_parameters(opt_result.x) # aux_values = eval_observables(self.estimator, bound_ansatz, aux_operators) # TODO remove once eval_operators have been ported. aux_values = self._eval_aux_ops(bound_ansatz, aux_operators) @@ -237,16 +247,16 @@ def gradient_evaluation(parameters): return gradient_evaluation - def _check_operator_ansatz(self, operator: BaseOperator | PauliSumOp) -> QuantumCircuit: + def _check_operator_ansatz(self, operator: BaseOperator | PauliSumOp, ansatz: QuantumCircuit) -> QuantumCircuit: """Check that the number of qubits of operator and ansatz match and that the ansatz is parameterized. """ - if operator.num_qubits != self.ansatz.num_qubits: + if operator.num_qubits != ansatz.num_qubits: try: logger.info( f"Trying to resize ansatz to match operator on {operator.num_qubits} qubits." ) - self.ansatz.num_qubits = operator.num_qubits + ansatz.num_qubits = operator.num_qubits except AttributeError as error: raise AlgorithmError( "The number of qubits of the ansatz does not match the " @@ -254,9 +264,11 @@ def _check_operator_ansatz(self, operator: BaseOperator | PauliSumOp) -> Quantum "number of qubits using `num_qubits`." ) from error - if self.ansatz.num_parameters == 0: + if ansatz.num_parameters == 0: raise AlgorithmError("The ansatz must be parameterized, but has no free parameters.") + return ansatz + def _eval_aux_ops( self, ansatz: QuantumCircuit, diff --git a/test/python/algorithms/minimum_eigensolvers/test_vqe.py b/test/python/algorithms/minimum_eigensolvers/test_vqe.py index 453b723296df..e93a3db82cf0 100644 --- a/test/python/algorithms/minimum_eigensolvers/test_vqe.py +++ b/test/python/algorithms/minimum_eigensolvers/test_vqe.py @@ -101,6 +101,12 @@ def test_basic_aer_statevector(self, estimator): with self.subTest(msg="assert optimizer_time is set"): self.assertIsNotNone(result.optimizer_time) + def test_default_values(self): + """Test all default values are set as expected.""" + vqe = VQE(Estimator()) + result = vqe.compute_minimum_eigenvalue(operator=self.h2_op) + self.assertAlmostEqual(result.eigenvalue, self.h2_energy, places=5) + def test_invalid_initial_point(self): """Test the proper error is raised when the initial point has the wrong size.""" ansatz = self.ryrz_wavefunction From 94596036f405f0ee6d74e663bd7d266a1c9a1e21 Mon Sep 17 00:00:00 2001 From: Declan Millar Date: Mon, 12 Sep 2022 16:31:46 +0100 Subject: [PATCH 033/100] Formatting --- qiskit/algorithms/minimum_eigensolvers/vqe.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/qiskit/algorithms/minimum_eigensolvers/vqe.py b/qiskit/algorithms/minimum_eigensolvers/vqe.py index 824c2376acde..47135b596d2d 100644 --- a/qiskit/algorithms/minimum_eigensolvers/vqe.py +++ b/qiskit/algorithms/minimum_eigensolvers/vqe.py @@ -171,9 +171,7 @@ def compute_minimum_eigenvalue( # perform optimization if callable(optimizer): - opt_result = optimizer( - fun=energy_evaluation, x0=initial_point, jac=gradient_evaluation - ) + opt_result = optimizer(fun=energy_evaluation, x0=initial_point, jac=gradient_evaluation) else: opt_result = optimizer.minimize( fun=energy_evaluation, x0=initial_point, jac=gradient_evaluation @@ -247,7 +245,9 @@ def gradient_evaluation(parameters): return gradient_evaluation - def _check_operator_ansatz(self, operator: BaseOperator | PauliSumOp, ansatz: QuantumCircuit) -> QuantumCircuit: + def _check_operator_ansatz( + self, operator: BaseOperator | PauliSumOp, ansatz: QuantumCircuit + ) -> QuantumCircuit: """Check that the number of qubits of operator and ansatz match and that the ansatz is parameterized. """ From 3623ad743bd3e2b17a50c6aa99cda3b5f9bcc96b Mon Sep 17 00:00:00 2001 From: Declan Millar Date: Mon, 12 Sep 2022 16:40:53 +0100 Subject: [PATCH 034/100] fix old and new batching --- qiskit/algorithms/optimizers/qnspsa.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/qiskit/algorithms/optimizers/qnspsa.py b/qiskit/algorithms/optimizers/qnspsa.py index 15a4e602803c..42296f9ddf7a 100644 --- a/qiskit/algorithms/optimizers/qnspsa.py +++ b/qiskit/algorithms/optimizers/qnspsa.py @@ -270,11 +270,13 @@ def fidelity(values_x, values_y): sampler = CircuitSampler(backend) def fidelity(values_x, values_y=None): - if values_y is not None: # no batches + # no batches + if isinstance(values_x, np.ndarray) and isinstance(values_y, np.ndarray): value_dict = dict( zip(params_x[:] + params_y[:], values_x.tolist() + values_y.tolist()) ) - else: + # legacy batching -- remove once QNSPSA.get_fidelity is only supported with sampler + elif values_y is None: value_dict = {p: [] for p in params_x[:] + params_y[:]} for values_xy in values_x: for value_x, param_x in zip(values_xy[0, :], params_x): @@ -282,6 +284,14 @@ def fidelity(values_x, values_y=None): for value_y, param_y in zip(values_xy[1, :], params_y): value_dict[param_y].append(value_y) + else: + value_dict = {p: [] for p in params_x[:] + params_y[:]} + for values_i_x, values_i_y in zip(values_x, values_y): + for value_x, param_x in zip(values_i_x, params_x): + value_dict[param_x].append(value_x) + + for value_y, param_y in zip(values_i_y, params_y): + value_dict[param_y].append(value_y) return np.abs(sampler.convert(expression, params=value_dict).eval()) ** 2 From 93213dc015bfd335390c0d4ff1be540c26c5ecb3 Mon Sep 17 00:00:00 2001 From: Declan Millar Date: Mon, 12 Sep 2022 17:55:55 +0100 Subject: [PATCH 035/100] Add tests for NumpyMES and import in module. --- .../{__init__ => __init__.py} | 3 + .../numpy_minimum_eigensolver.py | 4 + .../test_numpy_minimum_eigensolver.py | 225 ++++++++++++++++++ .../minimum_eigensolvers/test_vqe.py | 2 +- 4 files changed, 233 insertions(+), 1 deletion(-) rename qiskit/algorithms/minimum_eigensolvers/{__init__ => __init__.py} (81%) create mode 100644 test/python/algorithms/minimum_eigensolvers/test_numpy_minimum_eigensolver.py diff --git a/qiskit/algorithms/minimum_eigensolvers/__init__ b/qiskit/algorithms/minimum_eigensolvers/__init__.py similarity index 81% rename from qiskit/algorithms/minimum_eigensolvers/__init__ rename to qiskit/algorithms/minimum_eigensolvers/__init__.py index 2d48dddaa422..1352b9794bf8 100644 --- a/qiskit/algorithms/minimum_eigensolvers/__init__ +++ b/qiskit/algorithms/minimum_eigensolvers/__init__.py @@ -13,11 +13,14 @@ """The minimum eigensolvers package.""" from .minimum_eigensolver import MinimumEigensolver, MinimumEigensolverResult +from .numpy_minimum_eigensolver import NumPyMinimumEigensolver, NumPyMinimumEigensolverResult from .vqe import VQE, VQEResult __all__ = [ "MinimumEigensolver", "MinimumEigensolverResult", + "NumPyMinimumEigensolver", + "NumPyMinimumEigensolverResult", "VQE", "VQEResult", ] diff --git a/qiskit/algorithms/minimum_eigensolvers/numpy_minimum_eigensolver.py b/qiskit/algorithms/minimum_eigensolvers/numpy_minimum_eigensolver.py index da0d10423542..cd4818e2f1b1 100644 --- a/qiskit/algorithms/minimum_eigensolvers/numpy_minimum_eigensolver.py +++ b/qiskit/algorithms/minimum_eigensolvers/numpy_minimum_eigensolver.py @@ -92,6 +92,10 @@ def compute_minimum_eigenvalue( class NumPyMinimumEigensolverResult(MinimumEigensolverResult): """NumPy minimum eigensolver result.""" + def __init__(self) -> None: + super().__init__() + self._eigenstate = None + @property def eigenstate(self) -> np.ndarray | None: """Return eigenstate.""" diff --git a/test/python/algorithms/minimum_eigensolvers/test_numpy_minimum_eigensolver.py b/test/python/algorithms/minimum_eigensolvers/test_numpy_minimum_eigensolver.py new file mode 100644 index 000000000000..2ae7edb68803 --- /dev/null +++ b/test/python/algorithms/minimum_eigensolvers/test_numpy_minimum_eigensolver.py @@ -0,0 +1,225 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2022. +# +# 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. + +""" Test NumPy minimum eigensolver """ + +import unittest +from test.python.algorithms import QiskitAlgorithmsTestCase + +import numpy as np +from ddt import ddt, data + +from qiskit.algorithms.minimum_eigensolvers import NumPyMinimumEigensolver +from qiskit.opflow import PauliSumOp, X, Y, Z + + +@ddt +class TestNumPyMinimumEigensolver(QiskitAlgorithmsTestCase): + """Test NumPy minimum eigensolver""" + + def setUp(self): + super().setUp() + self.qubit_op = PauliSumOp.from_list( + [ + ("II", -1.052373245772859), + ("ZI", 0.39793742484318045), + ("IZ", -0.39793742484318045), + ("ZZ", -0.01128010425623538), + ("XX", 0.18093119978423156), + ] + ) + + aux_op1 = PauliSumOp.from_list([("II", 2.0)]) + aux_op2 = PauliSumOp.from_list([("II", 0.5), ("ZZ", 0.5), ("YY", 0.5), ("XX", -0.5)]) + self.aux_ops_list = [aux_op1, aux_op2] + self.aux_ops_dict = {"aux_op1": aux_op1, "aux_op2": aux_op2} + + def test_cme(self): + """Basic test""" + algo = NumPyMinimumEigensolver() + result = algo.compute_minimum_eigenvalue( + operator=self.qubit_op, aux_operators=self.aux_ops_list + ) + self.assertAlmostEqual(result.eigenvalue, -1.85727503 + 0j) + self.assertEqual(len(result.aux_operator_eigenvalues), 2) + np.testing.assert_array_almost_equal(result.aux_operator_eigenvalues[0], [2, 0]) + np.testing.assert_array_almost_equal(result.aux_operator_eigenvalues[1], [0, 0]) + + def test_cme_reuse(self): + """Test reuse""" + # Start with no operator or aux_operators, give via compute method + algo = NumPyMinimumEigensolver() + result = algo.compute_minimum_eigenvalue(operator=self.qubit_op) + self.assertEqual(result.eigenvalue.dtype, np.float64) + self.assertAlmostEqual(result.eigenvalue, -1.85727503) + self.assertIsNone(result.aux_operator_eigenvalues) + + # Add aux_operators and go again + result = algo.compute_minimum_eigenvalue( + operator=self.qubit_op, aux_operators=self.aux_ops_list + ) + self.assertAlmostEqual(result.eigenvalue, -1.85727503 + 0j) + self.assertEqual(len(result.aux_operator_eigenvalues), 2) + np.testing.assert_array_almost_equal(result.aux_operator_eigenvalues[0], [2, 0]) + np.testing.assert_array_almost_equal(result.aux_operator_eigenvalues[1], [0, 0]) + + # "Remove" aux_operators and go again + result = algo.compute_minimum_eigenvalue(operator=self.qubit_op, aux_operators=[]) + self.assertEqual(result.eigenvalue.dtype, np.float64) + self.assertAlmostEqual(result.eigenvalue, -1.85727503) + self.assertIsNone(result.aux_operator_eigenvalues) + + # Set aux_operators and go again + result = algo.compute_minimum_eigenvalue( + operator=self.qubit_op, aux_operators=self.aux_ops_list + ) + self.assertAlmostEqual(result.eigenvalue, -1.85727503 + 0j) + self.assertEqual(len(result.aux_operator_eigenvalues), 2) + np.testing.assert_array_almost_equal(result.aux_operator_eigenvalues[0], [2, 0]) + np.testing.assert_array_almost_equal(result.aux_operator_eigenvalues[1], [0, 0]) + + # Finally just set one of aux_operators and main operator, remove aux_operators + result = algo.compute_minimum_eigenvalue(operator=self.aux_ops_list[0], aux_operators=[]) + self.assertAlmostEqual(result.eigenvalue, 2 + 0j) + self.assertIsNone(result.aux_operator_eigenvalues) + + def test_cme_filter(self): + """Basic test""" + + # define filter criterion + # pylint: disable=unused-argument + def criterion(x, v, a_v): + return v >= -0.5 + + algo = NumPyMinimumEigensolver(filter_criterion=criterion) + result = algo.compute_minimum_eigenvalue( + operator=self.qubit_op, aux_operators=self.aux_ops_list + ) + self.assertAlmostEqual(result.eigenvalue, -0.22491125 + 0j) + self.assertEqual(len(result.aux_operator_eigenvalues), 2) + np.testing.assert_array_almost_equal(result.aux_operator_eigenvalues[0], [2, 0]) + np.testing.assert_array_almost_equal(result.aux_operator_eigenvalues[1], [0, 0]) + + def test_cme_filter_empty(self): + """Test with filter always returning False""" + + # define filter criterion + # pylint: disable=unused-argument + def criterion(x, v, a_v): + return False + + algo = NumPyMinimumEigensolver(filter_criterion=criterion) + result = algo.compute_minimum_eigenvalue( + operator=self.qubit_op, aux_operators=self.aux_ops_list + ) + self.assertEqual(result.eigenvalue, None) + self.assertEqual(result.eigenstate, None) + self.assertEqual(result.aux_operator_eigenvalues, None) + + @data(X, Y, Z) + def test_cme_1q(self, op): + """Test for 1 qubit operator""" + algo = NumPyMinimumEigensolver() + result = algo.compute_minimum_eigenvalue(operator=op) + self.assertAlmostEqual(result.eigenvalue, -1) + + def test_cme_aux_ops_dict(self): + """Test dictionary compatibility of aux_operators""" + # Start with an empty dictionary + algo = NumPyMinimumEigensolver() + result = algo.compute_minimum_eigenvalue(operator=self.qubit_op, aux_operators={}) + self.assertAlmostEqual(result.eigenvalue, -1.85727503 + 0j) + self.assertIsNone(result.aux_operator_eigenvalues) + + # Add aux_operators dictionary and go again + result = algo.compute_minimum_eigenvalue( + operator=self.qubit_op, aux_operators=self.aux_ops_dict + ) + self.assertAlmostEqual(result.eigenvalue, -1.85727503 + 0j) + self.assertEqual(len(result.aux_operator_eigenvalues), 2) + np.testing.assert_array_almost_equal(result.aux_operator_eigenvalues["aux_op1"], [2, 0]) + np.testing.assert_array_almost_equal(result.aux_operator_eigenvalues["aux_op2"], [0, 0]) + + # Add None and zero operators and go again + extra_ops = {"None_op": None, "zero_op": 0, **self.aux_ops_dict} + result = algo.compute_minimum_eigenvalue(operator=self.qubit_op, aux_operators=extra_ops) + self.assertAlmostEqual(result.eigenvalue, -1.85727503 + 0j) + self.assertEqual(len(result.aux_operator_eigenvalues), 3) + np.testing.assert_array_almost_equal(result.aux_operator_eigenvalues["aux_op1"], [2, 0]) + np.testing.assert_array_almost_equal(result.aux_operator_eigenvalues["aux_op2"], [0, 0]) + self.assertEqual(result.aux_operator_eigenvalues["zero_op"], (0.0, 0)) + + def test_aux_operators_list(self): + """Test list-based aux_operators.""" + aux_op1 = PauliSumOp.from_list([("II", 2.0)]) + aux_op2 = PauliSumOp.from_list([("II", 0.5), ("ZZ", 0.5), ("YY", 0.5), ("XX", -0.5)]) + aux_ops = [aux_op1, aux_op2] + algo = NumPyMinimumEigensolver() + result = algo.compute_minimum_eigenvalue(operator=self.qubit_op, aux_operators=aux_ops) + self.assertAlmostEqual(result.eigenvalue, -1.85727503 + 0j) + self.assertEqual(len(result.aux_operator_eigenvalues), 2) + # expectation values + self.assertAlmostEqual(result.aux_operator_eigenvalues[0][0], 2, places=6) + self.assertAlmostEqual(result.aux_operator_eigenvalues[1][0], 0, places=6) + # standard deviations + self.assertAlmostEqual(result.aux_operator_eigenvalues[0][1], 0.0) + self.assertAlmostEqual(result.aux_operator_eigenvalues[1][1], 0.0) + + # Go again with additional None and zero operators + extra_ops = [*aux_ops, None, 0] + result = algo.compute_minimum_eigenvalue(operator=self.qubit_op, aux_operators=extra_ops) + self.assertAlmostEqual(result.eigenvalue, -1.85727503 + 0j) + self.assertEqual(len(result.aux_operator_eigenvalues), 4) + # expectation values + self.assertAlmostEqual(result.aux_operator_eigenvalues[0][0], 2, places=6) + self.assertAlmostEqual(result.aux_operator_eigenvalues[1][0], 0, places=6) + self.assertIsNone(result.aux_operator_eigenvalues[2], None) + self.assertEqual(result.aux_operator_eigenvalues[3][0], 0.0) + # standard deviations + self.assertAlmostEqual(result.aux_operator_eigenvalues[0][1], 0.0) + self.assertAlmostEqual(result.aux_operator_eigenvalues[1][1], 0.0) + self.assertEqual(result.aux_operator_eigenvalues[3][1], 0.0) + + def test_aux_operators_dict(self): + """Test dict-based aux_operators.""" + aux_op1 = PauliSumOp.from_list([("II", 2.0)]) + aux_op2 = PauliSumOp.from_list([("II", 0.5), ("ZZ", 0.5), ("YY", 0.5), ("XX", -0.5)]) + aux_ops = {"aux_op1": aux_op1, "aux_op2": aux_op2} + algo = NumPyMinimumEigensolver() + result = algo.compute_minimum_eigenvalue(operator=self.qubit_op, aux_operators=aux_ops) + self.assertAlmostEqual(result.eigenvalue, -1.85727503 + 0j) + self.assertEqual(len(result.aux_operator_eigenvalues), 2) + # expectation values + self.assertAlmostEqual(result.aux_operator_eigenvalues["aux_op1"][0], 2, places=6) + self.assertAlmostEqual(result.aux_operator_eigenvalues["aux_op2"][0], 0, places=6) + # standard deviations + self.assertAlmostEqual(result.aux_operator_eigenvalues["aux_op1"][1], 0.0) + self.assertAlmostEqual(result.aux_operator_eigenvalues["aux_op2"][1], 0.0) + + # Go again with additional None and zero operators + extra_ops = {**aux_ops, "None_operator": None, "zero_operator": 0} + result = algo.compute_minimum_eigenvalue(operator=self.qubit_op, aux_operators=extra_ops) + self.assertAlmostEqual(result.eigenvalue, -1.85727503 + 0j) + self.assertEqual(len(result.aux_operator_eigenvalues), 3) + # expectation values + self.assertAlmostEqual(result.aux_operator_eigenvalues["aux_op1"][0], 2, places=6) + self.assertAlmostEqual(result.aux_operator_eigenvalues["aux_op2"][0], 0, places=6) + self.assertEqual(result.aux_operator_eigenvalues["zero_operator"][0], 0.0) + self.assertTrue("None_operator" not in result.aux_operator_eigenvalues.keys()) + # standard deviations + self.assertAlmostEqual(result.aux_operator_eigenvalues["aux_op1"][1], 0.0) + self.assertAlmostEqual(result.aux_operator_eigenvalues["aux_op2"][1], 0.0) + self.assertAlmostEqual(result.aux_operator_eigenvalues["zero_operator"][1], 0.0) + + +if __name__ == "__main__": + unittest.main() diff --git a/test/python/algorithms/minimum_eigensolvers/test_vqe.py b/test/python/algorithms/minimum_eigensolvers/test_vqe.py index e93a3db82cf0..0c2cb9081c7d 100644 --- a/test/python/algorithms/minimum_eigensolvers/test_vqe.py +++ b/test/python/algorithms/minimum_eigensolvers/test_vqe.py @@ -23,7 +23,7 @@ from qiskit import QuantumCircuit from qiskit.algorithms import AlgorithmError from qiskit.algorithms.gradients import ParamShiftEstimatorGradient -from qiskit.algorithms.minimum_eigensolvers.vqe import VQE +from qiskit.algorithms.minimum_eigensolvers import VQE from qiskit.algorithms.optimizers import ( CG, COBYLA, From 5defcd5f8384392e77416023eb128fb4fb484a79 Mon Sep 17 00:00:00 2001 From: Declan Millar Date: Mon, 12 Sep 2022 21:11:27 +0100 Subject: [PATCH 036/100] Use lazy formatting in log messages --- qiskit/algorithms/minimum_eigensolvers/vqe.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/qiskit/algorithms/minimum_eigensolvers/vqe.py b/qiskit/algorithms/minimum_eigensolvers/vqe.py index 47135b596d2d..eee636aefd28 100644 --- a/qiskit/algorithms/minimum_eigensolvers/vqe.py +++ b/qiskit/algorithms/minimum_eigensolvers/vqe.py @@ -188,7 +188,9 @@ def compute_minimum_eigenvalue( result.optimizer_time = eval_time logger.info( - f"Optimization complete in {eval_time} seconds.\nFound opt_params {result.optimal_point}." + "Optimization complete in %s seconds.\nFound opt_params %s", + eval_time, + result.optimal_point, ) if aux_operators: @@ -254,7 +256,7 @@ def _check_operator_ansatz( if operator.num_qubits != ansatz.num_qubits: try: logger.info( - f"Trying to resize ansatz to match operator on {operator.num_qubits} qubits." + "Trying to resize ansatz to match operator on %s qubits.", operator.num_qubits ) ansatz.num_qubits = operator.num_qubits except AttributeError as error: From b7c4a74cd35e31da63bbf4b6114d35d7e673c2ee Mon Sep 17 00:00:00 2001 From: Declan Millar Date: Mon, 12 Sep 2022 21:54:06 +0100 Subject: [PATCH 037/100] Use lazy formatting in log messages --- .../minimum_eigensolvers/numpy_minimum_eigensolver.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/qiskit/algorithms/minimum_eigensolvers/numpy_minimum_eigensolver.py b/qiskit/algorithms/minimum_eigensolvers/numpy_minimum_eigensolver.py index cd4818e2f1b1..c8494e4e3f29 100644 --- a/qiskit/algorithms/minimum_eigensolvers/numpy_minimum_eigensolver.py +++ b/qiskit/algorithms/minimum_eigensolvers/numpy_minimum_eigensolver.py @@ -55,7 +55,7 @@ def __init__( def filter_criterion( self, ) -> Callable[[list | np.ndarray, float, ListOrDict[float] | None], bool] | None: - """returns the filter criterion if set""" + """Filters the eigenstates/eigenvalues.""" return self._ces.filter_criterion @filter_criterion.setter @@ -63,7 +63,6 @@ def filter_criterion( self, filter_criterion: Callable[[list | np.ndarray, float, ListOrDict[float] | None], bool], ) -> None: - """set the filter criterion""" self._ces.filter_criterion = filter_criterion @classmethod @@ -84,7 +83,7 @@ def compute_minimum_eigenvalue( if result_ces.aux_operator_eigenvalues: result.aux_operator_eigenvalues = result_ces.aux_operator_eigenvalues[0] - logger.debug(f"MinimumEigensolver:\n{result}") + logger.debug("NumPy minimum eigensolver result: %s", result) return result @@ -98,10 +97,9 @@ def __init__(self) -> None: @property def eigenstate(self) -> np.ndarray | None: - """Return eigenstate.""" + """The eigenstate corresponding to the computed minimum eigenvalue.""" return self._eigenstate @eigenstate.setter def eigenstate(self, value: np.ndarray) -> None: - """Set eigenstate.""" self._eigenstate = value From af47deac27357b598f59209ea32f9aeebef0fd3c Mon Sep 17 00:00:00 2001 From: Declan Millar Date: Tue, 13 Sep 2022 11:37:24 +0100 Subject: [PATCH 038/100] Add back callback to VQE. --- qiskit/algorithms/minimum_eigensolvers/vqe.py | 16 ++++++++++- .../minimum_eigensolvers/test_vqe.py | 27 +++++++++++++++++++ 2 files changed, 42 insertions(+), 1 deletion(-) diff --git a/qiskit/algorithms/minimum_eigensolvers/vqe.py b/qiskit/algorithms/minimum_eigensolvers/vqe.py index eee636aefd28..ffff8a0e8be7 100644 --- a/qiskit/algorithms/minimum_eigensolvers/vqe.py +++ b/qiskit/algorithms/minimum_eigensolvers/vqe.py @@ -109,6 +109,7 @@ def __init__( optimizer: Optimizer | Minimizer | None = None, gradient: BaseEstimatorGradient | None = None, initial_point: Sequence[float] | None = None, + callback: Callable[[int, np.ndarray, float, float], None] | None = None, ) -> None: """ Args: @@ -129,9 +130,9 @@ def __init__( self.ansatz = ansatz self.optimizer = optimizer self.gradient = gradient - # this has to go via getters and setters due to the VariationalAlgorithm interface self.initial_point = initial_point + self.callback = callback @property def initial_point(self) -> Sequence[float] | None: @@ -222,13 +223,26 @@ def _get_energy_evaluation( """ num_parameters = ansatz.num_parameters + # avoid creating an instance variable to remain stateless regarding results + eval_count = 0 + def energy_evaluation(parameters): + nonlocal eval_count + # handle broadcasting: ensure parameters is of shape [array, array, ...] parameters = np.reshape(parameters, (-1, num_parameters)).tolist() batchsize = len(parameters) job = self.estimator.run(batchsize * [ansatz], batchsize * [operator], parameters) values = job.result().values + + # TODO recover variance from estimator if has metadata has shots? + + if self.callback is not None: + for params, value in zip(parameters, values): + eval_count += 1 + self.callback(eval_count, params, value, 0.0) + return values[0] if len(values) == 1 else values return energy_evaluation diff --git a/test/python/algorithms/minimum_eigensolvers/test_vqe.py b/test/python/algorithms/minimum_eigensolvers/test_vqe.py index 0c2cb9081c7d..35dd136cf72e 100644 --- a/test/python/algorithms/minimum_eigensolvers/test_vqe.py +++ b/test/python/algorithms/minimum_eigensolvers/test_vqe.py @@ -231,6 +231,33 @@ def test_with_two_qubit_reduction(self): result = vqe.compute_minimum_eigenvalue(tapered_qubit_op) self.assertAlmostEqual(result.eigenvalue.real, self.h2_energy, places=2) + def test_callback(self): + """Test the callback on VQE.""" + history = {"eval_count": [], "parameters": [], "mean": [], "std": []} + + def store_intermediate_result(eval_count, parameters, mean, std): + history["eval_count"].append(eval_count) + history["parameters"].append(parameters) + history["mean"].append(mean) + history["std"].append(std) + + optimizer = COBYLA(maxiter=3) + wavefunction = self.ry_wavefunction + + vqe = VQE( + Estimator(), + ansatz=wavefunction, + optimizer=optimizer, + callback=store_intermediate_result, + ) + vqe.compute_minimum_eigenvalue(operator=self.h2_op) + + self.assertTrue(all(isinstance(count, int) for count in history["eval_count"])) + self.assertTrue(all(isinstance(mean, float) for mean in history["mean"])) + self.assertTrue(all(isinstance(std, float) for std in history["std"])) + for params in history["parameters"]: + self.assertTrue(all(isinstance(param, float) for param in params)) + def test_reuse(self): """Test re-using a VQE algorithm instance.""" ansatz = TwoLocal(rotation_blocks=["ry", "rz"], entanglement_blocks="cz") From f6826167bae024d984a69ec15f6695473ceaa7fb Mon Sep 17 00:00:00 2001 From: Declan Millar Date: Tue, 13 Sep 2022 12:54:03 +0100 Subject: [PATCH 039/100] minor renaming --- qiskit/algorithms/minimum_eigensolvers/vqe.py | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/qiskit/algorithms/minimum_eigensolvers/vqe.py b/qiskit/algorithms/minimum_eigensolvers/vqe.py index ffff8a0e8be7..e21282fef932 100644 --- a/qiskit/algorithms/minimum_eigensolvers/vqe.py +++ b/qiskit/algorithms/minimum_eigensolvers/vqe.py @@ -163,19 +163,19 @@ def compute_minimum_eigenvalue( start_time = time() - energy_evaluation = self._get_energy_evaluation(ansatz, operator) + evaluate_energy = self._get_evaluate_energy(ansatz, operator) if self.gradient is not None: - gradient_evaluation = self._get_gradient_evaluation(ansatz, operator) + evaluate_gradient = self._get_evalute_gradient(ansatz, operator) else: - gradient_evaluation = None + evaluate_gradient = None # perform optimization if callable(optimizer): - opt_result = optimizer(fun=energy_evaluation, x0=initial_point, jac=gradient_evaluation) + opt_result = optimizer(fun=evaluate_energy, x0=initial_point, jac=evaluate_gradient) else: opt_result = optimizer.minimize( - fun=energy_evaluation, x0=initial_point, jac=gradient_evaluation + fun=evaluate_energy, x0=initial_point, jac=evaluate_gradient ) eval_time = time() - start_time @@ -208,7 +208,7 @@ def compute_minimum_eigenvalue( def supports_aux_operators(cls) -> bool: return True - def _get_energy_evaluation( + def _get_evaluate_energy( self, ansatz: QuantumCircuit, operator: BaseOperator | PauliSumOp, @@ -226,7 +226,7 @@ def _get_energy_evaluation( # avoid creating an instance variable to remain stateless regarding results eval_count = 0 - def energy_evaluation(parameters): + def evaluate_energy(parameters): nonlocal eval_count # handle broadcasting: ensure parameters is of shape [array, array, ...] @@ -245,21 +245,21 @@ def energy_evaluation(parameters): return values[0] if len(values) == 1 else values - return energy_evaluation + return evaluate_energy - def _get_gradient_evaluation( + def _get_evalute_gradient( self, ansatz: QuantumCircuit, operator: BaseOperator | PauliSumOp, ) -> tuple[Callable[[np.ndarray], np.ndarray]]: """Returns a function handle to evaluate the gradient at given parameters for the ansatz.""" - def gradient_evaluation(parameters): + def evaluate_gradient(parameters): # broadcasting not required for the estimator gradients result = self.gradient.run([ansatz], [operator], [parameters]).result() return result.gradients[0] - return gradient_evaluation + return evaluate_gradient def _check_operator_ansatz( self, operator: BaseOperator | PauliSumOp, ansatz: QuantumCircuit From 16dc12c141a0260d203e9406325432fc81226ce1 Mon Sep 17 00:00:00 2001 From: Declan Millar Date: Tue, 13 Sep 2022 13:20:47 +0100 Subject: [PATCH 040/100] raise algorithm error if primitive jobs fail --- qiskit/algorithms/minimum_eigensolvers/vqe.py | 30 +++++++++++++++---- 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/qiskit/algorithms/minimum_eigensolvers/vqe.py b/qiskit/algorithms/minimum_eigensolvers/vqe.py index e21282fef932..6086efe4cbed 100644 --- a/qiskit/algorithms/minimum_eigensolvers/vqe.py +++ b/qiskit/algorithms/minimum_eigensolvers/vqe.py @@ -216,10 +216,12 @@ def _get_evaluate_energy( """Returns a function handle to evaluates the energy at given parameters for the ansatz. This is the objective function to be passed to the optimizer that is used for evaluation. Args: - operator: The operator whose energy to evaluate. ansatz: The ansatz preparing the quantum state. + operator: The operator whose energy to evaluate. Returns: Energy of the hamiltonian of each parameter. + Raises: + AlgorithmError: If the primitive job to evaluate the energy fails. """ num_parameters = ansatz.num_parameters @@ -233,8 +235,11 @@ def evaluate_energy(parameters): parameters = np.reshape(parameters, (-1, num_parameters)).tolist() batchsize = len(parameters) - job = self.estimator.run(batchsize * [ansatz], batchsize * [operator], parameters) - values = job.result().values + try: + job = self.estimator.run(batchsize * [ansatz], batchsize * [operator], parameters) + values = job.result().values + except Exception as exc: + raise AlgorithmError("The primitive job to evaluate the energy failed!") from exc # TODO recover variance from estimator if has metadata has shots? @@ -252,12 +257,25 @@ def _get_evalute_gradient( ansatz: QuantumCircuit, operator: BaseOperator | PauliSumOp, ) -> tuple[Callable[[np.ndarray], np.ndarray]]: - """Returns a function handle to evaluate the gradient at given parameters for the ansatz.""" + """Returns a function handle to evaluate the gradient at given parameters for the ansatz. + + Args: + ansatz: The ansatz preparing the quantum state. + operator: The operator whose energy to evaluate. + + Raises: + AlgorithmError: If the primitive job to evaluate the gradient fails. + """ def evaluate_gradient(parameters): # broadcasting not required for the estimator gradients - result = self.gradient.run([ansatz], [operator], [parameters]).result() - return result.gradients[0] + try: + result = self.gradient.run([ansatz], [operator], [parameters]).result() + gradients = result.gradients + except Exception as exc: + raise AlgorithmError("The primitive job to evaluate the gradient failed!") from exc + + return gradients[0] return evaluate_gradient From 994247b58a965a2b11ecbf2dffe989242ac2685c Mon Sep 17 00:00:00 2001 From: Declan Millar Date: Tue, 13 Sep 2022 14:51:15 +0100 Subject: [PATCH 041/100] Add return documentation --- qiskit/algorithms/minimum_eigensolvers/vqe.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/qiskit/algorithms/minimum_eigensolvers/vqe.py b/qiskit/algorithms/minimum_eigensolvers/vqe.py index 6086efe4cbed..c1319d924d2a 100644 --- a/qiskit/algorithms/minimum_eigensolvers/vqe.py +++ b/qiskit/algorithms/minimum_eigensolvers/vqe.py @@ -257,12 +257,15 @@ def _get_evalute_gradient( ansatz: QuantumCircuit, operator: BaseOperator | PauliSumOp, ) -> tuple[Callable[[np.ndarray], np.ndarray]]: - """Returns a function handle to evaluate the gradient at given parameters for the ansatz. + """Get a function handle to evaluate the gradient at given parameters for the ansatz. Args: ansatz: The ansatz preparing the quantum state. operator: The operator whose energy to evaluate. + Returns: + A function handle to evaluate the gradient at given parameters for the ansatz. + Raises: AlgorithmError: If the primitive job to evaluate the gradient fails. """ From 688f9f16cb1d173b6687236bffc9fdb4ef5965cb Mon Sep 17 00:00:00 2001 From: Declan Millar Date: Wed, 14 Sep 2022 16:41:16 +0100 Subject: [PATCH 042/100] Improve var names and docstrings. --- .../minimum_eigensolver.py | 18 ++++++++--------- .../numpy_minimum_eigensolver.py | 20 +++++++++---------- qiskit/algorithms/minimum_eigensolvers/vqe.py | 13 ++++++++---- 3 files changed, 28 insertions(+), 23 deletions(-) diff --git a/qiskit/algorithms/minimum_eigensolvers/minimum_eigensolver.py b/qiskit/algorithms/minimum_eigensolvers/minimum_eigensolver.py index 03b917aa8578..571ad6e5bf6b 100644 --- a/qiskit/algorithms/minimum_eigensolvers/minimum_eigensolver.py +++ b/qiskit/algorithms/minimum_eigensolvers/minimum_eigensolver.py @@ -37,9 +37,10 @@ def compute_minimum_eigenvalue( aux_operators: ListOrDict[BaseOperator | PauliSumOp] | None = None, ) -> "MinimumEigensolverResult": """ - Computes minimum eigenvalue. Operator and aux_operators can be supplied here and if not None - will override any already set into algorithm so it can be reused with different operators. - While an operator is required by algorithms, aux_operators are optional. + Computes the minimum eigenvalue. The `operator` and `aux_operators` can be supplied here + and if not ``None`` will override any already set into algorithm so it can be reused with + different operators. While an ``operator`` is required by algorithms, ``aux_operators`` are + optional. Args: operator: Qubit operator of the observable. @@ -57,8 +58,9 @@ def compute_minimum_eigenvalue( def supports_aux_operators(cls) -> bool: """Whether computing the expectation value of auxiliary operators is supported. - If the minimum eigensolver computes an eigenvalue of the main operator then it can compute - the expectation value of the aux_operators for that state. Otherwise they will be ignored. + If the minimum eigensolver computes an eigenvalue of the main ``operator`` then it can + compute the expectation value of the ``aux_operators`` for that state. Otherwise they will + be ignored. Returns: True if aux_operator expectations can be evaluated, False otherwise @@ -76,17 +78,16 @@ def __init__(self) -> None: @property def eigenvalue(self) -> complex | None: - """Return the eigenvalue.""" + """The computed minimum eigenvalue.""" return self._eigenvalue @eigenvalue.setter def eigenvalue(self, value: complex) -> None: - """Set the eigenvalue.""" self._eigenvalue = value @property def aux_operator_eigenvalues(self) -> ListOrDict[tuple[complex, complex]] | None: - """Return aux operator expectation values. + """The aux operator expectation values. These values are in fact tuples formatted as (mean, standard deviation). """ @@ -94,5 +95,4 @@ def aux_operator_eigenvalues(self) -> ListOrDict[tuple[complex, complex]] | None @aux_operator_eigenvalues.setter def aux_operator_eigenvalues(self, value: ListOrDict[tuple[complex, complex]]) -> None: - """set aux operator eigenvalues""" self._aux_operator_eigenvalues = value diff --git a/qiskit/algorithms/minimum_eigensolvers/numpy_minimum_eigensolver.py b/qiskit/algorithms/minimum_eigensolvers/numpy_minimum_eigensolver.py index c8494e4e3f29..726f0c127006 100644 --- a/qiskit/algorithms/minimum_eigensolvers/numpy_minimum_eigensolver.py +++ b/qiskit/algorithms/minimum_eigensolvers/numpy_minimum_eigensolver.py @@ -49,21 +49,21 @@ def __init__( whether to consider this value or not. If there is no feasible element, the result can even be empty. """ - self._ces = NumPyEigensolver(filter_criterion=filter_criterion) + self._eigensolver = NumPyEigensolver(filter_criterion=filter_criterion) @property def filter_criterion( self, ) -> Callable[[list | np.ndarray, float, ListOrDict[float] | None], bool] | None: - """Filters the eigenstates/eigenvalues.""" - return self._ces.filter_criterion + """Returns the criterion for filtering eigenstates/eigenvalues.""" + return self._eigensolver.filter_criterion @filter_criterion.setter def filter_criterion( self, filter_criterion: Callable[[list | np.ndarray, float, ListOrDict[float] | None], bool], ) -> None: - self._ces.filter_criterion = filter_criterion + self._eigensolver.filter_criterion = filter_criterion @classmethod def supports_aux_operators(cls) -> bool: @@ -75,13 +75,13 @@ def compute_minimum_eigenvalue( aux_operators: ListOrDict[BaseOperator | PauliSumOp] | None = None, ) -> NumPyMinimumEigensolverResult: super().compute_minimum_eigenvalue(operator, aux_operators) - result_ces = self._ces.compute_eigenvalues(operator, aux_operators) + eigensolver_result = self._eigensolver.compute_eigenvalues(operator, aux_operators) result = NumPyMinimumEigensolverResult() - if result_ces.eigenvalues is not None and len(result_ces.eigenvalues) > 0: - result.eigenvalue = result_ces.eigenvalues[0] - result.eigenstate = result_ces.eigenstates[0] - if result_ces.aux_operator_eigenvalues: - result.aux_operator_eigenvalues = result_ces.aux_operator_eigenvalues[0] + if eigensolver_result.eigenvalues is not None and len(eigensolver_result.eigenvalues) > 0: + result.eigenvalue = eigensolver_result.eigenvalues[0] + result.eigenstate = eigensolver_result.eigenstates[0] + if eigensolver_result.aux_operator_eigenvalues: + result.aux_operator_eigenvalues = eigensolver_result.aux_operator_eigenvalues[0] logger.debug("NumPy minimum eigensolver result: %s", result) diff --git a/qiskit/algorithms/minimum_eigensolvers/vqe.py b/qiskit/algorithms/minimum_eigensolvers/vqe.py index c1319d924d2a..916c9b60e8e2 100644 --- a/qiskit/algorithms/minimum_eigensolvers/vqe.py +++ b/qiskit/algorithms/minimum_eigensolvers/vqe.py @@ -117,12 +117,17 @@ def __init__( ansatz: A parameterized circuit, preparing the ansatz for the wave function. If not provided, this defaults to a :class:`.RealAmplitudes` circuit. optimizer: A classical optimizer to find the minimum energy. This can either be a - Qiskit :class:`.Optimizer` or a callable implementing the :class:`.Minimizer` protocol. - Defaults to :class:`.SLSQP`. - gradient: An optional gradient function or operator for the optimizer. - initial_point: An optional initial point (i.e. initial parameter values) + Qiskit :class:`.Optimizer` or a callable implementing the :class:`.Minimizer` + protocol. Defaults to :class:`.SLSQP`. + gradient: An optional gradient function or operator for the optimizer. 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. + callback: A callback that can access the intermediate data during the optimization. + Four parameter values are passed to the callback as follows during each evaluation + by the optimizer for its current set of parameters as it works towards the minimum. + These are: the evaluation count, the optimizer parameters for the ansatz, the + evaluated mean and the evaluated standard deviation. """ super().__init__() From a9d6d419a0d331c96c0cff41ef1600979f940475 Mon Sep 17 00:00:00 2001 From: Declan Millar Date: Thu, 15 Sep 2022 12:07:05 +0100 Subject: [PATCH 043/100] Apply suggestions from code review Co-authored-by: ElePT <57907331+ElePT@users.noreply.github.com> Co-authored-by: dlasecki --- .../minimum_eigensolvers/numpy_minimum_eigensolver.py | 2 +- qiskit/algorithms/minimum_eigensolvers/vqe.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/qiskit/algorithms/minimum_eigensolvers/numpy_minimum_eigensolver.py b/qiskit/algorithms/minimum_eigensolvers/numpy_minimum_eigensolver.py index 726f0c127006..6d5cf2c33b1f 100644 --- a/qiskit/algorithms/minimum_eigensolvers/numpy_minimum_eigensolver.py +++ b/qiskit/algorithms/minimum_eigensolvers/numpy_minimum_eigensolver.py @@ -10,7 +10,7 @@ # copyright notice, and modified files need to carry a notice indicating # that they have been altered from the originals. -"""The NumPy minimum eigensolver algorithm.""" +"""The NumPy minimum eigensolver algorithm and result.""" from __future__ import annotations diff --git a/qiskit/algorithms/minimum_eigensolvers/vqe.py b/qiskit/algorithms/minimum_eigensolvers/vqe.py index 916c9b60e8e2..ad2f0d2df6ac 100644 --- a/qiskit/algorithms/minimum_eigensolvers/vqe.py +++ b/qiskit/algorithms/minimum_eigensolvers/vqe.py @@ -218,13 +218,13 @@ def _get_evaluate_energy( ansatz: QuantumCircuit, operator: BaseOperator | PauliSumOp, ) -> tuple[Callable[[np.ndarray], float | list[float]], dict]: - """Returns a function handle to evaluates the energy at given parameters for the ansatz. + """Returns a function handle to evaluate the energy at given parameters for the ansatz. This is the objective function to be passed to the optimizer that is used for evaluation. Args: ansatz: The ansatz preparing the quantum state. operator: The operator whose energy to evaluate. Returns: - Energy of the hamiltonian of each parameter. + Energy of the Hamiltonian of each parameter. Raises: AlgorithmError: If the primitive job to evaluate the energy fails. """ From 6e99b34044cf9c8e0f250d1524d371b56dbd7093 Mon Sep 17 00:00:00 2001 From: Declan Millar Date: Thu, 15 Sep 2022 12:30:51 +0100 Subject: [PATCH 044/100] minor formatting --- .../algorithms/minimum_eigensolvers/minimum_eigensolver.py | 2 +- .../minimum_eigensolvers/numpy_minimum_eigensolver.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/qiskit/algorithms/minimum_eigensolvers/minimum_eigensolver.py b/qiskit/algorithms/minimum_eigensolvers/minimum_eigensolver.py index 571ad6e5bf6b..fa38330861eb 100644 --- a/qiskit/algorithms/minimum_eigensolvers/minimum_eigensolver.py +++ b/qiskit/algorithms/minimum_eigensolvers/minimum_eigensolver.py @@ -37,7 +37,7 @@ def compute_minimum_eigenvalue( aux_operators: ListOrDict[BaseOperator | PauliSumOp] | None = None, ) -> "MinimumEigensolverResult": """ - Computes the minimum eigenvalue. The `operator` and `aux_operators` can be supplied here + Computes the minimum eigenvalue. The ``operator`` and ``aux_operators`` can be supplied here and if not ``None`` will override any already set into algorithm so it can be reused with different operators. While an ``operator`` is required by algorithms, ``aux_operators`` are optional. diff --git a/qiskit/algorithms/minimum_eigensolvers/numpy_minimum_eigensolver.py b/qiskit/algorithms/minimum_eigensolvers/numpy_minimum_eigensolver.py index 6d5cf2c33b1f..af6958408000 100644 --- a/qiskit/algorithms/minimum_eigensolvers/numpy_minimum_eigensolver.py +++ b/qiskit/algorithms/minimum_eigensolvers/numpy_minimum_eigensolver.py @@ -45,9 +45,9 @@ def __init__( filter_criterion: callable that allows to filter eigenvalues/eigenstates. The minimum eigensolver is only searching over feasible states and returns an eigenstate that has the smallest eigenvalue among feasible states. The callable has the signature - `filter(eigenstate, eigenvalue, aux_values)` and must return a boolean to indicate - whether to consider this value or not. If there is no - feasible element, the result can even be empty. + ``filter(eigenstate, eigenvalue, aux_values)`` and must return a boolean to indicate + whether to consider this value or not. If there is no feasible element, the result + can even be empty. """ self._eigensolver = NumPyEigensolver(filter_criterion=filter_criterion) From 6abff889c07651fe23eea6e0c8b484cc09c92781 Mon Sep 17 00:00:00 2001 From: Declan Millar Date: Thu, 15 Sep 2022 12:32:15 +0100 Subject: [PATCH 045/100] minor formatting --- .../minimum_eigensolvers/numpy_minimum_eigensolver.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qiskit/algorithms/minimum_eigensolvers/numpy_minimum_eigensolver.py b/qiskit/algorithms/minimum_eigensolvers/numpy_minimum_eigensolver.py index af6958408000..eb021f926109 100644 --- a/qiskit/algorithms/minimum_eigensolvers/numpy_minimum_eigensolver.py +++ b/qiskit/algorithms/minimum_eigensolvers/numpy_minimum_eigensolver.py @@ -97,7 +97,7 @@ def __init__(self) -> None: @property def eigenstate(self) -> np.ndarray | None: - """The eigenstate corresponding to the computed minimum eigenvalue.""" + """Returns the eigenstate corresponding to the computed minimum eigenvalue.""" return self._eigenstate @eigenstate.setter From 0b5c185edd6a8114a536a4bb1bcd436b55b881ba Mon Sep 17 00:00:00 2001 From: Declan Millar Date: Thu, 15 Sep 2022 15:50:31 +0100 Subject: [PATCH 046/100] Ensure evaluate_energy/gradient match Co-authored-by: Max Rossmannek --- qiskit/algorithms/minimum_eigensolvers/vqe.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/qiskit/algorithms/minimum_eigensolvers/vqe.py b/qiskit/algorithms/minimum_eigensolvers/vqe.py index ad2f0d2df6ac..6eec34f8ddc6 100644 --- a/qiskit/algorithms/minimum_eigensolvers/vqe.py +++ b/qiskit/algorithms/minimum_eigensolvers/vqe.py @@ -278,8 +278,8 @@ def _get_evalute_gradient( def evaluate_gradient(parameters): # broadcasting not required for the estimator gradients try: - result = self.gradient.run([ansatz], [operator], [parameters]).result() - gradients = result.gradients + job = self.gradient.run([ansatz], [operator], [parameters]) + gradients = job.result().gradients except Exception as exc: raise AlgorithmError("The primitive job to evaluate the gradient failed!") from exc From e2dafa8c815e2e887f9fdf9d322b847ad37b9c0b Mon Sep 17 00:00:00 2001 From: Declan Millar Date: Fri, 16 Sep 2022 12:42:08 +0100 Subject: [PATCH 047/100] return bounds logic; fix some docstrings and minor refactor --- qiskit/algorithms/minimum_eigensolvers/vqe.py | 59 ++++++++++++------- 1 file changed, 39 insertions(+), 20 deletions(-) diff --git a/qiskit/algorithms/minimum_eigensolvers/vqe.py b/qiskit/algorithms/minimum_eigensolvers/vqe.py index 6eec34f8ddc6..2cca1e653490 100644 --- a/qiskit/algorithms/minimum_eigensolvers/vqe.py +++ b/qiskit/algorithms/minimum_eigensolvers/vqe.py @@ -45,7 +45,10 @@ class VQE(VariationalAlgorithm, MinimumEigensolver): VQE is a quantum algorithm that uses a variational technique to find the minimum eigenvalue of the Hamiltonian :math:`H` of a given system [1]. - An instance of VQE requires defining two algorithmic sub-components: a trial state (a.k.a. + The central sub-component of VQE is an Estimator primitive, which must be passed in as the + first argument. This is used to estimate the expectation values of :math:`H`. + + An instance of VQE also requires defining two algorithmic sub-components: a trial state (a.k.a. ansatz) which is a :class:`QuantumCircuit`, and one of the classical :mod:`~qiskit.algorithms.optimizers`. @@ -56,11 +59,6 @@ class VQE(VariationalAlgorithm, MinimumEigensolver): 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 @@ -96,6 +94,11 @@ def my_minimizer(fun, x0, jac=None, bounds=None) -> OptimizerResult: initial_point: An optional initial point (i.e. initial parameter values) for the optimizer. If not provided, a random initial point with values in the interval :math:`[0, 2\pi]` is used. + callback: A callback that can access the intermediate data during the optimization. + Four parameter values are passed to the callback as follows during each evaluation + by the optimizer for its current set of parameters as it works towards the minimum. + These are: the evaluation count, the optimizer parameters for the ansatz, the + evaluated mean and the evaluated standard deviation. References: [1] Peruzzo et al, "A variational eigenvalue solver on a quantum processor" @@ -154,18 +157,14 @@ def compute_minimum_eigenvalue( operator: BaseOperator | PauliSumOp, aux_operators: ListOrDict[BaseOperator | PauliSumOp] | None = None, ) -> VQEResult: - # set defaults - if self.ansatz is None: - ansatz = RealAmplitudes(num_qubits=operator.num_qubits) - else: - ansatz = self.ansatz - - ansatz = self._check_operator_ansatz(operator, ansatz) + ansatz = self._check_operator_ansatz(operator) optimizer = SLSQP() if self.optimizer is None else self.optimizer initial_point = _validate_initial_point(self.initial_point, ansatz) + bounds = _validate_bounds(ansatz) + start_time = time() evaluate_energy = self._get_evaluate_energy(ansatz, operator) @@ -177,16 +176,18 @@ def compute_minimum_eigenvalue( # perform optimization if callable(optimizer): - opt_result = optimizer(fun=evaluate_energy, x0=initial_point, jac=evaluate_gradient) + opt_result = optimizer( + fun=evaluate_energy, x0=initial_point, jac=evaluate_gradient, bounds=bounds + ) else: opt_result = optimizer.minimize( - fun=evaluate_energy, x0=initial_point, jac=evaluate_gradient + fun=evaluate_energy, x0=initial_point, jac=evaluate_gradient, bounds=bounds ) eval_time = time() - start_time result = VQEResult() - result.eigenvalue = opt_result.fun + 0j + result.eigenvalue = opt_result.fun result.cost_function_evals = opt_result.nfev result.optimal_point = opt_result.x result.optimal_parameters = dict(zip(ansatz.parameters, opt_result.x)) @@ -287,12 +288,16 @@ def evaluate_gradient(parameters): return evaluate_gradient - def _check_operator_ansatz( - self, operator: BaseOperator | PauliSumOp, ansatz: QuantumCircuit - ) -> QuantumCircuit: + def _check_operator_ansatz(self, operator: BaseOperator | PauliSumOp) -> QuantumCircuit: """Check that the number of qubits of operator and ansatz match and that the ansatz is parameterized. """ + # set defaults + if self.ansatz is None: + ansatz = RealAmplitudes(num_qubits=operator.num_qubits) + else: + ansatz = self.ansatz + if operator.num_qubits != ansatz.num_qubits: try: logger.info( @@ -334,7 +339,7 @@ def _eval_aux_ops( return aux_values -def _validate_initial_point(point, ansatz): +def _validate_initial_point(point: Sequence[float], ansatz: QuantumCircuit) -> Sequence[float]: expected_size = ansatz.num_parameters if point is None: @@ -362,6 +367,20 @@ def _validate_initial_point(point, ansatz): return point +def _validate_bounds(ansatz: QuantumCircuit) -> list[tuple(float | None, float | None)]: + if hasattr(ansatz, "parameter_bounds") and ansatz.parameter_bounds is not None: + bounds = ansatz.parameter_bounds + if len(bounds) != ansatz.num_parameters: + raise ValueError( + f"The number of bounds ({len(bounds)}) does not match the number of " + f"parameters in the circuit ({ansatz.num_parameters})." + ) + else: + bounds = [(None, None)] * ansatz.num_parameters + + return bounds + + class VQEResult(VariationalResult, MinimumEigensolverResult): """Variational quantum eigensolver result.""" From 1eb2329252e3bf812ce2834e9e91299c9abbfbd0 Mon Sep 17 00:00:00 2001 From: Declan Millar Date: Fri, 16 Sep 2022 15:29:05 +0100 Subject: [PATCH 048/100] Force keyword arguments in vqe. --- qiskit/algorithms/minimum_eigensolvers/vqe.py | 37 ++++++++++--------- .../minimum_eigensolvers/test_vqe.py | 6 +-- 2 files changed, 22 insertions(+), 21 deletions(-) diff --git a/qiskit/algorithms/minimum_eigensolvers/vqe.py b/qiskit/algorithms/minimum_eigensolvers/vqe.py index 2cca1e653490..c699ab9feb73 100644 --- a/qiskit/algorithms/minimum_eigensolvers/vqe.py +++ b/qiskit/algorithms/minimum_eigensolvers/vqe.py @@ -87,13 +87,15 @@ def my_minimizer(fun, x0, jac=None, bounds=None) -> OptimizerResult: Attributes: estimator: The estimator primitive to compute the expectation value of the circuits. - ansatz: The parameterized circuit used as an ansatz for the wave function. + ansatz: A parameterized circuit, preparing the ansatz for the wave function. If + provided with ``None``, this defaults to a :class:`.RealAmplitudes` circuit. optimizer: A classical optimizer to find the minimum energy. This can either be a - Qiskit :class:`.Optimizer` or a callable implementing the :class:`.Minimizer` protocol. - gradient: An optional gradient function or operator for the optimizer. - initial_point: An optional initial point (i.e. initial parameter values) for the optimizer. - If not provided, a random initial point with values in the interval :math:`[0, 2\pi]` - is used. + Qiskit :class:`.Optimizer` or a callable implementing the :class:`.Minimizer` + protocol. + gradient: An optional estimator gradient to be used with the optimizer. + 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. callback: A callback that can access the intermediate data during the optimization. Four parameter values are passed to the callback as follows during each evaluation by the optimizer for its current set of parameters as it works towards the minimum. @@ -108,8 +110,9 @@ def my_minimizer(fun, x0, jac=None, bounds=None) -> OptimizerResult: def __init__( self, estimator: BaseEstimator, - ansatz: QuantumCircuit | None = None, - optimizer: Optimizer | Minimizer | None = None, + ansatz: QuantumCircuit | None, + optimizer: Optimizer | Minimizer, + *, gradient: BaseEstimatorGradient | None = None, initial_point: Sequence[float] | None = None, callback: Callable[[int, np.ndarray, float, float], None] | None = None, @@ -117,13 +120,13 @@ def __init__( """ Args: estimator: The estimator primitive to compute the expectation value of the circuits. - ansatz: A parameterized circuit, preparing the ansatz for the wave function. If not - provided, this defaults to a :class:`.RealAmplitudes` circuit. + ansatz: A parameterized circuit, preparing the ansatz for the wave function. If + provided with ``None``, this defaults to a :class:`.RealAmplitudes` circuit. optimizer: A classical optimizer to find the minimum energy. This can either be a Qiskit :class:`.Optimizer` or a callable implementing the :class:`.Minimizer` - protocol. Defaults to :class:`.SLSQP`. - gradient: An optional gradient function or operator for the optimizer. initial_point: An - optional initial point (i.e. initial parameter values) + protocol. + gradient: An optional estimator gradient to be used with the optimizer. + 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. callback: A callback that can access the intermediate data during the optimization. @@ -159,8 +162,6 @@ def compute_minimum_eigenvalue( ) -> VQEResult: ansatz = self._check_operator_ansatz(operator) - optimizer = SLSQP() if self.optimizer is None else self.optimizer - initial_point = _validate_initial_point(self.initial_point, ansatz) bounds = _validate_bounds(ansatz) @@ -175,12 +176,12 @@ def compute_minimum_eigenvalue( evaluate_gradient = None # perform optimization - if callable(optimizer): - opt_result = optimizer( + if callable(self.optimizer): + opt_result = self.optimizer( fun=evaluate_energy, x0=initial_point, jac=evaluate_gradient, bounds=bounds ) else: - opt_result = optimizer.minimize( + opt_result = self.optimizer.minimize( fun=evaluate_energy, x0=initial_point, jac=evaluate_gradient, bounds=bounds ) diff --git a/test/python/algorithms/minimum_eigensolvers/test_vqe.py b/test/python/algorithms/minimum_eigensolvers/test_vqe.py index 35dd136cf72e..af32c2d19a33 100644 --- a/test/python/algorithms/minimum_eigensolvers/test_vqe.py +++ b/test/python/algorithms/minimum_eigensolvers/test_vqe.py @@ -101,9 +101,9 @@ def test_basic_aer_statevector(self, estimator): with self.subTest(msg="assert optimizer_time is set"): self.assertIsNotNone(result.optimizer_time) - def test_default_values(self): - """Test all default values are set as expected.""" - vqe = VQE(Estimator()) + def test_default_ansatz(self): + """Test default ansatz is set as expected.""" + vqe = VQE(Estimator(), None, SLSQP()) result = vqe.compute_minimum_eigenvalue(operator=self.h2_op) self.assertAlmostEqual(result.eigenvalue, self.h2_energy, places=5) From c05cdbc01750b93f772862261f40027bf8076cc7 Mon Sep 17 00:00:00 2001 From: Declan Millar Date: Fri, 16 Sep 2022 16:54:27 +0100 Subject: [PATCH 049/100] Use estimate_observables function --- qiskit/algorithms/minimum_eigensolvers/vqe.py | 9 +- .../test_numpy_minimum_eigensolver.py | 8 +- .../minimum_eigensolvers/test_vqe.py | 148 ++++++++++-------- 3 files changed, 90 insertions(+), 75 deletions(-) diff --git a/qiskit/algorithms/minimum_eigensolvers/vqe.py b/qiskit/algorithms/minimum_eigensolvers/vqe.py index c699ab9feb73..0604aea03b20 100644 --- a/qiskit/algorithms/minimum_eigensolvers/vqe.py +++ b/qiskit/algorithms/minimum_eigensolvers/vqe.py @@ -34,7 +34,7 @@ from ..variational_algorithm import VariationalAlgorithm, VariationalResult from .minimum_eigensolver import MinimumEigensolver, MinimumEigensolverResult -# from qiskit.algorithms.observables_evaluator import eval_observables +from ..observables_evaluator import estimate_observables logger = logging.getLogger(__name__) @@ -201,12 +201,9 @@ def compute_minimum_eigenvalue( result.optimal_point, ) - if aux_operators: - # not None and not empty list or dict + if aux_operators is not None: bound_ansatz = ansatz.bind_parameters(opt_result.x) - # aux_values = eval_observables(self.estimator, bound_ansatz, aux_operators) - # TODO remove once eval_operators have been ported. - aux_values = self._eval_aux_ops(bound_ansatz, aux_operators) + aux_values = estimate_observables(self.estimator, bound_ansatz, aux_operators) result.aux_operator_eigenvalues = aux_values return result diff --git a/test/python/algorithms/minimum_eigensolvers/test_numpy_minimum_eigensolver.py b/test/python/algorithms/minimum_eigensolvers/test_numpy_minimum_eigensolver.py index 2ae7edb68803..45d5d48c38c6 100644 --- a/test/python/algorithms/minimum_eigensolvers/test_numpy_minimum_eigensolver.py +++ b/test/python/algorithms/minimum_eigensolvers/test_numpy_minimum_eigensolver.py @@ -19,7 +19,8 @@ from ddt import ddt, data from qiskit.algorithms.minimum_eigensolvers import NumPyMinimumEigensolver -from qiskit.opflow import PauliSumOp, X, Y, Z +from qiskit.opflow import PauliSumOp +from qiskit.quantum_info import SparsePauliOp @ddt @@ -125,11 +126,12 @@ def criterion(x, v, a_v): self.assertEqual(result.eigenstate, None) self.assertEqual(result.aux_operator_eigenvalues, None) - @data(X, Y, Z) + @data("X", "Y", "Z") def test_cme_1q(self, op): """Test for 1 qubit operator""" algo = NumPyMinimumEigensolver() - result = algo.compute_minimum_eigenvalue(operator=op) + operator = PauliSumOp.from_list([(op, 1.0)]) + result = algo.compute_minimum_eigenvalue(operator=operator) self.assertAlmostEqual(result.eigenvalue, -1) def test_cme_aux_ops_dict(self): diff --git a/test/python/algorithms/minimum_eigensolvers/test_vqe.py b/test/python/algorithms/minimum_eigensolvers/test_vqe.py index af32c2d19a33..b23ff5ae90ef 100644 --- a/test/python/algorithms/minimum_eigensolvers/test_vqe.py +++ b/test/python/algorithms/minimum_eigensolvers/test_vqe.py @@ -363,77 +363,93 @@ def test_aux_operators_list(self): """Test list-based aux_operators.""" vqe = VQE(Estimator(), self.ry_wavefunction, SLSQP(maxiter=300)) - # Start with an empty list - result = vqe.compute_minimum_eigenvalue(self.h2_op, aux_operators=[]) - self.assertAlmostEqual(result.eigenvalue.real, self.h2_energy, places=6) - self.assertIsNone(result.aux_operator_eigenvalues) - - # Go again with two auxiliary operators - aux_op1 = PauliSumOp.from_list([("II", 2.0)]) - aux_op2 = PauliSumOp.from_list([("II", 0.5), ("ZZ", 0.5), ("YY", 0.5), ("XX", -0.5)]) - aux_ops = [aux_op1, aux_op2] - result = vqe.compute_minimum_eigenvalue(self.h2_op, aux_operators=aux_ops) - self.assertAlmostEqual(result.eigenvalue.real, self.h2_energy, places=6) - self.assertEqual(len(result.aux_operator_eigenvalues), 2) - # expectation values - self.assertAlmostEqual(result.aux_operator_eigenvalues[0][0], 2, places=6) - self.assertAlmostEqual(result.aux_operator_eigenvalues[1][0], 0, places=6) - # standard deviations - self.assertAlmostEqual(result.aux_operator_eigenvalues[0][1], 0.0) - self.assertAlmostEqual(result.aux_operator_eigenvalues[1][1], 0.0) - - # Go again with additional zero operator - # TODO waiting for eval_operators to be ported. - # extra_ops = [*aux_ops, 0] - # result = vqe.compute_minimum_eigenvalue(self.h2_op, aux_operators=extra_ops) - # self.assertAlmostEqual(result.eigenvalue.real, self.h2_energy, places=6) - # self.assertEqual(len(result.aux_operator_eigenvalues), 3) - # # # expectation values - # self.assertAlmostEqual(result.aux_operator_eigenvalues[0][0], 2, places=6) - # self.assertAlmostEqual(result.aux_operator_eigenvalues[1][0], 0, places=6) - # self.assertEqual(result.aux_operator_eigenvalues[2][0], 0.0) - # # # standard deviations - # self.assertAlmostEqual(result.aux_operator_eigenvalues[0][1], 0.0) - # self.assertAlmostEqual(result.aux_operator_eigenvalues[1][1], 0.0) - # self.assertAlmostEqual(result.aux_operator_eigenvalues[2][1], 0.0) + with self.subTest("Test with an empty list."): + result = vqe.compute_minimum_eigenvalue(self.h2_op, aux_operators=[]) + self.assertAlmostEqual(result.eigenvalue.real, self.h2_energy, places=6) + self.assertIsInstance(result.aux_operator_eigenvalues, list) + self.assertEqual(len(result.aux_operator_eigenvalues), 0) + + with self.subTest("Test with two auxiliary operators."): + aux_op1 = PauliSumOp.from_list([("II", 2.0)]) + aux_op2 = PauliSumOp.from_list([("II", 0.5), ("ZZ", 0.5), ("YY", 0.5), ("XX", -0.5)]) + aux_ops = [aux_op1, aux_op2] + result = vqe.compute_minimum_eigenvalue(self.h2_op, aux_operators=aux_ops) + self.assertAlmostEqual(result.eigenvalue.real, self.h2_energy, places=6) + self.assertEqual(len(result.aux_operator_eigenvalues), 2) + # expectation values + self.assertAlmostEqual(result.aux_operator_eigenvalues[0][0], 2.0, places=6) + self.assertAlmostEqual(result.aux_operator_eigenvalues[1][0], 0.0, places=6) + # variances + self.assertEqual(result.aux_operator_eigenvalues[0][1][0], 0.0) + self.assertEqual(result.aux_operator_eigenvalues[1][1][0], 0.0) + # shots + self.assertEqual(result.aux_operator_eigenvalues[0][1][1], 0) + self.assertEqual(result.aux_operator_eigenvalues[1][1][1], 0) + + with self.subTest("Test with additional zero operator."): + extra_ops = [*aux_ops, 0] + result = vqe.compute_minimum_eigenvalue(self.h2_op, aux_operators=extra_ops) + self.assertAlmostEqual(result.eigenvalue.real, self.h2_energy, places=6) + self.assertEqual(len(result.aux_operator_eigenvalues), 3) + # expectation values + self.assertAlmostEqual(result.aux_operator_eigenvalues[0][0], 2.0, places=6) + self.assertAlmostEqual(result.aux_operator_eigenvalues[1][0], 0.0, places=6) + self.assertAlmostEqual(result.aux_operator_eigenvalues[2][0], 0.0) + # variances + self.assertEqual(result.aux_operator_eigenvalues[0][1][0], 0.0) + self.assertEqual(result.aux_operator_eigenvalues[1][1][0], 0.0) + self.assertEqual(result.aux_operator_eigenvalues[2][1][0], 0.0) + # shots + self.assertEqual(result.aux_operator_eigenvalues[0][1][1], 0) + self.assertEqual(result.aux_operator_eigenvalues[1][1][1], 0) + self.assertEqual(result.aux_operator_eigenvalues[2][1][1], 0) def test_aux_operators_dict(self): """Test dictionary compatibility of aux_operators""" vqe = VQE(Estimator(), self.ry_wavefunction, SLSQP(maxiter=300)) - # Start with an empty dictionary - result = vqe.compute_minimum_eigenvalue(self.h2_op, aux_operators={}) - self.assertAlmostEqual(result.eigenvalue.real, self.h2_energy, places=6) - self.assertIsNone(result.aux_operator_eigenvalues) - - # Go again with two auxiliary operators - aux_op1 = PauliSumOp.from_list([("II", 2.0)]) - aux_op2 = PauliSumOp.from_list([("II", 0.5), ("ZZ", 0.5), ("YY", 0.5), ("XX", -0.5)]) - aux_ops = {"aux_op1": aux_op1, "aux_op2": aux_op2} - result = vqe.compute_minimum_eigenvalue(self.h2_op, aux_operators=aux_ops) - self.assertAlmostEqual(result.eigenvalue.real, self.h2_energy, places=6) - self.assertEqual(len(result.aux_operator_eigenvalues), 2) - # expectation values - self.assertAlmostEqual(result.aux_operator_eigenvalues["aux_op1"][0], 2, places=5) - self.assertAlmostEqual(result.aux_operator_eigenvalues["aux_op2"][0], 0, places=5) - # standard deviations - self.assertAlmostEqual(result.aux_operator_eigenvalues["aux_op1"][1], 0.0) - self.assertAlmostEqual(result.aux_operator_eigenvalues["aux_op2"][1], 0.0) - - # TODO waiting for eval_operators to be ported. - # Go again with additional zero operator - # extra_ops = {**aux_ops, "zero_operator": 0} - # result = vqe.compute_minimum_eigenvalue(self.h2_op, aux_operators=extra_ops) - # self.assertAlmostEqual(result.eigenvalue.real, self.h2_energy, places=6) - # self.assertEqual(len(result.aux_operator_eigenvalues), 3) - # # expectation values - # self.assertAlmostEqual(result.aux_operator_eigenvalues["aux_op1"][0], 2, places=5) - # self.assertAlmostEqual(result.aux_operator_eigenvalues["aux_op2"][0], 0, places=5) - # self.assertEqual(result.aux_operator_eigenvalues["zero_operator"][0], 0.0) - # # standard deviations - # self.assertAlmostEqual(result.aux_operator_eigenvalues["aux_op1"][1], 0.0) - # self.assertAlmostEqual(result.aux_operator_eigenvalues["aux_op2"][1], 0.0) - # self.assertAlmostEqual(result.aux_operator_eigenvalues["zero_operator"][1], 0.0) + with self.subTest("Test with an empty dictionary."): + result = vqe.compute_minimum_eigenvalue(self.h2_op, aux_operators={}) + self.assertAlmostEqual(result.eigenvalue.real, self.h2_energy, places=6) + # self.assertIsNone(result.aux_operator_eigenvalues) + self.assertIsInstance(result.aux_operator_eigenvalues, dict) + self.assertEqual(len(result.aux_operator_eigenvalues), 0) + + with self.subTest("Test with two auxiliary operators."): + aux_op1 = PauliSumOp.from_list([("II", 2.0)]) + aux_op2 = PauliSumOp.from_list([("II", 0.5), ("ZZ", 0.5), ("YY", 0.5), ("XX", -0.5)]) + aux_ops = {"aux_op1": aux_op1, "aux_op2": aux_op2} + result = vqe.compute_minimum_eigenvalue(self.h2_op, aux_operators=aux_ops) + self.assertAlmostEqual(result.eigenvalue.real, self.h2_energy, places=6) + self.assertEqual(len(result.aux_operator_eigenvalues), 2) + + # expectation values + self.assertAlmostEqual(result.aux_operator_eigenvalues["aux_op1"][0], 2, places=5) + self.assertAlmostEqual(result.aux_operator_eigenvalues["aux_op2"][0], 0, places=5) + # variances + self.assertEqual(result.aux_operator_eigenvalues["aux_op1"][1][0], 0.0) + self.assertEqual(result.aux_operator_eigenvalues["aux_op2"][1][0], 0.0) + # shots + self.assertEqual(result.aux_operator_eigenvalues["aux_op1"][1][1], 0) + self.assertEqual(result.aux_operator_eigenvalues["aux_op2"][1][1], 0) + + with self.subTest("Test with additional zero operator."): + extra_ops = {**aux_ops, "zero_operator": 0} + result = vqe.compute_minimum_eigenvalue(self.h2_op, aux_operators=extra_ops) + self.assertAlmostEqual(result.eigenvalue.real, self.h2_energy, places=6) + self.assertEqual(len(result.aux_operator_eigenvalues), 3) + # expectation values + self.assertAlmostEqual(result.aux_operator_eigenvalues["aux_op1"][0], 2, places=5) + self.assertAlmostEqual(result.aux_operator_eigenvalues["aux_op2"][0], 0, places=5) + self.assertAlmostEqual(result.aux_operator_eigenvalues["zero_operator"][0], 0.0) + # variances + self.assertEqual(result.aux_operator_eigenvalues["aux_op1"][1][0], 0.0) + self.assertEqual(result.aux_operator_eigenvalues["aux_op2"][1][0], 0.0) + self.assertEqual(result.aux_operator_eigenvalues["zero_operator"][1][0], 0.0) + # shots + self.assertEqual(result.aux_operator_eigenvalues["aux_op1"][1][1], 0) + self.assertEqual(result.aux_operator_eigenvalues["aux_op2"][1][1], 0) + self.assertEqual(result.aux_operator_eigenvalues["zero_operator"][1][1], 0) if __name__ == "__main__": From 4cbbe67f0fb3d987d7c956004bb5a98a0b6245b9 Mon Sep 17 00:00:00 2001 From: Declan Millar Date: Tue, 20 Sep 2022 11:39:27 +0100 Subject: [PATCH 050/100] break up numpy mes tests with subTest --- .../test_numpy_minimum_eigensolver.py | 210 +++++++++--------- .../minimum_eigensolvers/test_vqe.py | 1 - 2 files changed, 108 insertions(+), 103 deletions(-) diff --git a/test/python/algorithms/minimum_eigensolvers/test_numpy_minimum_eigensolver.py b/test/python/algorithms/minimum_eigensolvers/test_numpy_minimum_eigensolver.py index 45d5d48c38c6..b744785df3ff 100644 --- a/test/python/algorithms/minimum_eigensolvers/test_numpy_minimum_eigensolver.py +++ b/test/python/algorithms/minimum_eigensolvers/test_numpy_minimum_eigensolver.py @@ -20,7 +20,6 @@ from qiskit.algorithms.minimum_eigensolvers import NumPyMinimumEigensolver from qiskit.opflow import PauliSumOp -from qiskit.quantum_info import SparsePauliOp @ddt @@ -57,41 +56,42 @@ def test_cme(self): def test_cme_reuse(self): """Test reuse""" - # Start with no operator or aux_operators, give via compute method algo = NumPyMinimumEigensolver() - result = algo.compute_minimum_eigenvalue(operator=self.qubit_op) - self.assertEqual(result.eigenvalue.dtype, np.float64) - self.assertAlmostEqual(result.eigenvalue, -1.85727503) - self.assertIsNone(result.aux_operator_eigenvalues) - # Add aux_operators and go again - result = algo.compute_minimum_eigenvalue( - operator=self.qubit_op, aux_operators=self.aux_ops_list - ) - self.assertAlmostEqual(result.eigenvalue, -1.85727503 + 0j) - self.assertEqual(len(result.aux_operator_eigenvalues), 2) - np.testing.assert_array_almost_equal(result.aux_operator_eigenvalues[0], [2, 0]) - np.testing.assert_array_almost_equal(result.aux_operator_eigenvalues[1], [0, 0]) - - # "Remove" aux_operators and go again - result = algo.compute_minimum_eigenvalue(operator=self.qubit_op, aux_operators=[]) - self.assertEqual(result.eigenvalue.dtype, np.float64) - self.assertAlmostEqual(result.eigenvalue, -1.85727503) - self.assertIsNone(result.aux_operator_eigenvalues) - - # Set aux_operators and go again - result = algo.compute_minimum_eigenvalue( - operator=self.qubit_op, aux_operators=self.aux_ops_list - ) - self.assertAlmostEqual(result.eigenvalue, -1.85727503 + 0j) - self.assertEqual(len(result.aux_operator_eigenvalues), 2) - np.testing.assert_array_almost_equal(result.aux_operator_eigenvalues[0], [2, 0]) - np.testing.assert_array_almost_equal(result.aux_operator_eigenvalues[1], [0, 0]) - - # Finally just set one of aux_operators and main operator, remove aux_operators - result = algo.compute_minimum_eigenvalue(operator=self.aux_ops_list[0], aux_operators=[]) - self.assertAlmostEqual(result.eigenvalue, 2 + 0j) - self.assertIsNone(result.aux_operator_eigenvalues) + with self.subTest("Test with no operator or aux_operators, give via compute method"): + result = algo.compute_minimum_eigenvalue(operator=self.qubit_op) + self.assertEqual(result.eigenvalue.dtype, np.float64) + self.assertAlmostEqual(result.eigenvalue, -1.85727503) + self.assertIsNone(result.aux_operator_eigenvalues) + + with self.subTest("Test with added aux_operators"): + result = algo.compute_minimum_eigenvalue( + operator=self.qubit_op, aux_operators=self.aux_ops_list + ) + self.assertAlmostEqual(result.eigenvalue, -1.85727503 + 0j) + self.assertEqual(len(result.aux_operator_eigenvalues), 2) + np.testing.assert_array_almost_equal(result.aux_operator_eigenvalues[0], [2, 0]) + np.testing.assert_array_almost_equal(result.aux_operator_eigenvalues[1], [0, 0]) + + with self.subTest("Test with aux_operators removed"): + result = algo.compute_minimum_eigenvalue(operator=self.qubit_op, aux_operators=[]) + self.assertEqual(result.eigenvalue.dtype, np.float64) + self.assertAlmostEqual(result.eigenvalue, -1.85727503) + self.assertIsNone(result.aux_operator_eigenvalues) + + with self.subTest("Test with aux_operators set again"): + result = algo.compute_minimum_eigenvalue( + operator=self.qubit_op, aux_operators=self.aux_ops_list + ) + self.assertAlmostEqual(result.eigenvalue, -1.85727503 + 0j) + self.assertEqual(len(result.aux_operator_eigenvalues), 2) + np.testing.assert_array_almost_equal(result.aux_operator_eigenvalues[0], [2, 0]) + np.testing.assert_array_almost_equal(result.aux_operator_eigenvalues[1], [0, 0]) + + with self.subTest("Test after setting first aux_operators as main operator"): + result = algo.compute_minimum_eigenvalue(operator=self.aux_ops_list[0], aux_operators=[]) + self.assertAlmostEqual(result.eigenvalue, 2 + 0j) + self.assertIsNone(result.aux_operator_eigenvalues) def test_cme_filter(self): """Basic test""" @@ -138,27 +138,29 @@ def test_cme_aux_ops_dict(self): """Test dictionary compatibility of aux_operators""" # Start with an empty dictionary algo = NumPyMinimumEigensolver() - result = algo.compute_minimum_eigenvalue(operator=self.qubit_op, aux_operators={}) - self.assertAlmostEqual(result.eigenvalue, -1.85727503 + 0j) - self.assertIsNone(result.aux_operator_eigenvalues) - - # Add aux_operators dictionary and go again - result = algo.compute_minimum_eigenvalue( - operator=self.qubit_op, aux_operators=self.aux_ops_dict - ) - self.assertAlmostEqual(result.eigenvalue, -1.85727503 + 0j) - self.assertEqual(len(result.aux_operator_eigenvalues), 2) - np.testing.assert_array_almost_equal(result.aux_operator_eigenvalues["aux_op1"], [2, 0]) - np.testing.assert_array_almost_equal(result.aux_operator_eigenvalues["aux_op2"], [0, 0]) - # Add None and zero operators and go again - extra_ops = {"None_op": None, "zero_op": 0, **self.aux_ops_dict} - result = algo.compute_minimum_eigenvalue(operator=self.qubit_op, aux_operators=extra_ops) - self.assertAlmostEqual(result.eigenvalue, -1.85727503 + 0j) - self.assertEqual(len(result.aux_operator_eigenvalues), 3) - np.testing.assert_array_almost_equal(result.aux_operator_eigenvalues["aux_op1"], [2, 0]) - np.testing.assert_array_almost_equal(result.aux_operator_eigenvalues["aux_op2"], [0, 0]) - self.assertEqual(result.aux_operator_eigenvalues["zero_op"], (0.0, 0)) + with self.subTest("Test with an empty dictionary."): + result = algo.compute_minimum_eigenvalue(operator=self.qubit_op, aux_operators={}) + self.assertAlmostEqual(result.eigenvalue, -1.85727503 + 0j) + self.assertIsNone(result.aux_operator_eigenvalues) + + with self.subTest("Test with two auxiliary operators."): + result = algo.compute_minimum_eigenvalue( + operator=self.qubit_op, aux_operators=self.aux_ops_dict + ) + self.assertAlmostEqual(result.eigenvalue, -1.85727503 + 0j) + self.assertEqual(len(result.aux_operator_eigenvalues), 2) + np.testing.assert_array_almost_equal(result.aux_operator_eigenvalues["aux_op1"], [2, 0]) + np.testing.assert_array_almost_equal(result.aux_operator_eigenvalues["aux_op2"], [0, 0]) + + with self.subTest("Test with additional zero and None operators."): + extra_ops = {"None_op": None, "zero_op": 0, **self.aux_ops_dict} + result = algo.compute_minimum_eigenvalue(operator=self.qubit_op, aux_operators=extra_ops) + self.assertAlmostEqual(result.eigenvalue, -1.85727503 + 0j) + self.assertEqual(len(result.aux_operator_eigenvalues), 3) + np.testing.assert_array_almost_equal(result.aux_operator_eigenvalues["aux_op1"], [2, 0]) + np.testing.assert_array_almost_equal(result.aux_operator_eigenvalues["aux_op2"], [0, 0]) + self.assertEqual(result.aux_operator_eigenvalues["zero_op"], (0.0, 0)) def test_aux_operators_list(self): """Test list-based aux_operators.""" @@ -166,30 +168,32 @@ def test_aux_operators_list(self): aux_op2 = PauliSumOp.from_list([("II", 0.5), ("ZZ", 0.5), ("YY", 0.5), ("XX", -0.5)]) aux_ops = [aux_op1, aux_op2] algo = NumPyMinimumEigensolver() - result = algo.compute_minimum_eigenvalue(operator=self.qubit_op, aux_operators=aux_ops) - self.assertAlmostEqual(result.eigenvalue, -1.85727503 + 0j) - self.assertEqual(len(result.aux_operator_eigenvalues), 2) - # expectation values - self.assertAlmostEqual(result.aux_operator_eigenvalues[0][0], 2, places=6) - self.assertAlmostEqual(result.aux_operator_eigenvalues[1][0], 0, places=6) - # standard deviations - self.assertAlmostEqual(result.aux_operator_eigenvalues[0][1], 0.0) - self.assertAlmostEqual(result.aux_operator_eigenvalues[1][1], 0.0) - - # Go again with additional None and zero operators - extra_ops = [*aux_ops, None, 0] - result = algo.compute_minimum_eigenvalue(operator=self.qubit_op, aux_operators=extra_ops) - self.assertAlmostEqual(result.eigenvalue, -1.85727503 + 0j) - self.assertEqual(len(result.aux_operator_eigenvalues), 4) - # expectation values - self.assertAlmostEqual(result.aux_operator_eigenvalues[0][0], 2, places=6) - self.assertAlmostEqual(result.aux_operator_eigenvalues[1][0], 0, places=6) - self.assertIsNone(result.aux_operator_eigenvalues[2], None) - self.assertEqual(result.aux_operator_eigenvalues[3][0], 0.0) - # standard deviations - self.assertAlmostEqual(result.aux_operator_eigenvalues[0][1], 0.0) - self.assertAlmostEqual(result.aux_operator_eigenvalues[1][1], 0.0) - self.assertEqual(result.aux_operator_eigenvalues[3][1], 0.0) + + with self.subTest("Test with two auxiliary operators."): + result = algo.compute_minimum_eigenvalue(operator=self.qubit_op, aux_operators=aux_ops) + self.assertAlmostEqual(result.eigenvalue, -1.85727503 + 0j) + self.assertEqual(len(result.aux_operator_eigenvalues), 2) + # expectation values + self.assertAlmostEqual(result.aux_operator_eigenvalues[0][0], 2, places=6) + self.assertAlmostEqual(result.aux_operator_eigenvalues[1][0], 0, places=6) + # standard deviations + self.assertAlmostEqual(result.aux_operator_eigenvalues[0][1], 0.0) + self.assertAlmostEqual(result.aux_operator_eigenvalues[1][1], 0.0) + + with self.subTest("Test with additional zero and None operators."): + extra_ops = [*aux_ops, None, 0] + result = algo.compute_minimum_eigenvalue(operator=self.qubit_op, aux_operators=extra_ops) + self.assertAlmostEqual(result.eigenvalue, -1.85727503 + 0j) + self.assertEqual(len(result.aux_operator_eigenvalues), 4) + # expectation values + self.assertAlmostEqual(result.aux_operator_eigenvalues[0][0], 2, places=6) + self.assertAlmostEqual(result.aux_operator_eigenvalues[1][0], 0, places=6) + self.assertIsNone(result.aux_operator_eigenvalues[2], None) + self.assertEqual(result.aux_operator_eigenvalues[3][0], 0.0) + # standard deviations + self.assertAlmostEqual(result.aux_operator_eigenvalues[0][1], 0.0) + self.assertAlmostEqual(result.aux_operator_eigenvalues[1][1], 0.0) + self.assertEqual(result.aux_operator_eigenvalues[3][1], 0.0) def test_aux_operators_dict(self): """Test dict-based aux_operators.""" @@ -197,30 +201,32 @@ def test_aux_operators_dict(self): aux_op2 = PauliSumOp.from_list([("II", 0.5), ("ZZ", 0.5), ("YY", 0.5), ("XX", -0.5)]) aux_ops = {"aux_op1": aux_op1, "aux_op2": aux_op2} algo = NumPyMinimumEigensolver() - result = algo.compute_minimum_eigenvalue(operator=self.qubit_op, aux_operators=aux_ops) - self.assertAlmostEqual(result.eigenvalue, -1.85727503 + 0j) - self.assertEqual(len(result.aux_operator_eigenvalues), 2) - # expectation values - self.assertAlmostEqual(result.aux_operator_eigenvalues["aux_op1"][0], 2, places=6) - self.assertAlmostEqual(result.aux_operator_eigenvalues["aux_op2"][0], 0, places=6) - # standard deviations - self.assertAlmostEqual(result.aux_operator_eigenvalues["aux_op1"][1], 0.0) - self.assertAlmostEqual(result.aux_operator_eigenvalues["aux_op2"][1], 0.0) - - # Go again with additional None and zero operators - extra_ops = {**aux_ops, "None_operator": None, "zero_operator": 0} - result = algo.compute_minimum_eigenvalue(operator=self.qubit_op, aux_operators=extra_ops) - self.assertAlmostEqual(result.eigenvalue, -1.85727503 + 0j) - self.assertEqual(len(result.aux_operator_eigenvalues), 3) - # expectation values - self.assertAlmostEqual(result.aux_operator_eigenvalues["aux_op1"][0], 2, places=6) - self.assertAlmostEqual(result.aux_operator_eigenvalues["aux_op2"][0], 0, places=6) - self.assertEqual(result.aux_operator_eigenvalues["zero_operator"][0], 0.0) - self.assertTrue("None_operator" not in result.aux_operator_eigenvalues.keys()) - # standard deviations - self.assertAlmostEqual(result.aux_operator_eigenvalues["aux_op1"][1], 0.0) - self.assertAlmostEqual(result.aux_operator_eigenvalues["aux_op2"][1], 0.0) - self.assertAlmostEqual(result.aux_operator_eigenvalues["zero_operator"][1], 0.0) + + with self.subTest("Test with two auxiliary operators."): + result = algo.compute_minimum_eigenvalue(operator=self.qubit_op, aux_operators=aux_ops) + self.assertAlmostEqual(result.eigenvalue, -1.85727503 + 0j) + self.assertEqual(len(result.aux_operator_eigenvalues), 2) + # expectation values + self.assertAlmostEqual(result.aux_operator_eigenvalues["aux_op1"][0], 2, places=6) + self.assertAlmostEqual(result.aux_operator_eigenvalues["aux_op2"][0], 0, places=6) + # standard deviations + self.assertAlmostEqual(result.aux_operator_eigenvalues["aux_op1"][1], 0.0) + self.assertAlmostEqual(result.aux_operator_eigenvalues["aux_op2"][1], 0.0) + + with self.subTest("Test with additional zero and None operators."): + extra_ops = {**aux_ops, "None_operator": None, "zero_operator": 0} + result = algo.compute_minimum_eigenvalue(operator=self.qubit_op, aux_operators=extra_ops) + self.assertAlmostEqual(result.eigenvalue, -1.85727503 + 0j) + self.assertEqual(len(result.aux_operator_eigenvalues), 3) + # expectation values + self.assertAlmostEqual(result.aux_operator_eigenvalues["aux_op1"][0], 2, places=6) + self.assertAlmostEqual(result.aux_operator_eigenvalues["aux_op2"][0], 0, places=6) + self.assertEqual(result.aux_operator_eigenvalues["zero_operator"][0], 0.0) + self.assertTrue("None_operator" not in result.aux_operator_eigenvalues.keys()) + # standard deviations + self.assertAlmostEqual(result.aux_operator_eigenvalues["aux_op1"][1], 0.0) + self.assertAlmostEqual(result.aux_operator_eigenvalues["aux_op2"][1], 0.0) + self.assertAlmostEqual(result.aux_operator_eigenvalues["zero_operator"][1], 0.0) if __name__ == "__main__": diff --git a/test/python/algorithms/minimum_eigensolvers/test_vqe.py b/test/python/algorithms/minimum_eigensolvers/test_vqe.py index b23ff5ae90ef..6a29514aed7d 100644 --- a/test/python/algorithms/minimum_eigensolvers/test_vqe.py +++ b/test/python/algorithms/minimum_eigensolvers/test_vqe.py @@ -411,7 +411,6 @@ def test_aux_operators_dict(self): with self.subTest("Test with an empty dictionary."): result = vqe.compute_minimum_eigenvalue(self.h2_op, aux_operators={}) self.assertAlmostEqual(result.eigenvalue.real, self.h2_energy, places=6) - # self.assertIsNone(result.aux_operator_eigenvalues) self.assertIsInstance(result.aux_operator_eigenvalues, dict) self.assertEqual(len(result.aux_operator_eigenvalues), 0) From 455981cd8e8f7c24f41cf6287ff41f887e12f121 Mon Sep 17 00:00:00 2001 From: Declan Millar Date: Tue, 20 Sep 2022 11:44:19 +0100 Subject: [PATCH 051/100] formatting --- .../test_numpy_minimum_eigensolver.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/test/python/algorithms/minimum_eigensolvers/test_numpy_minimum_eigensolver.py b/test/python/algorithms/minimum_eigensolvers/test_numpy_minimum_eigensolver.py index b744785df3ff..3201712ff0b6 100644 --- a/test/python/algorithms/minimum_eigensolvers/test_numpy_minimum_eigensolver.py +++ b/test/python/algorithms/minimum_eigensolvers/test_numpy_minimum_eigensolver.py @@ -89,7 +89,9 @@ def test_cme_reuse(self): np.testing.assert_array_almost_equal(result.aux_operator_eigenvalues[1], [0, 0]) with self.subTest("Test after setting first aux_operators as main operator"): - result = algo.compute_minimum_eigenvalue(operator=self.aux_ops_list[0], aux_operators=[]) + result = algo.compute_minimum_eigenvalue( + operator=self.aux_ops_list[0], aux_operators=[] + ) self.assertAlmostEqual(result.eigenvalue, 2 + 0j) self.assertIsNone(result.aux_operator_eigenvalues) @@ -155,7 +157,9 @@ def test_cme_aux_ops_dict(self): with self.subTest("Test with additional zero and None operators."): extra_ops = {"None_op": None, "zero_op": 0, **self.aux_ops_dict} - result = algo.compute_minimum_eigenvalue(operator=self.qubit_op, aux_operators=extra_ops) + result = algo.compute_minimum_eigenvalue( + operator=self.qubit_op, aux_operators=extra_ops + ) self.assertAlmostEqual(result.eigenvalue, -1.85727503 + 0j) self.assertEqual(len(result.aux_operator_eigenvalues), 3) np.testing.assert_array_almost_equal(result.aux_operator_eigenvalues["aux_op1"], [2, 0]) @@ -182,7 +186,9 @@ def test_aux_operators_list(self): with self.subTest("Test with additional zero and None operators."): extra_ops = [*aux_ops, None, 0] - result = algo.compute_minimum_eigenvalue(operator=self.qubit_op, aux_operators=extra_ops) + result = algo.compute_minimum_eigenvalue( + operator=self.qubit_op, aux_operators=extra_ops + ) self.assertAlmostEqual(result.eigenvalue, -1.85727503 + 0j) self.assertEqual(len(result.aux_operator_eigenvalues), 4) # expectation values @@ -215,7 +221,9 @@ def test_aux_operators_dict(self): with self.subTest("Test with additional zero and None operators."): extra_ops = {**aux_ops, "None_operator": None, "zero_operator": 0} - result = algo.compute_minimum_eigenvalue(operator=self.qubit_op, aux_operators=extra_ops) + result = algo.compute_minimum_eigenvalue( + operator=self.qubit_op, aux_operators=extra_ops + ) self.assertAlmostEqual(result.eigenvalue, -1.85727503 + 0j) self.assertEqual(len(result.aux_operator_eigenvalues), 3) # expectation values From ae66409792342b281f6642dcba382c546ef2e365 Mon Sep 17 00:00:00 2001 From: Declan Millar Date: Tue, 20 Sep 2022 11:45:27 +0100 Subject: [PATCH 052/100] remove redundant eval_aux_ops --- qiskit/algorithms/minimum_eigensolvers/vqe.py | 22 ------------------- 1 file changed, 22 deletions(-) diff --git a/qiskit/algorithms/minimum_eigensolvers/vqe.py b/qiskit/algorithms/minimum_eigensolvers/vqe.py index 0604aea03b20..71ca1855ef51 100644 --- a/qiskit/algorithms/minimum_eigensolvers/vqe.py +++ b/qiskit/algorithms/minimum_eigensolvers/vqe.py @@ -314,28 +314,6 @@ def _check_operator_ansatz(self, operator: BaseOperator | PauliSumOp) -> Quantum return ansatz - def _eval_aux_ops( - self, - ansatz: QuantumCircuit, - aux_operators: ListOrDict[BaseOperator | PauliSumOp], - ) -> ListOrDict[tuple(complex, complex)]: - """Compute auxiliary operator eigenvalues.""" - - if isinstance(aux_operators, dict): - aux_ops = list(aux_operators.values()) - else: - aux_ops = aux_operators - - num_aux_ops = len(aux_ops) - aux_job = self.estimator.run([ansatz] * num_aux_ops, aux_ops) - aux_values = aux_job.result().values - aux_values = list(zip(aux_values, [0] * len(aux_values))) - - if isinstance(aux_operators, dict): - aux_values = dict(zip(aux_operators.keys(), aux_values)) - - return aux_values - def _validate_initial_point(point: Sequence[float], ansatz: QuantumCircuit) -> Sequence[float]: expected_size = ansatz.num_parameters From 3c178622136e32b3995b6adfe32cfdcf757f1100 Mon Sep 17 00:00:00 2001 From: Declan Millar Date: Tue, 20 Sep 2022 11:59:09 +0100 Subject: [PATCH 053/100] add typehints to docstring attributes --- qiskit/algorithms/minimum_eigensolvers/vqe.py | 32 +++++++++++-------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/qiskit/algorithms/minimum_eigensolvers/vqe.py b/qiskit/algorithms/minimum_eigensolvers/vqe.py index 71ca1855ef51..9eb648c54978 100644 --- a/qiskit/algorithms/minimum_eigensolvers/vqe.py +++ b/qiskit/algorithms/minimum_eigensolvers/vqe.py @@ -86,21 +86,25 @@ def my_minimizer(fun, x0, jac=None, bounds=None) -> OptimizerResult: optimizer = partial(minimize, method="L-BFGS-B") Attributes: - estimator: The estimator primitive to compute the expectation value of the circuits. - ansatz: A parameterized circuit, preparing the ansatz for the wave function. If - provided with ``None``, this defaults to a :class:`.RealAmplitudes` circuit. - optimizer: A classical optimizer to find the minimum energy. This can either be a - Qiskit :class:`.Optimizer` or a callable implementing the :class:`.Minimizer` - protocol. - gradient: An optional estimator gradient to be used with the optimizer. - 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 + estimator (BaseEstimator): The estimator primitive to compute the expectation value of the + circuits. + ansatz (Quantum Circuit | None): A parameterized circuit, preparing the ansatz for the wave + function. If provided with ``None``, this defaults to a :class:`.RealAmplitudes` + circuit. + optimizer (Optimizer | Minimizer): A classical optimizer to find the minimum energy. This + can either be a Qiskit :class:`.Optimizer` or a callable implementing the + :class:`.Minimizer` protocol. + gradient (BaseEstimatorGradient | None): An optional estimator gradient to be used with the + optimizer. + initial_point (Sequence[float] | None): 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. - callback: A callback that can access the intermediate data during the optimization. - Four parameter values are passed to the callback as follows during each evaluation - by the optimizer for its current set of parameters as it works towards the minimum. - These are: the evaluation count, the optimizer parameters for the ansatz, the - evaluated mean and the evaluated standard deviation. + callback (Callable[[int, np.ndarray, float, float], None] | None): A callback that can + access the intermediate data during the optimization. Four parameter values are passed + to the callback as follows during each evaluation by the optimizer for its current set + of parameters as it works towards the minimum. These are: the evaluation count, the + optimizer parameters for the ansatz, the evaluated mean and the evaluated standard + deviation. References: [1] Peruzzo et al, "A variational eigenvalue solver on a quantum processor" From 067cd07ce5b1b7df38a39739f82a2657b9b35ce9 Mon Sep 17 00:00:00 2001 From: Declan Millar Date: Tue, 20 Sep 2022 12:41:24 +0100 Subject: [PATCH 054/100] remove usused imports --- qiskit/algorithms/minimum_eigensolvers/vqe.py | 2 +- test/python/algorithms/minimum_eigensolvers/test_vqe.py | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/qiskit/algorithms/minimum_eigensolvers/vqe.py b/qiskit/algorithms/minimum_eigensolvers/vqe.py index 9eb648c54978..f80f1828f467 100644 --- a/qiskit/algorithms/minimum_eigensolvers/vqe.py +++ b/qiskit/algorithms/minimum_eigensolvers/vqe.py @@ -30,7 +30,7 @@ from ..exceptions import AlgorithmError from ..list_or_dict import ListOrDict -from ..optimizers import Optimizer, Minimizer, SLSQP +from ..optimizers import Optimizer, Minimizer from ..variational_algorithm import VariationalAlgorithm, VariationalResult from .minimum_eigensolver import MinimumEigensolver, MinimumEigensolverResult diff --git a/test/python/algorithms/minimum_eigensolvers/test_vqe.py b/test/python/algorithms/minimum_eigensolvers/test_vqe.py index 6a29514aed7d..b8e98d280934 100644 --- a/test/python/algorithms/minimum_eigensolvers/test_vqe.py +++ b/test/python/algorithms/minimum_eigensolvers/test_vqe.py @@ -450,6 +450,8 @@ def test_aux_operators_dict(self): self.assertEqual(result.aux_operator_eigenvalues["aux_op2"][1][1], 0) self.assertEqual(result.aux_operator_eigenvalues["zero_operator"][1][1], 0) + # TODO test with non-zero metadata. Affected by PR #8105. + if __name__ == "__main__": unittest.main() From 3acf449d7e63d38cc970b02befc1dd0b41650d5e Mon Sep 17 00:00:00 2001 From: Declan Millar Date: Tue, 20 Sep 2022 13:32:41 +0100 Subject: [PATCH 055/100] remove default ansatz --- qiskit/algorithms/minimum_eigensolvers/vqe.py | 40 +++++++------------ .../minimum_eigensolvers/test_vqe.py | 6 --- 2 files changed, 15 insertions(+), 31 deletions(-) diff --git a/qiskit/algorithms/minimum_eigensolvers/vqe.py b/qiskit/algorithms/minimum_eigensolvers/vqe.py index f80f1828f467..24feca1a9f59 100644 --- a/qiskit/algorithms/minimum_eigensolvers/vqe.py +++ b/qiskit/algorithms/minimum_eigensolvers/vqe.py @@ -88,9 +88,8 @@ def my_minimizer(fun, x0, jac=None, bounds=None) -> OptimizerResult: Attributes: estimator (BaseEstimator): The estimator primitive to compute the expectation value of the circuits. - ansatz (Quantum Circuit | None): A parameterized circuit, preparing the ansatz for the wave - function. If provided with ``None``, this defaults to a :class:`.RealAmplitudes` - circuit. + ansatz (Quantum Circuit): A parameterized circuit, preparing the ansatz for the wave + function. optimizer (Optimizer | Minimizer): A classical optimizer to find the minimum energy. This can either be a Qiskit :class:`.Optimizer` or a callable implementing the :class:`.Minimizer` protocol. @@ -114,7 +113,7 @@ def my_minimizer(fun, x0, jac=None, bounds=None) -> OptimizerResult: def __init__( self, estimator: BaseEstimator, - ansatz: QuantumCircuit | None, + ansatz: QuantumCircuit, optimizer: Optimizer | Minimizer, *, gradient: BaseEstimatorGradient | None = None, @@ -124,8 +123,7 @@ def __init__( """ Args: estimator: The estimator primitive to compute the expectation value of the circuits. - ansatz: A parameterized circuit, preparing the ansatz for the wave function. If - provided with ``None``, this defaults to a :class:`.RealAmplitudes` circuit. + ansatz: A parameterized circuit, preparing the ansatz for the wave function. optimizer: A classical optimizer to find the minimum energy. This can either be a Qiskit :class:`.Optimizer` or a callable implementing the :class:`.Minimizer` protocol. @@ -164,18 +162,18 @@ def compute_minimum_eigenvalue( operator: BaseOperator | PauliSumOp, aux_operators: ListOrDict[BaseOperator | PauliSumOp] | None = None, ) -> VQEResult: - ansatz = self._check_operator_ansatz(operator) + self._check_operator_ansatz(operator) - initial_point = _validate_initial_point(self.initial_point, ansatz) + initial_point = _validate_initial_point(self.initial_point, self.ansatz) - bounds = _validate_bounds(ansatz) + bounds = _validate_bounds(self.ansatz) start_time = time() - evaluate_energy = self._get_evaluate_energy(ansatz, operator) + evaluate_energy = self._get_evaluate_energy(self.ansatz, operator) if self.gradient is not None: - evaluate_gradient = self._get_evalute_gradient(ansatz, operator) + evaluate_gradient = self._get_evalute_gradient(self.ansatz, operator) else: evaluate_gradient = None @@ -195,7 +193,7 @@ def compute_minimum_eigenvalue( result.eigenvalue = opt_result.fun result.cost_function_evals = opt_result.nfev result.optimal_point = opt_result.x - result.optimal_parameters = dict(zip(ansatz.parameters, opt_result.x)) + result.optimal_parameters = dict(zip(self.ansatz.parameters, opt_result.x)) result.optimal_value = opt_result.fun result.optimizer_time = eval_time @@ -206,7 +204,7 @@ def compute_minimum_eigenvalue( ) if aux_operators is not None: - bound_ansatz = ansatz.bind_parameters(opt_result.x) + bound_ansatz = self.ansatz.bind_parameters(opt_result.x) aux_values = estimate_observables(self.estimator, bound_ansatz, aux_operators) result.aux_operator_eigenvalues = aux_values @@ -290,22 +288,16 @@ def evaluate_gradient(parameters): return evaluate_gradient - def _check_operator_ansatz(self, operator: BaseOperator | PauliSumOp) -> QuantumCircuit: + def _check_operator_ansatz(self, operator: BaseOperator | PauliSumOp): """Check that the number of qubits of operator and ansatz match and that the ansatz is parameterized. """ - # set defaults - if self.ansatz is None: - ansatz = RealAmplitudes(num_qubits=operator.num_qubits) - else: - ansatz = self.ansatz - - if operator.num_qubits != ansatz.num_qubits: + if operator.num_qubits != self.ansatz.num_qubits: try: logger.info( "Trying to resize ansatz to match operator on %s qubits.", operator.num_qubits ) - ansatz.num_qubits = operator.num_qubits + self.ansatz.num_qubits = operator.num_qubits except AttributeError as error: raise AlgorithmError( "The number of qubits of the ansatz does not match the " @@ -313,11 +305,9 @@ def _check_operator_ansatz(self, operator: BaseOperator | PauliSumOp) -> Quantum "number of qubits using `num_qubits`." ) from error - if ansatz.num_parameters == 0: + if self.ansatz.num_parameters == 0: raise AlgorithmError("The ansatz must be parameterized, but has no free parameters.") - return ansatz - def _validate_initial_point(point: Sequence[float], ansatz: QuantumCircuit) -> Sequence[float]: expected_size = ansatz.num_parameters diff --git a/test/python/algorithms/minimum_eigensolvers/test_vqe.py b/test/python/algorithms/minimum_eigensolvers/test_vqe.py index b8e98d280934..fd281be4e893 100644 --- a/test/python/algorithms/minimum_eigensolvers/test_vqe.py +++ b/test/python/algorithms/minimum_eigensolvers/test_vqe.py @@ -101,12 +101,6 @@ def test_basic_aer_statevector(self, estimator): with self.subTest(msg="assert optimizer_time is set"): self.assertIsNotNone(result.optimizer_time) - def test_default_ansatz(self): - """Test default ansatz is set as expected.""" - vqe = VQE(Estimator(), None, SLSQP()) - result = vqe.compute_minimum_eigenvalue(operator=self.h2_op) - self.assertAlmostEqual(result.eigenvalue, self.h2_energy, places=5) - def test_invalid_initial_point(self): """Test the proper error is raised when the initial point has the wrong size.""" ansatz = self.ryrz_wavefunction From b3db99893e557263239de782214d006ba258fcd8 Mon Sep 17 00:00:00 2001 From: Declan Millar Date: Tue, 20 Sep 2022 16:31:31 +0100 Subject: [PATCH 056/100] remove usused imports --- qiskit/algorithms/minimum_eigensolvers/vqe.py | 1 - 1 file changed, 1 deletion(-) diff --git a/qiskit/algorithms/minimum_eigensolvers/vqe.py b/qiskit/algorithms/minimum_eigensolvers/vqe.py index 24feca1a9f59..cda76aebcb1a 100644 --- a/qiskit/algorithms/minimum_eigensolvers/vqe.py +++ b/qiskit/algorithms/minimum_eigensolvers/vqe.py @@ -22,7 +22,6 @@ from qiskit.algorithms.gradients import BaseEstimatorGradient from qiskit.circuit import QuantumCircuit -from qiskit.circuit.library import RealAmplitudes from qiskit.opflow import PauliSumOp from qiskit.primitives import BaseEstimator from qiskit.quantum_info.operators.base_operator import BaseOperator From 1990045ba0427f2a27658366b6cbe912a2742d72 Mon Sep 17 00:00:00 2001 From: Declan Millar Date: Tue, 20 Sep 2022 18:43:09 +0100 Subject: [PATCH 057/100] update typehints --- qiskit/algorithms/minimum_eigensolvers/vqe.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/qiskit/algorithms/minimum_eigensolvers/vqe.py b/qiskit/algorithms/minimum_eigensolvers/vqe.py index cda76aebcb1a..f562175f36a0 100644 --- a/qiskit/algorithms/minimum_eigensolvers/vqe.py +++ b/qiskit/algorithms/minimum_eigensolvers/vqe.py @@ -217,7 +217,7 @@ def _get_evaluate_energy( self, ansatz: QuantumCircuit, operator: BaseOperator | PauliSumOp, - ) -> tuple[Callable[[np.ndarray], float | list[float]], dict]: + ) -> Callable[[np.ndarray], np.ndarray | float]: """Returns a function handle to evaluate the energy at given parameters for the ansatz. This is the objective function to be passed to the optimizer that is used for evaluation. Args: @@ -233,7 +233,7 @@ def _get_evaluate_energy( # avoid creating an instance variable to remain stateless regarding results eval_count = 0 - def evaluate_energy(parameters): + def evaluate_energy(parameters: np.ndarray) -> np.ndarray | float: nonlocal eval_count # handle broadcasting: ensure parameters is of shape [array, array, ...] @@ -246,7 +246,7 @@ def evaluate_energy(parameters): except Exception as exc: raise AlgorithmError("The primitive job to evaluate the energy failed!") from exc - # TODO recover variance from estimator if has metadata has shots? + # TODO recover variance from estimator if metadata has shots? if self.callback is not None: for params, value in zip(parameters, values): @@ -275,7 +275,7 @@ def _get_evalute_gradient( AlgorithmError: If the primitive job to evaluate the gradient fails. """ - def evaluate_gradient(parameters): + def evaluate_gradient(parameters: np.ndarray) -> np.ndarray: # broadcasting not required for the estimator gradients try: job = self.gradient.run([ansatz], [operator], [parameters]) From 86e7b86c9f18f19f8be901389607bbee1cbc9f11 Mon Sep 17 00:00:00 2001 From: Declan Millar Date: Wed, 21 Sep 2022 10:59:22 +0100 Subject: [PATCH 058/100] avoid changing the original ansatz --- qiskit/algorithms/minimum_eigensolvers/vqe.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/qiskit/algorithms/minimum_eigensolvers/vqe.py b/qiskit/algorithms/minimum_eigensolvers/vqe.py index f562175f36a0..bc5233e8b76a 100644 --- a/qiskit/algorithms/minimum_eigensolvers/vqe.py +++ b/qiskit/algorithms/minimum_eigensolvers/vqe.py @@ -172,7 +172,7 @@ def compute_minimum_eigenvalue( evaluate_energy = self._get_evaluate_energy(self.ansatz, operator) if self.gradient is not None: - evaluate_gradient = self._get_evalute_gradient(self.ansatz, operator) + evaluate_gradient = self._get_evaluate_gradient(self.ansatz, operator) else: evaluate_gradient = None @@ -257,7 +257,7 @@ def evaluate_energy(parameters: np.ndarray) -> np.ndarray | float: return evaluate_energy - def _get_evalute_gradient( + def _get_evaluate_gradient( self, ansatz: QuantumCircuit, operator: BaseOperator | PauliSumOp, @@ -287,16 +287,19 @@ def evaluate_gradient(parameters: np.ndarray) -> np.ndarray: return evaluate_gradient - def _check_operator_ansatz(self, operator: BaseOperator | PauliSumOp): + def _check_operator_ansatz(self, operator: BaseOperator | PauliSumOp) -> QuantumCircuit: """Check that the number of qubits of operator and ansatz match and that the ansatz is parameterized. """ - if operator.num_qubits != self.ansatz.num_qubits: + # avoid changing the original ansatz as a user may not expect this behavior + ansatz = self.ansatz.copy() + + if operator.num_qubits != ansatz.num_qubits: try: logger.info( "Trying to resize ansatz to match operator on %s qubits.", operator.num_qubits ) - self.ansatz.num_qubits = operator.num_qubits + ansatz.num_qubits = operator.num_qubits except AttributeError as error: raise AlgorithmError( "The number of qubits of the ansatz does not match the " @@ -304,9 +307,11 @@ def _check_operator_ansatz(self, operator: BaseOperator | PauliSumOp): "number of qubits using `num_qubits`." ) from error - if self.ansatz.num_parameters == 0: + if ansatz.num_parameters == 0: raise AlgorithmError("The ansatz must be parameterized, but has no free parameters.") + return ansatz + def _validate_initial_point(point: Sequence[float], ansatz: QuantumCircuit) -> Sequence[float]: expected_size = ansatz.num_parameters From bd0c97dc423d51b80e590977a58430066969b1b3 Mon Sep 17 00:00:00 2001 From: Declan Millar Date: Wed, 21 Sep 2022 11:20:27 +0100 Subject: [PATCH 059/100] avoid changing the original ansatz --- qiskit/algorithms/minimum_eigensolvers/vqe.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/qiskit/algorithms/minimum_eigensolvers/vqe.py b/qiskit/algorithms/minimum_eigensolvers/vqe.py index bc5233e8b76a..1776091ae30e 100644 --- a/qiskit/algorithms/minimum_eigensolvers/vqe.py +++ b/qiskit/algorithms/minimum_eigensolvers/vqe.py @@ -161,18 +161,18 @@ def compute_minimum_eigenvalue( operator: BaseOperator | PauliSumOp, aux_operators: ListOrDict[BaseOperator | PauliSumOp] | None = None, ) -> VQEResult: - self._check_operator_ansatz(operator) + ansatz = self._check_operator_ansatz(operator) - initial_point = _validate_initial_point(self.initial_point, self.ansatz) + initial_point = _validate_initial_point(self.initial_point, ansatz) - bounds = _validate_bounds(self.ansatz) + bounds = _validate_bounds(ansatz) start_time = time() - evaluate_energy = self._get_evaluate_energy(self.ansatz, operator) + evaluate_energy = self._get_evaluate_energy(ansatz, operator) if self.gradient is not None: - evaluate_gradient = self._get_evaluate_gradient(self.ansatz, operator) + evaluate_gradient = self._get_evaluate_gradient(ansatz, operator) else: evaluate_gradient = None @@ -192,7 +192,7 @@ def compute_minimum_eigenvalue( result.eigenvalue = opt_result.fun result.cost_function_evals = opt_result.nfev result.optimal_point = opt_result.x - result.optimal_parameters = dict(zip(self.ansatz.parameters, opt_result.x)) + result.optimal_parameters = dict(zip(ansatz.parameters, opt_result.x)) result.optimal_value = opt_result.fun result.optimizer_time = eval_time @@ -203,7 +203,7 @@ def compute_minimum_eigenvalue( ) if aux_operators is not None: - bound_ansatz = self.ansatz.bind_parameters(opt_result.x) + bound_ansatz = ansatz.bind_parameters(opt_result.x) aux_values = estimate_observables(self.estimator, bound_ansatz, aux_operators) result.aux_operator_eigenvalues = aux_values From 7cdd399803ea677e2a340d78f1fbda1de1e98f4f Mon Sep 17 00:00:00 2001 From: Declan Millar Date: Wed, 21 Sep 2022 11:36:23 +0100 Subject: [PATCH 060/100] create separate function to build vqe result --- qiskit/algorithms/minimum_eigensolvers/vqe.py | 41 +++++++++++-------- 1 file changed, 25 insertions(+), 16 deletions(-) diff --git a/qiskit/algorithms/minimum_eigensolvers/vqe.py b/qiskit/algorithms/minimum_eigensolvers/vqe.py index 1776091ae30e..3437b6e2f1e8 100644 --- a/qiskit/algorithms/minimum_eigensolvers/vqe.py +++ b/qiskit/algorithms/minimum_eigensolvers/vqe.py @@ -29,7 +29,7 @@ from ..exceptions import AlgorithmError from ..list_or_dict import ListOrDict -from ..optimizers import Optimizer, Minimizer +from ..optimizers import Optimizer, Minimizer, OptimizerResult from ..variational_algorithm import VariationalAlgorithm, VariationalResult from .minimum_eigensolver import MinimumEigensolver, MinimumEigensolverResult @@ -178,36 +178,28 @@ def compute_minimum_eigenvalue( # perform optimization if callable(self.optimizer): - opt_result = self.optimizer( + optimizer_result = self.optimizer( fun=evaluate_energy, x0=initial_point, jac=evaluate_gradient, bounds=bounds ) else: - opt_result = self.optimizer.minimize( + optimizer_result = self.optimizer.minimize( fun=evaluate_energy, x0=initial_point, jac=evaluate_gradient, bounds=bounds ) eval_time = time() - start_time - result = VQEResult() - result.eigenvalue = opt_result.fun - result.cost_function_evals = opt_result.nfev - result.optimal_point = opt_result.x - result.optimal_parameters = dict(zip(ansatz.parameters, opt_result.x)) - result.optimal_value = opt_result.fun - result.optimizer_time = eval_time - logger.info( - "Optimization complete in %s seconds.\nFound opt_params %s", + "Optimization complete in %s seconds.\nFound optimal point %s", eval_time, - result.optimal_point, + optimizer_result.x, ) + aux_values = None if aux_operators is not None: - bound_ansatz = ansatz.bind_parameters(opt_result.x) + bound_ansatz = ansatz.bind_parameters(optimizer_result.x) aux_values = estimate_observables(self.estimator, bound_ansatz, aux_operators) - result.aux_operator_eigenvalues = aux_values - return result + return _build_vqe_result(optimizer_result, eval_time, ansatz, aux_values) @classmethod def supports_aux_operators(cls) -> bool: @@ -355,6 +347,23 @@ def _validate_bounds(ansatz: QuantumCircuit) -> list[tuple(float | None, float | return bounds +def _build_vqe_result( + optimizer_result: OptimizerResult, + eval_time: float, + ansatz: QuantumCircuit, + aux_values: ListOrDict[tuple[complex, tuple[complex, int]]], +) -> VQEResult: + result = VQEResult() + result.eigenvalue = optimizer_result.fun + result.cost_function_evals = optimizer_result.nfev + result.optimal_point = optimizer_result.x + result.optimal_parameters = dict(zip(ansatz.parameters, optimizer_result.x)) + result.optimal_value = optimizer_result.fun + result.optimizer_time = eval_time + result.aux_operator_eigenvalues = aux_values + return result + + class VQEResult(VariationalResult, MinimumEigensolverResult): """Variational quantum eigensolver result.""" From c204a7ea872af79aeaa53533f6c99204e5af0a32 Mon Sep 17 00:00:00 2001 From: Declan Millar Date: Wed, 21 Sep 2022 15:08:49 +0100 Subject: [PATCH 061/100] Correct aux operator eignvalue type hint Co-authored-by: ElePT <57907331+ElePT@users.noreply.github.com> --- .../algorithms/minimum_eigensolvers/minimum_eigensolver.py | 6 +++--- qiskit/algorithms/minimum_eigensolvers/vqe.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/qiskit/algorithms/minimum_eigensolvers/minimum_eigensolver.py b/qiskit/algorithms/minimum_eigensolvers/minimum_eigensolver.py index fa38330861eb..cfee72201216 100644 --- a/qiskit/algorithms/minimum_eigensolvers/minimum_eigensolver.py +++ b/qiskit/algorithms/minimum_eigensolvers/minimum_eigensolver.py @@ -86,13 +86,13 @@ def eigenvalue(self, value: complex) -> None: self._eigenvalue = value @property - def aux_operator_eigenvalues(self) -> ListOrDict[tuple[complex, complex]] | None: + def aux_operator_eigenvalues(self) -> ListOrDict[tuple[complex, tuple[complex, int]]] | None: """The aux operator expectation values. - These values are in fact tuples formatted as (mean, standard deviation). + These values are in fact tuples formatted as (mean, (variance, shots)). """ return self._aux_operator_eigenvalues @aux_operator_eigenvalues.setter - def aux_operator_eigenvalues(self, value: ListOrDict[tuple[complex, complex]]) -> None: + def aux_operator_eigenvalues(self, value: ListOrDict[tuple[complex, tuple[complex, int]]]) -> None: self._aux_operator_eigenvalues = value diff --git a/qiskit/algorithms/minimum_eigensolvers/vqe.py b/qiskit/algorithms/minimum_eigensolvers/vqe.py index 3437b6e2f1e8..c45202b8d742 100644 --- a/qiskit/algorithms/minimum_eigensolvers/vqe.py +++ b/qiskit/algorithms/minimum_eigensolvers/vqe.py @@ -216,7 +216,7 @@ def _get_evaluate_energy( ansatz: The ansatz preparing the quantum state. operator: The operator whose energy to evaluate. Returns: - Energy of the Hamiltonian of each parameter. + A callable that computes and returns the energy of the hamiltonian of each parameter. Raises: AlgorithmError: If the primitive job to evaluate the energy fails. """ From 91fe42784500b2fa3fead580861a87656f594d84 Mon Sep 17 00:00:00 2001 From: Declan Millar Date: Thu, 22 Sep 2022 17:38:30 +0100 Subject: [PATCH 062/100] add variance to callback --- qiskit/algorithms/minimum_eigensolvers/vqe.py | 47 ++++++++++++++--- qiskit/algorithms/observables_evaluator.py | 2 +- .../minimum_eigensolvers/test_vqe.py | 52 +++++++++++++------ 3 files changed, 77 insertions(+), 24 deletions(-) diff --git a/qiskit/algorithms/minimum_eigensolvers/vqe.py b/qiskit/algorithms/minimum_eigensolvers/vqe.py index c45202b8d742..299243748ed4 100644 --- a/qiskit/algorithms/minimum_eigensolvers/vqe.py +++ b/qiskit/algorithms/minimum_eigensolvers/vqe.py @@ -23,7 +23,7 @@ from qiskit.algorithms.gradients import BaseEstimatorGradient from qiskit.circuit import QuantumCircuit from qiskit.opflow import PauliSumOp -from qiskit.primitives import BaseEstimator +from qiskit.primitives import BaseEstimator, EstimatorResult from qiskit.quantum_info.operators.base_operator import BaseOperator from qiskit.utils import algorithm_globals @@ -230,22 +230,26 @@ def evaluate_energy(parameters: np.ndarray) -> np.ndarray | float: # handle broadcasting: ensure parameters is of shape [array, array, ...] parameters = np.reshape(parameters, (-1, num_parameters)).tolist() - batchsize = len(parameters) + batch_size = len(parameters) try: - job = self.estimator.run(batchsize * [ansatz], batchsize * [operator], parameters) - values = job.result().values + job = self.estimator.run(batch_size * [ansatz], batch_size * [operator], parameters) + estimator_result = job.result() except Exception as exc: raise AlgorithmError("The primitive job to evaluate the energy failed!") from exc - # TODO recover variance from estimator if metadata has shots? + values = estimator_result.values + + metadata = _prep_metadata(estimator_result) if self.callback is not None: - for params, value in zip(parameters, values): + for params, value, meta in zip(parameters, values, metadata): eval_count += 1 - self.callback(eval_count, params, value, 0.0) + self.callback(eval_count, params, value, meta[0]) + + energy = values[0] if len(values) == 1 else values - return values[0] if len(values) == 1 else values + return energy return evaluate_energy @@ -347,6 +351,33 @@ def _validate_bounds(ansatz: QuantumCircuit) -> list[tuple(float | None, float | return bounds +def _prep_metadata( + estimator_result: EstimatorResult, +) -> list[tuple[float, dict]]: + """ + Prepares a list of tuples of (variance, metadata) from the estimator result. + + Args: + observables_results: A list of tuples (mean, (variance, shots)). + + Returns: + A list of tuples (variance, metadata)). + """ + if not estimator_result.metadata: + return [(0.0, {})] * len(estimator_result.values) + + results = [] + for metadata in estimator_result.metadata: + variance = 0.0 + if metadata: + if "variance" in metadata.keys(): + variance = metadata["variance"] + + results.append((variance, metadata)) + + return results + + def _build_vqe_result( optimizer_result: OptimizerResult, eval_time: float, diff --git a/qiskit/algorithms/observables_evaluator.py b/qiskit/algorithms/observables_evaluator.py index f6a6c3d8094a..9f6f60104515 100644 --- a/qiskit/algorithms/observables_evaluator.py +++ b/qiskit/algorithms/observables_evaluator.py @@ -71,7 +71,7 @@ def estimate_observables( except Exception as exc: raise AlgorithmError("The primitive job failed!") from exc - variance_and_shots = _prep_variance_and_shots(estimator_job, len(expectation_values)) + variance_and_shots = _prep_variance_and_shots(estimator_job.result(), len(expectation_values)) # Discard values below threshold observables_means = expectation_values * (np.abs(expectation_values) > threshold) diff --git a/test/python/algorithms/minimum_eigensolvers/test_vqe.py b/test/python/algorithms/minimum_eigensolvers/test_vqe.py index fd281be4e893..95c4103b1a3b 100644 --- a/test/python/algorithms/minimum_eigensolvers/test_vqe.py +++ b/test/python/algorithms/minimum_eigensolvers/test_vqe.py @@ -227,30 +227,52 @@ def test_with_two_qubit_reduction(self): def test_callback(self): """Test the callback on VQE.""" - history = {"eval_count": [], "parameters": [], "mean": [], "std": []} + history = {"eval_count": [], "parameters": [], "mean": [], "variance": []} - def store_intermediate_result(eval_count, parameters, mean, std): + def store_intermediate_result(eval_count, parameters, mean, variance): history["eval_count"].append(eval_count) history["parameters"].append(parameters) history["mean"].append(mean) - history["std"].append(std) + history["variance"].append(variance) optimizer = COBYLA(maxiter=3) wavefunction = self.ry_wavefunction - vqe = VQE( - Estimator(), - ansatz=wavefunction, - optimizer=optimizer, - callback=store_intermediate_result, - ) - vqe.compute_minimum_eigenvalue(operator=self.h2_op) + with self.subTest("Test without specifying shots."): + estimator = Estimator() + + vqe = VQE( + estimator, + ansatz=wavefunction, + optimizer=optimizer, + callback=store_intermediate_result, + ) + vqe.compute_minimum_eigenvalue(operator=self.h2_op) + + self.assertTrue(all(isinstance(count, int) for count in history["eval_count"])) + self.assertTrue(all(isinstance(mean, float) for mean in history["mean"])) + self.assertTrue(all(variance == 0.0 for variance in history["variance"])) + + for params in history["parameters"]: + self.assertTrue(all(isinstance(param, float) for param in params)) + + with self.subTest("Test when specifying shots."): + estimator = Estimator(options={"shots": 1000}) + + vqe = VQE( + estimator, + ansatz=wavefunction, + optimizer=optimizer, + callback=store_intermediate_result, + ) + vqe.compute_minimum_eigenvalue(operator=self.h2_op) + + self.assertTrue(all(isinstance(count, int) for count in history["eval_count"])) + self.assertTrue(all(isinstance(mean, float) for mean in history["mean"])) + self.assertTrue(all(isinstance(variance, float) for variance in history["variance"])) - self.assertTrue(all(isinstance(count, int) for count in history["eval_count"])) - self.assertTrue(all(isinstance(mean, float) for mean in history["mean"])) - self.assertTrue(all(isinstance(std, float) for std in history["std"])) - for params in history["parameters"]: - self.assertTrue(all(isinstance(param, float) for param in params)) + for params in history["parameters"]: + self.assertTrue(all(isinstance(param, float) for param in params)) def test_reuse(self): """Test re-using a VQE algorithm instance.""" From 7afa7eb5baf0b59bf9dafe4d2639400b7d90d139 Mon Sep 17 00:00:00 2001 From: Declan Millar Date: Fri, 23 Sep 2022 09:55:02 +0100 Subject: [PATCH 063/100] use std_dev in callback rather than variance --- qiskit/algorithms/minimum_eigensolvers/vqe.py | 35 +++---------------- .../minimum_eigensolvers/test_vqe.py | 10 +++--- 2 files changed, 10 insertions(+), 35 deletions(-) diff --git a/qiskit/algorithms/minimum_eigensolvers/vqe.py b/qiskit/algorithms/minimum_eigensolvers/vqe.py index 299243748ed4..e7f9cc0cf8f1 100644 --- a/qiskit/algorithms/minimum_eigensolvers/vqe.py +++ b/qiskit/algorithms/minimum_eigensolvers/vqe.py @@ -239,13 +239,15 @@ def evaluate_energy(parameters: np.ndarray) -> np.ndarray | float: raise AlgorithmError("The primitive job to evaluate the energy failed!") from exc values = estimator_result.values - - metadata = _prep_metadata(estimator_result) + metadata = estimator_result.metadata if self.callback is not None: for params, value, meta in zip(parameters, values, metadata): eval_count += 1 - self.callback(eval_count, params, value, meta[0]) + variance = meta.pop("variance", 0.0) + shots = meta.pop("shots", 0) + estimator_error = np.sqrt(variance / shots) if shots > 0 else 0.0 + self.callback(eval_count, params, value, estimator_error) energy = values[0] if len(values) == 1 else values @@ -351,33 +353,6 @@ def _validate_bounds(ansatz: QuantumCircuit) -> list[tuple(float | None, float | return bounds -def _prep_metadata( - estimator_result: EstimatorResult, -) -> list[tuple[float, dict]]: - """ - Prepares a list of tuples of (variance, metadata) from the estimator result. - - Args: - observables_results: A list of tuples (mean, (variance, shots)). - - Returns: - A list of tuples (variance, metadata)). - """ - if not estimator_result.metadata: - return [(0.0, {})] * len(estimator_result.values) - - results = [] - for metadata in estimator_result.metadata: - variance = 0.0 - if metadata: - if "variance" in metadata.keys(): - variance = metadata["variance"] - - results.append((variance, metadata)) - - return results - - def _build_vqe_result( optimizer_result: OptimizerResult, eval_time: float, diff --git a/test/python/algorithms/minimum_eigensolvers/test_vqe.py b/test/python/algorithms/minimum_eigensolvers/test_vqe.py index 95c4103b1a3b..c842e44a4d8f 100644 --- a/test/python/algorithms/minimum_eigensolvers/test_vqe.py +++ b/test/python/algorithms/minimum_eigensolvers/test_vqe.py @@ -227,13 +227,13 @@ def test_with_two_qubit_reduction(self): def test_callback(self): """Test the callback on VQE.""" - history = {"eval_count": [], "parameters": [], "mean": [], "variance": []} + history = {"eval_count": [], "parameters": [], "mean": [], "std_dev": []} - def store_intermediate_result(eval_count, parameters, mean, variance): + def store_intermediate_result(eval_count, parameters, mean, std_dev): history["eval_count"].append(eval_count) history["parameters"].append(parameters) history["mean"].append(mean) - history["variance"].append(variance) + history["std_dev"].append(std_dev) optimizer = COBYLA(maxiter=3) wavefunction = self.ry_wavefunction @@ -251,7 +251,7 @@ def store_intermediate_result(eval_count, parameters, mean, variance): self.assertTrue(all(isinstance(count, int) for count in history["eval_count"])) self.assertTrue(all(isinstance(mean, float) for mean in history["mean"])) - self.assertTrue(all(variance == 0.0 for variance in history["variance"])) + self.assertTrue(all(std_dev == 0.0 for std_dev in history["std_dev"])) for params in history["parameters"]: self.assertTrue(all(isinstance(param, float) for param in params)) @@ -269,7 +269,7 @@ def store_intermediate_result(eval_count, parameters, mean, variance): self.assertTrue(all(isinstance(count, int) for count in history["eval_count"])) self.assertTrue(all(isinstance(mean, float) for mean in history["mean"])) - self.assertTrue(all(isinstance(variance, float) for variance in history["variance"])) + self.assertTrue(all(isinstance(std_dev, float) for std_dev in history["std_dev"])) for params in history["parameters"]: self.assertTrue(all(isinstance(param, float) for param in params)) From ac2871a6b7753cce894a4efcab3a0468c54a7857 Mon Sep 17 00:00:00 2001 From: Declan Millar Date: Fri, 23 Sep 2022 10:04:23 +0100 Subject: [PATCH 064/100] formatting --- qiskit/algorithms/minimum_eigensolvers/minimum_eigensolver.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/qiskit/algorithms/minimum_eigensolvers/minimum_eigensolver.py b/qiskit/algorithms/minimum_eigensolvers/minimum_eigensolver.py index cfee72201216..017639ab121b 100644 --- a/qiskit/algorithms/minimum_eigensolvers/minimum_eigensolver.py +++ b/qiskit/algorithms/minimum_eigensolvers/minimum_eigensolver.py @@ -94,5 +94,7 @@ def aux_operator_eigenvalues(self) -> ListOrDict[tuple[complex, tuple[complex, i return self._aux_operator_eigenvalues @aux_operator_eigenvalues.setter - def aux_operator_eigenvalues(self, value: ListOrDict[tuple[complex, tuple[complex, int]]]) -> None: + def aux_operator_eigenvalues( + self, value: ListOrDict[tuple[complex, tuple[complex, int]]] + ) -> None: self._aux_operator_eigenvalues = value From 0e0a5d43492121fb9a146adcc3bae01f090c13c9 Mon Sep 17 00:00:00 2001 From: Declan Millar Date: Fri, 23 Sep 2022 11:35:14 +0100 Subject: [PATCH 065/100] return variance and shots in callback --- qiskit/algorithms/minimum_eigensolvers/vqe.py | 15 +++-- .../minimum_eigensolvers/test_vqe.py | 62 +++++++------------ 2 files changed, 31 insertions(+), 46 deletions(-) diff --git a/qiskit/algorithms/minimum_eigensolvers/vqe.py b/qiskit/algorithms/minimum_eigensolvers/vqe.py index e7f9cc0cf8f1..0527f5ed2254 100644 --- a/qiskit/algorithms/minimum_eigensolvers/vqe.py +++ b/qiskit/algorithms/minimum_eigensolvers/vqe.py @@ -117,7 +117,7 @@ def __init__( *, gradient: BaseEstimatorGradient | None = None, initial_point: Sequence[float] | None = None, - callback: Callable[[int, np.ndarray, float, float], None] | None = None, + callback: Callable[[int, np.ndarray, float, float, int], None] | None = None, ) -> None: """ Args: @@ -134,7 +134,7 @@ def __init__( Four parameter values are passed to the callback as follows during each evaluation by the optimizer for its current set of parameters as it works towards the minimum. These are: the evaluation count, the optimizer parameters for the ansatz, the - evaluated mean and the evaluated standard deviation. + evaluated mean, the evaluated variance, and the number of shots. """ super().__init__() @@ -194,10 +194,11 @@ def compute_minimum_eigenvalue( optimizer_result.x, ) - aux_values = None if aux_operators is not None: bound_ansatz = ansatz.bind_parameters(optimizer_result.x) aux_values = estimate_observables(self.estimator, bound_ansatz, aux_operators) + else: + aux_values = None return _build_vqe_result(optimizer_result, eval_time, ansatz, aux_values) @@ -239,15 +240,14 @@ def evaluate_energy(parameters: np.ndarray) -> np.ndarray | float: raise AlgorithmError("The primitive job to evaluate the energy failed!") from exc values = estimator_result.values - metadata = estimator_result.metadata if self.callback is not None: + metadata = estimator_result.metadata for params, value, meta in zip(parameters, values, metadata): eval_count += 1 variance = meta.pop("variance", 0.0) shots = meta.pop("shots", 0) - estimator_error = np.sqrt(variance / shots) if shots > 0 else 0.0 - self.callback(eval_count, params, value, estimator_error) + self.callback(eval_count, params, value, variance, shots) energy = values[0] if len(values) == 1 else values @@ -379,10 +379,9 @@ def __init__(self) -> None: @property def cost_function_evals(self) -> int | None: - """Returns number of cost optimizer evaluations.""" + """The number of cost optimizer evaluations.""" return self._cost_function_evals @cost_function_evals.setter def cost_function_evals(self, value: int) -> None: - """Sets number of cost function evaluations.""" self._cost_function_evals = value diff --git a/test/python/algorithms/minimum_eigensolvers/test_vqe.py b/test/python/algorithms/minimum_eigensolvers/test_vqe.py index c842e44a4d8f..5478febf4ae2 100644 --- a/test/python/algorithms/minimum_eigensolvers/test_vqe.py +++ b/test/python/algorithms/minimum_eigensolvers/test_vqe.py @@ -225,54 +225,40 @@ def test_with_two_qubit_reduction(self): result = vqe.compute_minimum_eigenvalue(tapered_qubit_op) self.assertAlmostEqual(result.eigenvalue.real, self.h2_energy, places=2) - def test_callback(self): + @data({}, {"shots": 1000}) + def test_callback(self, options): """Test the callback on VQE.""" - history = {"eval_count": [], "parameters": [], "mean": [], "std_dev": []} + history = {"eval_count": [], "parameters": [], "mean": [], "variance": [], "shots": []} + expected_shots = options["shots"] if "shots" in options else 0 - def store_intermediate_result(eval_count, parameters, mean, std_dev): + def store_intermediate_result(eval_count, parameters, mean, variance, shots): history["eval_count"].append(eval_count) history["parameters"].append(parameters) history["mean"].append(mean) - history["std_dev"].append(std_dev) + history["variance"].append(variance) + history["shots"].append(shots) optimizer = COBYLA(maxiter=3) wavefunction = self.ry_wavefunction - with self.subTest("Test without specifying shots."): - estimator = Estimator() + estimator = Estimator(options=options) - vqe = VQE( - estimator, - ansatz=wavefunction, - optimizer=optimizer, - callback=store_intermediate_result, - ) - vqe.compute_minimum_eigenvalue(operator=self.h2_op) - - self.assertTrue(all(isinstance(count, int) for count in history["eval_count"])) - self.assertTrue(all(isinstance(mean, float) for mean in history["mean"])) - self.assertTrue(all(std_dev == 0.0 for std_dev in history["std_dev"])) - - for params in history["parameters"]: - self.assertTrue(all(isinstance(param, float) for param in params)) - - with self.subTest("Test when specifying shots."): - estimator = Estimator(options={"shots": 1000}) - - vqe = VQE( - estimator, - ansatz=wavefunction, - optimizer=optimizer, - callback=store_intermediate_result, - ) - vqe.compute_minimum_eigenvalue(operator=self.h2_op) - - self.assertTrue(all(isinstance(count, int) for count in history["eval_count"])) - self.assertTrue(all(isinstance(mean, float) for mean in history["mean"])) - self.assertTrue(all(isinstance(std_dev, float) for std_dev in history["std_dev"])) - - for params in history["parameters"]: - self.assertTrue(all(isinstance(param, float) for param in params)) + vqe = VQE( + estimator, + wavefunction, + optimizer, + callback=store_intermediate_result, + ) + vqe.compute_minimum_eigenvalue(operator=self.h2_op) + + self.assertTrue(all(isinstance(count, int) for count in history["eval_count"])) + self.assertTrue(all(isinstance(mean, float) for mean in history["mean"])) + self.assertTrue(all(isinstance(variance, float) for variance in history["variance"])) + if expected_shots == 0: + np.testing.assert_array_equal(history["variance"], 0.0) + np.testing.assert_array_equal(history["shots"], expected_shots) + for params in history["parameters"]: + self.assertTrue(all(isinstance(param, float) for param in params)) def test_reuse(self): """Test re-using a VQE algorithm instance.""" From 8d238bfccd979011df4a92c09aa643623f64f2b6 Mon Sep 17 00:00:00 2001 From: Declan Millar Date: Fri, 23 Sep 2022 12:12:21 +0100 Subject: [PATCH 066/100] return full metadata in callback --- qiskit/algorithms/minimum_eigensolvers/vqe.py | 10 ++++------ .../minimum_eigensolvers/test_vqe.py | 19 +++++++------------ 2 files changed, 11 insertions(+), 18 deletions(-) diff --git a/qiskit/algorithms/minimum_eigensolvers/vqe.py b/qiskit/algorithms/minimum_eigensolvers/vqe.py index 0527f5ed2254..d7092d2c10ee 100644 --- a/qiskit/algorithms/minimum_eigensolvers/vqe.py +++ b/qiskit/algorithms/minimum_eigensolvers/vqe.py @@ -23,7 +23,7 @@ from qiskit.algorithms.gradients import BaseEstimatorGradient from qiskit.circuit import QuantumCircuit from qiskit.opflow import PauliSumOp -from qiskit.primitives import BaseEstimator, EstimatorResult +from qiskit.primitives import BaseEstimator from qiskit.quantum_info.operators.base_operator import BaseOperator from qiskit.utils import algorithm_globals @@ -117,7 +117,7 @@ def __init__( *, gradient: BaseEstimatorGradient | None = None, initial_point: Sequence[float] | None = None, - callback: Callable[[int, np.ndarray, float, float, int], None] | None = None, + callback: Callable[[int, np.ndarray, float, dict], None] | None = None, ) -> None: """ Args: @@ -134,7 +134,7 @@ def __init__( Four parameter values are passed to the callback as follows during each evaluation by the optimizer for its current set of parameters as it works towards the minimum. These are: the evaluation count, the optimizer parameters for the ansatz, the - evaluated mean, the evaluated variance, and the number of shots. + evaluated mean and the metadata dictionary. """ super().__init__() @@ -245,9 +245,7 @@ def evaluate_energy(parameters: np.ndarray) -> np.ndarray | float: metadata = estimator_result.metadata for params, value, meta in zip(parameters, values, metadata): eval_count += 1 - variance = meta.pop("variance", 0.0) - shots = meta.pop("shots", 0) - self.callback(eval_count, params, value, variance, shots) + self.callback(eval_count, params, value, meta) energy = values[0] if len(values) == 1 else values diff --git a/test/python/algorithms/minimum_eigensolvers/test_vqe.py b/test/python/algorithms/minimum_eigensolvers/test_vqe.py index 5478febf4ae2..66655220d263 100644 --- a/test/python/algorithms/minimum_eigensolvers/test_vqe.py +++ b/test/python/algorithms/minimum_eigensolvers/test_vqe.py @@ -225,23 +225,21 @@ def test_with_two_qubit_reduction(self): result = vqe.compute_minimum_eigenvalue(tapered_qubit_op) self.assertAlmostEqual(result.eigenvalue.real, self.h2_energy, places=2) - @data({}, {"shots": 1000}) - def test_callback(self, options): + def test_callback(self): """Test the callback on VQE.""" - history = {"eval_count": [], "parameters": [], "mean": [], "variance": [], "shots": []} - expected_shots = options["shots"] if "shots" in options else 0 + history = {"eval_count": [], "parameters": [], "mean": [], "metadata": []} + # expected_shots = options["shots"] if "shots" in options else 0 - def store_intermediate_result(eval_count, parameters, mean, variance, shots): + def store_intermediate_result(eval_count, parameters, mean, metadata): history["eval_count"].append(eval_count) history["parameters"].append(parameters) history["mean"].append(mean) - history["variance"].append(variance) - history["shots"].append(shots) + history["metadata"].append(metadata) optimizer = COBYLA(maxiter=3) wavefunction = self.ry_wavefunction - estimator = Estimator(options=options) + estimator = Estimator() vqe = VQE( estimator, @@ -253,10 +251,7 @@ def store_intermediate_result(eval_count, parameters, mean, variance, shots): self.assertTrue(all(isinstance(count, int) for count in history["eval_count"])) self.assertTrue(all(isinstance(mean, float) for mean in history["mean"])) - self.assertTrue(all(isinstance(variance, float) for variance in history["variance"])) - if expected_shots == 0: - np.testing.assert_array_equal(history["variance"], 0.0) - np.testing.assert_array_equal(history["shots"], expected_shots) + self.assertTrue(all(isinstance(metadata, dict) for metadata in history["metadata"])) for params in history["parameters"]: self.assertTrue(all(isinstance(param, float) for param in params)) From a2cd35a1f842997e82991f03410959ff9b3983ed Mon Sep 17 00:00:00 2001 From: Declan Millar Date: Fri, 23 Sep 2022 14:06:28 +0100 Subject: [PATCH 067/100] Move validation functions to algorithms/utils --- qiskit/algorithms/minimum_eigensolvers/vqe.py | 45 +--------------- qiskit/algorithms/utils/__init__.py | 21 ++++++++ qiskit/algorithms/utils/validate_bounds.py | 31 +++++++++++ .../utils/validate_initial_point.py | 52 ++++++++++++++++++ test/python/algorithms/utils/__init__.py | 11 ++++ .../algorithms/utils/test_validate_bounds.py | 53 +++++++++++++++++++ .../utils/test_validate_initial_point.py | 50 +++++++++++++++++ 7 files changed, 219 insertions(+), 44 deletions(-) create mode 100644 qiskit/algorithms/utils/__init__.py create mode 100644 qiskit/algorithms/utils/validate_bounds.py create mode 100644 qiskit/algorithms/utils/validate_initial_point.py create mode 100644 test/python/algorithms/utils/__init__.py create mode 100644 test/python/algorithms/utils/test_validate_bounds.py create mode 100644 test/python/algorithms/utils/test_validate_initial_point.py diff --git a/qiskit/algorithms/minimum_eigensolvers/vqe.py b/qiskit/algorithms/minimum_eigensolvers/vqe.py index d7092d2c10ee..0ae1babdb27a 100644 --- a/qiskit/algorithms/minimum_eigensolvers/vqe.py +++ b/qiskit/algorithms/minimum_eigensolvers/vqe.py @@ -25,15 +25,14 @@ from qiskit.opflow import PauliSumOp from qiskit.primitives import BaseEstimator from qiskit.quantum_info.operators.base_operator import BaseOperator -from qiskit.utils import algorithm_globals from ..exceptions import AlgorithmError from ..list_or_dict import ListOrDict from ..optimizers import Optimizer, Minimizer, OptimizerResult from ..variational_algorithm import VariationalAlgorithm, VariationalResult from .minimum_eigensolver import MinimumEigensolver, MinimumEigensolverResult - from ..observables_evaluator import estimate_observables +from ..utils import _validate_initial_point, _validate_bounds logger = logging.getLogger(__name__) @@ -309,48 +308,6 @@ def _check_operator_ansatz(self, operator: BaseOperator | PauliSumOp) -> Quantum return ansatz -def _validate_initial_point(point: Sequence[float], ansatz: QuantumCircuit) -> Sequence[float]: - expected_size = ansatz.num_parameters - - if point is None: - # get bounds if ansatz has them set, otherwise use [-2pi, 2pi] for each parameter - bounds = getattr(ansatz, "parameter_bounds", None) - if bounds is None: - bounds = [(-2 * np.pi, 2 * np.pi)] * expected_size - - # replace all Nones by [-2pi, 2pi] - lower_bounds = [] - upper_bounds = [] - for lower, upper in bounds: - lower_bounds.append(lower if lower is not None else -2 * np.pi) - upper_bounds.append(upper if upper is not None else 2 * np.pi) - - # sample from within bounds - point = algorithm_globals.random.uniform(lower_bounds, upper_bounds) - - elif len(point) != expected_size: - raise ValueError( - f"The dimension of the initial point ({len(point)}) does not match the " - f"number of parameters in the circuit ({expected_size})." - ) - - return point - - -def _validate_bounds(ansatz: QuantumCircuit) -> list[tuple(float | None, float | None)]: - if hasattr(ansatz, "parameter_bounds") and ansatz.parameter_bounds is not None: - bounds = ansatz.parameter_bounds - if len(bounds) != ansatz.num_parameters: - raise ValueError( - f"The number of bounds ({len(bounds)}) does not match the number of " - f"parameters in the circuit ({ansatz.num_parameters})." - ) - else: - bounds = [(None, None)] * ansatz.num_parameters - - return bounds - - def _build_vqe_result( optimizer_result: OptimizerResult, eval_time: float, diff --git a/qiskit/algorithms/utils/__init__.py b/qiskit/algorithms/utils/__init__.py new file mode 100644 index 000000000000..946d68e5c5fa --- /dev/null +++ b/qiskit/algorithms/utils/__init__.py @@ -0,0 +1,21 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2022. +# +# 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. + +"""Common Qiskit algorithms utility functions.""" + +from .validate_initial_point import _validate_initial_point +from .validate_bounds import _validate_bounds + +__all__ = [ + "_validate_initial_point", + "_validate_bounds", +] diff --git a/qiskit/algorithms/utils/validate_bounds.py b/qiskit/algorithms/utils/validate_bounds.py new file mode 100644 index 000000000000..75ea71cf669d --- /dev/null +++ b/qiskit/algorithms/utils/validate_bounds.py @@ -0,0 +1,31 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2022. +# +# 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. + +"""Validate parameter bounds.""" + +from __future__ import annotations + +from qiskit.circuit import QuantumCircuit + + +def _validate_bounds(ansatz: QuantumCircuit) -> list[tuple(float | None, float | None)]: + if hasattr(ansatz, "parameter_bounds") and ansatz.parameter_bounds is not None: + bounds = ansatz.parameter_bounds + if len(bounds) != ansatz.num_parameters: + raise ValueError( + f"The number of bounds ({len(bounds)}) does not match the number of " + f"parameters in the circuit ({ansatz.num_parameters})." + ) + else: + bounds = [(None, None)] * ansatz.num_parameters + + return bounds diff --git a/qiskit/algorithms/utils/validate_initial_point.py b/qiskit/algorithms/utils/validate_initial_point.py new file mode 100644 index 000000000000..70bef3a08aad --- /dev/null +++ b/qiskit/algorithms/utils/validate_initial_point.py @@ -0,0 +1,52 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2022. +# +# 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. + +"""Validate an initial point.""" + +from __future__ import annotations + +from collections.abc import Sequence + +import numpy as np + +from qiskit.circuit import QuantumCircuit +from qiskit.utils import algorithm_globals + + +def _validate_initial_point( + point: Sequence[float] | None, ansatz: QuantumCircuit +) -> Sequence[float]: + expected_size = ansatz.num_parameters + + if point is None: + # get bounds if ansatz has them set, otherwise use [-2pi, 2pi] for each parameter + bounds = getattr(ansatz, "parameter_bounds", None) + if bounds is None: + bounds = [(-2 * np.pi, 2 * np.pi)] * expected_size + + # replace all Nones by [-2pi, 2pi] + lower_bounds = [] + upper_bounds = [] + for lower, upper in bounds: + lower_bounds.append(lower if lower is not None else -2 * np.pi) + upper_bounds.append(upper if upper is not None else 2 * np.pi) + + # sample from within bounds + point = algorithm_globals.random.uniform(lower_bounds, upper_bounds) + + elif len(point) != expected_size: + raise ValueError( + f"The dimension of the initial point ({len(point)}) does not match the " + f"number of parameters in the circuit ({expected_size})." + ) + + return point diff --git a/test/python/algorithms/utils/__init__.py b/test/python/algorithms/utils/__init__.py new file mode 100644 index 000000000000..fdb172d367f0 --- /dev/null +++ b/test/python/algorithms/utils/__init__.py @@ -0,0 +1,11 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2022. +# +# 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. diff --git a/test/python/algorithms/utils/test_validate_bounds.py b/test/python/algorithms/utils/test_validate_bounds.py new file mode 100644 index 000000000000..4958e9f380d9 --- /dev/null +++ b/test/python/algorithms/utils/test_validate_bounds.py @@ -0,0 +1,53 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2022. +# +# 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. + +"""Test validate bounds.""" + +from test.python.algorithms import QiskitAlgorithmsTestCase + +from unittest.mock import Mock + +import numpy as np + +from qiskit.algorithms.utils import _validate_bounds +from qiskit.utils import algorithm_globals + + +class TestValidateBounds(QiskitAlgorithmsTestCase): + """Test the ``_validate_bounds`` utility function.""" + + def setUp(self): + super().setUp() + algorithm_globals.random_seed = 0 + self.bounds = [(-np.pi / 2, np.pi / 2)] + self.ansatz = Mock() + + def test_with_no_ansatz_bounds(self): + """Test with no ansatz bounds.""" + self.ansatz.num_parameters = 1 + self.ansatz.parameter_bounds = None + bounds = _validate_bounds(self.ansatz) + self.assertEqual(bounds, [(None, None)]) + + def test_with_ansatz_bounds(self): + """Test with ansatz bounds.""" + self.ansatz.num_parameters = 1 + self.ansatz.parameter_bounds = self.bounds + bounds = _validate_bounds(self.ansatz) + self.assertEqual(bounds, self.bounds) + + def test_with_mismatched_num_params(self): + """Test with a mismatched number of parameters and bounds""" + self.ansatz.num_parameters = 2 + self.ansatz.parameter_bounds = self.bounds + with self.assertRaises(ValueError): + _ = _validate_bounds(self.ansatz) diff --git a/test/python/algorithms/utils/test_validate_initial_point.py b/test/python/algorithms/utils/test_validate_initial_point.py new file mode 100644 index 000000000000..e38bc90e18b1 --- /dev/null +++ b/test/python/algorithms/utils/test_validate_initial_point.py @@ -0,0 +1,50 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2022. +# +# 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. + +"""Test validate initial point.""" + +from test.python.algorithms import QiskitAlgorithmsTestCase + +from unittest.mock import Mock + +import numpy as np + +from qiskit.algorithms.utils import _validate_initial_point +from qiskit.utils import algorithm_globals + + +class TestValidateInitialPoint(QiskitAlgorithmsTestCase): + """Test the ``_validate_initial_point`` utility function.""" + + def setUp(self): + super().setUp() + algorithm_globals.random_seed = 0 + self.ansatz = Mock() + self.ansatz.num_parameters = 1 + + def test_with_no_initial_point_or_bounds(self): + """Test with no user-defined initial point and no ansatz bounds.""" + self.ansatz.parameter_bounds = None + initial_point = _validate_initial_point(None, self.ansatz) + np.testing.assert_array_almost_equal(initial_point, [1.721111]) + + def test_with_no_initial_point(self): + """Test with no user-defined initial point with ansatz bounds.""" + self.ansatz.parameter_bounds = [(-np.pi / 2, np.pi / 2)] + initial_point = _validate_initial_point(None, self.ansatz) + np.testing.assert_array_almost_equal(initial_point, [0.430278]) + + def test_with_mismatched_params(self): + """Test with mistmatched parameters and bounds..""" + self.ansatz.parameter_bounds = None + with self.assertRaises(ValueError): + _ = _validate_initial_point([1.0, 2.0], self.ansatz) From 4dab8bc0e76fdeb0445b55d566480fdf8011dc54 Mon Sep 17 00:00:00 2001 From: Declan Millar Date: Fri, 23 Sep 2022 15:39:58 +0100 Subject: [PATCH 068/100] correct the callback attribute documentation --- qiskit/algorithms/minimum_eigensolvers/vqe.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/qiskit/algorithms/minimum_eigensolvers/vqe.py b/qiskit/algorithms/minimum_eigensolvers/vqe.py index 0ae1babdb27a..7e7ff2002b1f 100644 --- a/qiskit/algorithms/minimum_eigensolvers/vqe.py +++ b/qiskit/algorithms/minimum_eigensolvers/vqe.py @@ -100,8 +100,7 @@ def my_minimizer(fun, x0, jac=None, bounds=None) -> OptimizerResult: access the intermediate data during the optimization. Four parameter values are passed to the callback as follows during each evaluation by the optimizer for its current set of parameters as it works towards the minimum. These are: the evaluation count, the - optimizer parameters for the ansatz, the evaluated mean and the evaluated standard - deviation. + optimizer parameters for the ansatz, the evaluated mean and the metadata dictionary. References: [1] Peruzzo et al, "A variational eigenvalue solver on a quantum processor" @@ -147,12 +146,11 @@ def __init__( @property def initial_point(self) -> Sequence[float] | None: - """Return the initial point.""" + """The initial point of the optimization.""" return self._initial_point @initial_point.setter def initial_point(self, value: Sequence[float] | None) -> None: - """Set the initial point.""" self._initial_point = value def compute_minimum_eigenvalue( From 78d21c54e24ce1718f37d3598483d47e2242327d Mon Sep 17 00:00:00 2001 From: Declan Millar Date: Fri, 23 Sep 2022 15:45:05 +0100 Subject: [PATCH 069/100] correct the callback attribute typehint docstring --- qiskit/algorithms/minimum_eigensolvers/vqe.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qiskit/algorithms/minimum_eigensolvers/vqe.py b/qiskit/algorithms/minimum_eigensolvers/vqe.py index 7e7ff2002b1f..f953c07850ed 100644 --- a/qiskit/algorithms/minimum_eigensolvers/vqe.py +++ b/qiskit/algorithms/minimum_eigensolvers/vqe.py @@ -96,7 +96,7 @@ def my_minimizer(fun, x0, jac=None, bounds=None) -> OptimizerResult: initial_point (Sequence[float] | None): 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. - callback (Callable[[int, np.ndarray, float, float], None] | None): A callback that can + callback (Callable[[int, np.ndarray, float, dict], None] | None): A callback that can access the intermediate data during the optimization. Four parameter values are passed to the callback as follows during each evaluation by the optimizer for its current set of parameters as it works towards the minimum. These are: the evaluation count, the From d9f5cb2623e1bfbf25c488ab8d9cc33dd4d2cb42 Mon Sep 17 00:00:00 2001 From: Declan Millar Date: Fri, 23 Sep 2022 17:16:41 +0100 Subject: [PATCH 070/100] update VQE docstring --- qiskit/algorithms/minimum_eigensolvers/vqe.py | 24 ++++++++++--------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/qiskit/algorithms/minimum_eigensolvers/vqe.py b/qiskit/algorithms/minimum_eigensolvers/vqe.py index f953c07850ed..424faa5a3a65 100644 --- a/qiskit/algorithms/minimum_eigensolvers/vqe.py +++ b/qiskit/algorithms/minimum_eigensolvers/vqe.py @@ -40,19 +40,22 @@ class VQE(VariationalAlgorithm, MinimumEigensolver): r"""The variational quantum eigensolver (VQE) algorithm. - VQE is a quantum algorithm that uses a variational technique to find the minimum eigenvalue of - the Hamiltonian :math:`H` of a given system [1]. + VQE is a hybrid quantum-classical algorithm that uses a variational technique to find the + minimum eigenvalue of a given Hamiltonian operator. - The central sub-component of VQE is an Estimator primitive, which must be passed in as the - first argument. This is used to estimate the expectation values of :math:`H`. + The VQE algorithm is executed using an Estimator primitive, which must be specified as the first + argument on instantiation. - An instance of VQE also requires defining two algorithmic sub-components: a trial state (a.k.a. - ansatz) which is a :class:`QuantumCircuit`, and one of the classical - :mod:`~qiskit.algorithms.optimizers`. + An instance of VQE also requires defining an ansatz (a.k.a. trial state), given by a + parameterized :class:`.QuantumCircuit`, as well as a classical optimizer. The optimizer varies + the circuit parameters :math:`\vec\theta` such that the expectation value of the operator + :math:`H` on the corresponding state :math:`|{\psi(\vec\theta)\rangle` approaches a minimum, - The ansatz is varied, via its set of parameters, by the optimizer, such that it works towards a - state, as determined by the parameters applied to the ansatz, that will result in the minimum - expectation value being measured of the input operator (Hamiltonian). + ..math:: + + \min_{\vec\theta} \langle\psi(\vec\theta)|H|\psi(\vec\theta)\rangle. + + The Estimator is used to compute this expectation value for every optimization step. The optimizer can either be one of Qiskit's optimizers, such as :class:`~qiskit.algorithms.optimizers.SPSA` or a callable with the following signature: @@ -146,7 +149,6 @@ def __init__( @property def initial_point(self) -> Sequence[float] | None: - """The initial point of the optimization.""" return self._initial_point @initial_point.setter From eaea3e6bfa660375f87526d869034288be7e66b9 Mon Sep 17 00:00:00 2001 From: Declan Millar Date: Fri, 23 Sep 2022 21:24:12 +0100 Subject: [PATCH 071/100] release note and pending-depreciate old algs --- qiskit/algorithms/__init__.py | 13 +++++++++++-- qiskit/algorithms/minimum_eigen_solvers/vqe.py | 15 +++++++++++++++ .../numpy_minimum_eigensolver.py | 8 ++++++++ ...with-estimator-primitive-7cbcc462ad4dc593.yaml | 14 ++++++++++++++ 4 files changed, 48 insertions(+), 2 deletions(-) create mode 100644 releasenotes/notes/vqe-with-estimator-primitive-7cbcc462ad4dc593.yaml diff --git a/qiskit/algorithms/__init__.py b/qiskit/algorithms/__init__.py index 322acba21dac..cebfbbbf3cec 100644 --- a/qiskit/algorithms/__init__.py +++ b/qiskit/algorithms/__init__.py @@ -183,8 +183,8 @@ linear_solvers -Minimum Eigensolvers --------------------- +Minimum Eigen Solvers +--------------------- Algorithms that can find the minimum eigenvalue of an operator. @@ -203,6 +203,15 @@ QAOA VQE +Minimum Eigensolvers +-------------------- + +Algorithms that can find the minimum eigenvalue of an operator and leverage primitives. + +.. autosummary:: + :toctree: ../stubs/ + minimum_eigensolvers + Optimizers ---------- diff --git a/qiskit/algorithms/minimum_eigen_solvers/vqe.py b/qiskit/algorithms/minimum_eigen_solvers/vqe.py index 87ab8707b0f1..a9e5b83aa83b 100755 --- a/qiskit/algorithms/minimum_eigen_solvers/vqe.py +++ b/qiskit/algorithms/minimum_eigen_solvers/vqe.py @@ -40,6 +40,7 @@ from qiskit.utils import QuantumInstance, algorithm_globals from qiskit.utils.backend_utils import is_aer_provider from qiskit.utils.validation import validate_min +from qiskit.utils.deprecation import deprecate_function from ..aux_ops_evaluator import eval_observables from ..exceptions import AlgorithmError @@ -120,6 +121,13 @@ def my_minimizer(fun, x0, jac=None, bounds=None) -> OptimizerResult: """ + @deprecate_function( + "The VQE class has been superseded by the " + "qiskit.algorithms.minimum_eigensolvers.VQE class. " + "This class will be deprecated in a future release and subsequently " + "removed after that.", + category=PendingDeprecationWarning, + ) def __init__( self, ansatz: Optional[QuantumCircuit] = None, @@ -643,6 +651,13 @@ def _get_eigenstate(self, optimal_parameters) -> Union[List[float], Dict[str, in class VQEResult(VariationalResult, MinimumEigensolverResult): """VQE Result.""" + @deprecate_function( + "The VQEResult class has been superseded by the " + "qiskit.algorithms.minimum_eigensolvers.VQEResult class. " + "This class will be deprecated in a future release and subsequently " + "removed after that.", + category=PendingDeprecationWarning, + ) def __init__(self) -> None: super().__init__() self._cost_function_evals = None diff --git a/qiskit/algorithms/minimum_eigensolvers/numpy_minimum_eigensolver.py b/qiskit/algorithms/minimum_eigensolvers/numpy_minimum_eigensolver.py index eb021f926109..cfd8ab5b816a 100644 --- a/qiskit/algorithms/minimum_eigensolvers/numpy_minimum_eigensolver.py +++ b/qiskit/algorithms/minimum_eigensolvers/numpy_minimum_eigensolver.py @@ -20,6 +20,7 @@ from qiskit.opflow import PauliSumOp from qiskit.quantum_info.operators.base_operator import BaseOperator +from qiskit.utils.deprecation import deprecate_function # TODO this path will need updating from ..eigen_solvers.numpy_eigen_solver import NumPyEigensolver @@ -34,6 +35,13 @@ class NumPyMinimumEigensolver(MinimumEigensolver): The NumPy minimum eigensolver algorithm. """ + @deprecate_function( + "The NumPyMinimumEigensolver class has been superseded by the " + "qiskit.algorithms.minimum_eigensolvers.NumPyMinimumEigensolver class. " + "This class will be deprecated in a future release and subsequently " + "removed after that.", + category=PendingDeprecationWarning, + ) def __init__( self, filter_criterion: Callable[ diff --git a/releasenotes/notes/vqe-with-estimator-primitive-7cbcc462ad4dc593.yaml b/releasenotes/notes/vqe-with-estimator-primitive-7cbcc462ad4dc593.yaml new file mode 100644 index 000000000000..b997fadfa177 --- /dev/null +++ b/releasenotes/notes/vqe-with-estimator-primitive-7cbcc462ad4dc593.yaml @@ -0,0 +1,14 @@ +--- +features: + - | + Added :class:`qiskit.algorithms.minimum_eigensolvers` package to include + interfaces for primitive-enabled algorithms. Refactored + :class:`qiskit.algorithms.minimum_eigensolvers.VQE` to leverage primitives. + Please see the Qiskit Runtime migration guide for more information. +deprecations: + - | + :class:`qiskit.algorithms.minimum_eigen_solvers` package will now issue a + ``PendingDeprecationWarning``. It will be deprecated in a future release and subsequently + removed after that. This is being replaced by the new + :class:`qiskit.algorithms.minimum_eigensolvers` package that will host primitive-enabled + algorithms. \ No newline at end of file From 183c20c979227c24ad87a403df41b05a5a9e4136 Mon Sep 17 00:00:00 2001 From: Declan Millar Date: Fri, 23 Sep 2022 21:24:46 +0100 Subject: [PATCH 072/100] update vqe class docstring --- qiskit/algorithms/minimum_eigensolvers/vqe.py | 61 ++++++++++--------- 1 file changed, 31 insertions(+), 30 deletions(-) diff --git a/qiskit/algorithms/minimum_eigensolvers/vqe.py b/qiskit/algorithms/minimum_eigensolvers/vqe.py index 424faa5a3a65..03bebea69627 100644 --- a/qiskit/algorithms/minimum_eigensolvers/vqe.py +++ b/qiskit/algorithms/minimum_eigensolvers/vqe.py @@ -41,15 +41,15 @@ class VQE(VariationalAlgorithm, MinimumEigensolver): r"""The variational quantum eigensolver (VQE) algorithm. VQE is a hybrid quantum-classical algorithm that uses a variational technique to find the - minimum eigenvalue of a given Hamiltonian operator. + minimum eigenvalue of a given Hamiltonian operator :math:`\vec\theta`. - The VQE algorithm is executed using an Estimator primitive, which must be specified as the first - argument on instantiation. + The VQE algorithm is executed using an :attr:`estimator` primitive, which must be specified as + the first argument on instantiation. - An instance of VQE also requires defining an ansatz (a.k.a. trial state), given by a - parameterized :class:`.QuantumCircuit`, as well as a classical optimizer. The optimizer varies - the circuit parameters :math:`\vec\theta` such that the expectation value of the operator - :math:`H` on the corresponding state :math:`|{\psi(\vec\theta)\rangle` approaches a minimum, + An instance of ``VQE`` also requires an :attr:`ansatz` parameterized :class:`.QuantumCircuit` to + prepare the trial state :math:`|\psi(\vec\theta)\rangle`, as well as a classical + :attr:`optimizer`. The optimizer varies the circuit parameters :math:`\vec\theta` such that the + expectation value of the operator :math:`H` on the corresponding state approaches a minimum, ..math:: @@ -88,22 +88,22 @@ def my_minimizer(fun, x0, jac=None, bounds=None) -> OptimizerResult: Attributes: estimator (BaseEstimator): The estimator primitive to compute the expectation value of the - circuits. - ansatz (Quantum Circuit): A parameterized circuit, preparing the ansatz for the wave - function. + Hamiltonian operator. + ansatz (QuantumCircuit): A parameterized quantum circuit to prepare the trial state. optimizer (Optimizer | Minimizer): A classical optimizer to find the minimum energy. This can either be a Qiskit :class:`.Optimizer` or a callable implementing the :class:`.Minimizer` protocol. gradient (BaseEstimatorGradient | None): An optional estimator gradient to be used with the optimizer. initial_point (Sequence[float] | None): 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. - callback (Callable[[int, np.ndarray, float, dict], None] | None): A callback that can - access the intermediate data during the optimization. Four parameter values are passed - to the callback as follows during each evaluation by the optimizer for its current set - of parameters as it works towards the minimum. These are: the evaluation count, the - optimizer parameters for the ansatz, the evaluated mean and the metadata dictionary. + values) for the optimizer. The length of the initial point must match the number of + :attr:`ansatz` parameters. If ``None``, a random point will be generated within certain + parameter bounds. ``VQE`` will look to the ansatz for these bounds. If the ansatz does + not specify bounds, bounds of :math:`-2\pi`, :math:`2\pi` will be used. + callback (Callable[[int, np.ndarray, float, dict], None] | None): A callback that can access + the intermediate data at each optimization step. These data are: the evaluation count, + the optimizer parameters for the ansatz, the evaluated mean, and the metadata + dictionary. References: [1] Peruzzo et al, "A variational eigenvalue solver on a quantum processor" @@ -122,20 +122,21 @@ def __init__( ) -> None: """ Args: - estimator: The estimator primitive to compute the expectation value of the circuits. - ansatz: A parameterized circuit, preparing the ansatz for the wave function. - optimizer: A classical optimizer to find the minimum energy. This can either be a - Qiskit :class:`.Optimizer` or a callable implementing the :class:`.Minimizer` - protocol. + estimator: The estimator primitive to compute the expectation value of the + Hamiltonian operator. + ansatz: A parameterized quantum circuit to prepare the trial state. + optimizer: A classical optimizer to find the minimum energy. This + can either be a Qiskit :class:`.Optimizer` or a callable implementing the + :class:`.Minimizer` protocol. gradient: An optional estimator gradient to be used with the optimizer. - 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. - callback: A callback that can access the intermediate data during the optimization. - Four parameter values are passed to the callback as follows during each evaluation - by the optimizer for its current set of parameters as it works towards the minimum. - These are: the evaluation count, the optimizer parameters for the ansatz, the - evaluated mean and the metadata dictionary. + initial_point: An optional initial point (i.e. initial parameter values) for the optimizer. + The length of the initial point must match the number of :attr:`ansatz` parameters. + If ``None``, a random point will be generated within certain parameter bounds. + ``VQE`` will look to the ansatz for these bounds. If the ansatz does not specify + bounds, bounds of :math:`-2\pi`, :math:`2\pi` will be used. + callback: A callback that can access the intermediate data at each optimization step. + These data are: the evaluation count, the optimizer parameters for the ansatz, the + evaluated mean, and the metadata dictionary. """ super().__init__() From d01d47b25f65ee186d259335643e8d10622c1b12 Mon Sep 17 00:00:00 2001 From: Declan Millar Date: Mon, 26 Sep 2022 10:09:46 +0100 Subject: [PATCH 073/100] Apply suggestions from code review Co-authored-by: ElePT <57907331+ElePT@users.noreply.github.com> --- .../algorithms/minimum_eigen_solvers/vqe.py | 7 ------ .../minimum_eigensolvers/__init__.py | 24 ++++++++++++++++++- .../numpy_minimum_eigensolver.py | 7 ------ qiskit/algorithms/minimum_eigensolvers/vqe.py | 2 +- qiskit/algorithms/utils/__init__.py | 8 +++---- qiskit/algorithms/utils/validate_bounds.py | 2 +- .../utils/validate_initial_point.py | 2 +- ...-estimator-primitive-7cbcc462ad4dc593.yaml | 1 - .../algorithms/utils/test_validate_bounds.py | 8 +++---- .../utils/test_validate_initial_point.py | 6 ++--- 10 files changed, 37 insertions(+), 30 deletions(-) diff --git a/qiskit/algorithms/minimum_eigen_solvers/vqe.py b/qiskit/algorithms/minimum_eigen_solvers/vqe.py index a9e5b83aa83b..71f2a5496bd0 100755 --- a/qiskit/algorithms/minimum_eigen_solvers/vqe.py +++ b/qiskit/algorithms/minimum_eigen_solvers/vqe.py @@ -121,13 +121,6 @@ def my_minimizer(fun, x0, jac=None, bounds=None) -> OptimizerResult: """ - @deprecate_function( - "The VQE class has been superseded by the " - "qiskit.algorithms.minimum_eigensolvers.VQE class. " - "This class will be deprecated in a future release and subsequently " - "removed after that.", - category=PendingDeprecationWarning, - ) def __init__( self, ansatz: Optional[QuantumCircuit] = None, diff --git a/qiskit/algorithms/minimum_eigensolvers/__init__.py b/qiskit/algorithms/minimum_eigensolvers/__init__.py index 1352b9794bf8..b3208de4c3fd 100644 --- a/qiskit/algorithms/minimum_eigensolvers/__init__.py +++ b/qiskit/algorithms/minimum_eigensolvers/__init__.py @@ -10,8 +10,30 @@ # copyright notice, and modified files need to carry a notice indicating # that they have been altered from the originals. -"""The minimum eigensolvers package.""" +===================================================================== +Minimum Eigensolvers Package (:mod:`qiskit.algorithms.minimum_eigensolvers`) +===================================================================== +.. currentmodule:: qiskit.algorithms.minimum_eigensolvers + +Minimum Eigensolvers +================ +.. autosummary:: + :toctree: ../stubs/ + + MinimumEigensolver + NumPyMinimumEigensolver + QAOA + VQE + +.. autosummary:: + :toctree: ../stubs/ + + MinimumEigensolverResult + NumPyMinimumEigensolverResult + VQEResult + + from .minimum_eigensolver import MinimumEigensolver, MinimumEigensolverResult from .numpy_minimum_eigensolver import NumPyMinimumEigensolver, NumPyMinimumEigensolverResult from .vqe import VQE, VQEResult diff --git a/qiskit/algorithms/minimum_eigensolvers/numpy_minimum_eigensolver.py b/qiskit/algorithms/minimum_eigensolvers/numpy_minimum_eigensolver.py index cfd8ab5b816a..ee752b936627 100644 --- a/qiskit/algorithms/minimum_eigensolvers/numpy_minimum_eigensolver.py +++ b/qiskit/algorithms/minimum_eigensolvers/numpy_minimum_eigensolver.py @@ -35,13 +35,6 @@ class NumPyMinimumEigensolver(MinimumEigensolver): The NumPy minimum eigensolver algorithm. """ - @deprecate_function( - "The NumPyMinimumEigensolver class has been superseded by the " - "qiskit.algorithms.minimum_eigensolvers.NumPyMinimumEigensolver class. " - "This class will be deprecated in a future release and subsequently " - "removed after that.", - category=PendingDeprecationWarning, - ) def __init__( self, filter_criterion: Callable[ diff --git a/qiskit/algorithms/minimum_eigensolvers/vqe.py b/qiskit/algorithms/minimum_eigensolvers/vqe.py index 03bebea69627..e580cf84a0ff 100644 --- a/qiskit/algorithms/minimum_eigensolvers/vqe.py +++ b/qiskit/algorithms/minimum_eigensolvers/vqe.py @@ -136,7 +136,7 @@ def __init__( bounds, bounds of :math:`-2\pi`, :math:`2\pi` will be used. callback: A callback that can access the intermediate data at each optimization step. These data are: the evaluation count, the optimizer parameters for the ansatz, the - evaluated mean, and the metadata dictionary. + estimated value, and the metadata dictionary. """ super().__init__() diff --git a/qiskit/algorithms/utils/__init__.py b/qiskit/algorithms/utils/__init__.py index 946d68e5c5fa..2b49396270c7 100644 --- a/qiskit/algorithms/utils/__init__.py +++ b/qiskit/algorithms/utils/__init__.py @@ -12,10 +12,10 @@ """Common Qiskit algorithms utility functions.""" -from .validate_initial_point import _validate_initial_point -from .validate_bounds import _validate_bounds +from .validate_initial_point import validate_initial_point +from .validate_bounds import validate_bounds __all__ = [ - "_validate_initial_point", - "_validate_bounds", + "validate_initial_point", + "validate_bounds", ] diff --git a/qiskit/algorithms/utils/validate_bounds.py b/qiskit/algorithms/utils/validate_bounds.py index 75ea71cf669d..515ccbd1822d 100644 --- a/qiskit/algorithms/utils/validate_bounds.py +++ b/qiskit/algorithms/utils/validate_bounds.py @@ -17,7 +17,7 @@ from qiskit.circuit import QuantumCircuit -def _validate_bounds(ansatz: QuantumCircuit) -> list[tuple(float | None, float | None)]: +def validate_bounds(ansatz: QuantumCircuit) -> list[tuple(float | None, float | None)]: if hasattr(ansatz, "parameter_bounds") and ansatz.parameter_bounds is not None: bounds = ansatz.parameter_bounds if len(bounds) != ansatz.num_parameters: diff --git a/qiskit/algorithms/utils/validate_initial_point.py b/qiskit/algorithms/utils/validate_initial_point.py index 70bef3a08aad..31c078c2fc2a 100644 --- a/qiskit/algorithms/utils/validate_initial_point.py +++ b/qiskit/algorithms/utils/validate_initial_point.py @@ -22,7 +22,7 @@ from qiskit.utils import algorithm_globals -def _validate_initial_point( +def validate_initial_point( point: Sequence[float] | None, ansatz: QuantumCircuit ) -> Sequence[float]: expected_size = ansatz.num_parameters diff --git a/releasenotes/notes/vqe-with-estimator-primitive-7cbcc462ad4dc593.yaml b/releasenotes/notes/vqe-with-estimator-primitive-7cbcc462ad4dc593.yaml index b997fadfa177..63f88699ac05 100644 --- a/releasenotes/notes/vqe-with-estimator-primitive-7cbcc462ad4dc593.yaml +++ b/releasenotes/notes/vqe-with-estimator-primitive-7cbcc462ad4dc593.yaml @@ -4,7 +4,6 @@ features: Added :class:`qiskit.algorithms.minimum_eigensolvers` package to include interfaces for primitive-enabled algorithms. Refactored :class:`qiskit.algorithms.minimum_eigensolvers.VQE` to leverage primitives. - Please see the Qiskit Runtime migration guide for more information. deprecations: - | :class:`qiskit.algorithms.minimum_eigen_solvers` package will now issue a diff --git a/test/python/algorithms/utils/test_validate_bounds.py b/test/python/algorithms/utils/test_validate_bounds.py index 4958e9f380d9..1f97aa1e4c48 100644 --- a/test/python/algorithms/utils/test_validate_bounds.py +++ b/test/python/algorithms/utils/test_validate_bounds.py @@ -18,7 +18,7 @@ import numpy as np -from qiskit.algorithms.utils import _validate_bounds +from qiskit.algorithms.utils import validate_bounds from qiskit.utils import algorithm_globals @@ -35,14 +35,14 @@ def test_with_no_ansatz_bounds(self): """Test with no ansatz bounds.""" self.ansatz.num_parameters = 1 self.ansatz.parameter_bounds = None - bounds = _validate_bounds(self.ansatz) + bounds = validate_bounds(self.ansatz) self.assertEqual(bounds, [(None, None)]) def test_with_ansatz_bounds(self): """Test with ansatz bounds.""" self.ansatz.num_parameters = 1 self.ansatz.parameter_bounds = self.bounds - bounds = _validate_bounds(self.ansatz) + bounds = validate_bounds(self.ansatz) self.assertEqual(bounds, self.bounds) def test_with_mismatched_num_params(self): @@ -50,4 +50,4 @@ def test_with_mismatched_num_params(self): self.ansatz.num_parameters = 2 self.ansatz.parameter_bounds = self.bounds with self.assertRaises(ValueError): - _ = _validate_bounds(self.ansatz) + _ = validate_bounds(self.ansatz) diff --git a/test/python/algorithms/utils/test_validate_initial_point.py b/test/python/algorithms/utils/test_validate_initial_point.py index e38bc90e18b1..65fb5f79f8e0 100644 --- a/test/python/algorithms/utils/test_validate_initial_point.py +++ b/test/python/algorithms/utils/test_validate_initial_point.py @@ -34,17 +34,17 @@ def setUp(self): def test_with_no_initial_point_or_bounds(self): """Test with no user-defined initial point and no ansatz bounds.""" self.ansatz.parameter_bounds = None - initial_point = _validate_initial_point(None, self.ansatz) + initial_point = validate_initial_point(None, self.ansatz) np.testing.assert_array_almost_equal(initial_point, [1.721111]) def test_with_no_initial_point(self): """Test with no user-defined initial point with ansatz bounds.""" self.ansatz.parameter_bounds = [(-np.pi / 2, np.pi / 2)] - initial_point = _validate_initial_point(None, self.ansatz) + initial_point = validate_initial_point(None, self.ansatz) np.testing.assert_array_almost_equal(initial_point, [0.430278]) def test_with_mismatched_params(self): """Test with mistmatched parameters and bounds..""" self.ansatz.parameter_bounds = None with self.assertRaises(ValueError): - _ = _validate_initial_point([1.0, 2.0], self.ansatz) + _ = validate_initial_point([1.0, 2.0], self.ansatz) From f06a74c9c4926509388635c2b5112f3219347664 Mon Sep 17 00:00:00 2001 From: Declan Millar Date: Mon, 26 Sep 2022 10:12:51 +0100 Subject: [PATCH 074/100] Do not copy ansatz --- qiskit/algorithms/minimum_eigensolvers/vqe.py | 58 +++++++++---------- 1 file changed, 26 insertions(+), 32 deletions(-) diff --git a/qiskit/algorithms/minimum_eigensolvers/vqe.py b/qiskit/algorithms/minimum_eigensolvers/vqe.py index e580cf84a0ff..2c62bbd994db 100644 --- a/qiskit/algorithms/minimum_eigensolvers/vqe.py +++ b/qiskit/algorithms/minimum_eigensolvers/vqe.py @@ -161,18 +161,18 @@ def compute_minimum_eigenvalue( operator: BaseOperator | PauliSumOp, aux_operators: ListOrDict[BaseOperator | PauliSumOp] | None = None, ) -> VQEResult: - ansatz = self._check_operator_ansatz(operator) + self._check_operator_ansatz(operator) - initial_point = _validate_initial_point(self.initial_point, ansatz) + initial_point = _validate_initial_point(self.initial_point, self.ansatz) - bounds = _validate_bounds(ansatz) + bounds = _validate_bounds(self.ansatz) start_time = time() - evaluate_energy = self._get_evaluate_energy(ansatz, operator) + evaluate_energy = self._get_evaluate_energy(self.ansatz, operator) if self.gradient is not None: - evaluate_gradient = self._get_evaluate_gradient(ansatz, operator) + evaluate_gradient = self._get_evaluate_gradient(self.ansatz, operator) else: evaluate_gradient = None @@ -195,12 +195,12 @@ def compute_minimum_eigenvalue( ) if aux_operators is not None: - bound_ansatz = ansatz.bind_parameters(optimizer_result.x) + bound_ansatz = self.ansatz.bind_parameters(optimizer_result.x) aux_values = estimate_observables(self.estimator, bound_ansatz, aux_operators) else: aux_values = None - return _build_vqe_result(optimizer_result, eval_time, ansatz, aux_values) + return _build_vqe_result(optimizer_result, aux_values, eval_time) @classmethod def supports_aux_operators(cls) -> bool: @@ -283,19 +283,16 @@ def evaluate_gradient(parameters: np.ndarray) -> np.ndarray: return evaluate_gradient - def _check_operator_ansatz(self, operator: BaseOperator | PauliSumOp) -> QuantumCircuit: + def _check_operator_ansatz(self, operator: BaseOperator | PauliSumOp): """Check that the number of qubits of operator and ansatz match and that the ansatz is parameterized. """ - # avoid changing the original ansatz as a user may not expect this behavior - ansatz = self.ansatz.copy() - - if operator.num_qubits != ansatz.num_qubits: + if operator.num_qubits != self.ansatz.num_qubits: try: logger.info( "Trying to resize ansatz to match operator on %s qubits.", operator.num_qubits ) - ansatz.num_qubits = operator.num_qubits + self.ansatz.num_qubits = operator.num_qubits except AttributeError as error: raise AlgorithmError( "The number of qubits of the ansatz does not match the " @@ -303,27 +300,24 @@ def _check_operator_ansatz(self, operator: BaseOperator | PauliSumOp) -> Quantum "number of qubits using `num_qubits`." ) from error - if ansatz.num_parameters == 0: + if self.ansatz.num_parameters == 0: raise AlgorithmError("The ansatz must be parameterized, but has no free parameters.") - return ansatz - - -def _build_vqe_result( - optimizer_result: OptimizerResult, - eval_time: float, - ansatz: QuantumCircuit, - aux_values: ListOrDict[tuple[complex, tuple[complex, int]]], -) -> VQEResult: - result = VQEResult() - result.eigenvalue = optimizer_result.fun - result.cost_function_evals = optimizer_result.nfev - result.optimal_point = optimizer_result.x - result.optimal_parameters = dict(zip(ansatz.parameters, optimizer_result.x)) - result.optimal_value = optimizer_result.fun - result.optimizer_time = eval_time - result.aux_operator_eigenvalues = aux_values - return result + def _build_vqe_result( + self, + optimizer_result: OptimizerResult, + aux_values: ListOrDict[tuple[complex, tuple[complex, int]]], + eval_time: float, + ) -> VQEResult: + result = VQEResult() + result.eigenvalue = optimizer_result.fun + result.cost_function_evals = optimizer_result.nfev + result.optimal_point = optimizer_result.x + result.optimal_parameters = dict(zip(self.ansatz.parameters, optimizer_result.x)) + result.optimal_value = optimizer_result.fun + result.optimizer_time = eval_time + result.aux_operator_eigenvalues = aux_values + return result class VQEResult(VariationalResult, MinimumEigensolverResult): From 9cfb1b22a3301900d82f3f200c9f4268a6f6b79d Mon Sep 17 00:00:00 2001 From: Declan Millar Date: Mon, 26 Sep 2022 10:18:55 +0100 Subject: [PATCH 075/100] Note pending depreciation of old algs --- qiskit/algorithms/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/qiskit/algorithms/__init__.py b/qiskit/algorithms/__init__.py index cebfbbbf3cec..11ee475f8043 100644 --- a/qiskit/algorithms/__init__.py +++ b/qiskit/algorithms/__init__.py @@ -187,6 +187,8 @@ --------------------- Algorithms that can find the minimum eigenvalue of an operator. +These algorithms are pending depreciation. One should instead make use of the +Minimum Eigensolver classes in the section below, which leverage Runtime primitives. .. autosummary:: :toctree: ../stubs/ From 3ec9dba792c7e0d667953d66e696766a33cde14f Mon Sep 17 00:00:00 2001 From: Declan Millar Date: Mon, 26 Sep 2022 12:32:25 +0100 Subject: [PATCH 076/100] fix docstrings and imports --- .../minimum_eigensolvers/__init__.py | 12 ++++----- .../numpy_minimum_eigensolver.py | 1 - qiskit/algorithms/minimum_eigensolvers/vqe.py | 10 ++++---- qiskit/algorithms/utils/validate_bounds.py | 25 ++++++++++++++----- .../utils/validate_initial_point.py | 25 ++++++++++++++++--- .../algorithms/utils/test_validate_bounds.py | 2 +- .../utils/test_validate_initial_point.py | 4 +-- 7 files changed, 54 insertions(+), 25 deletions(-) diff --git a/qiskit/algorithms/minimum_eigensolvers/__init__.py b/qiskit/algorithms/minimum_eigensolvers/__init__.py index b3208de4c3fd..9da052fab6ff 100644 --- a/qiskit/algorithms/minimum_eigensolvers/__init__.py +++ b/qiskit/algorithms/minimum_eigensolvers/__init__.py @@ -10,30 +10,30 @@ # copyright notice, and modified files need to carry a notice indicating # that they have been altered from the originals. -===================================================================== +""" +============================================================================ Minimum Eigensolvers Package (:mod:`qiskit.algorithms.minimum_eigensolvers`) -===================================================================== +============================================================================ .. currentmodule:: qiskit.algorithms.minimum_eigensolvers Minimum Eigensolvers -================ +==================== .. autosummary:: :toctree: ../stubs/ MinimumEigensolver NumPyMinimumEigensolver - QAOA VQE - + .. autosummary:: :toctree: ../stubs/ MinimumEigensolverResult NumPyMinimumEigensolverResult VQEResult +""" - from .minimum_eigensolver import MinimumEigensolver, MinimumEigensolverResult from .numpy_minimum_eigensolver import NumPyMinimumEigensolver, NumPyMinimumEigensolverResult from .vqe import VQE, VQEResult diff --git a/qiskit/algorithms/minimum_eigensolvers/numpy_minimum_eigensolver.py b/qiskit/algorithms/minimum_eigensolvers/numpy_minimum_eigensolver.py index ee752b936627..eb021f926109 100644 --- a/qiskit/algorithms/minimum_eigensolvers/numpy_minimum_eigensolver.py +++ b/qiskit/algorithms/minimum_eigensolvers/numpy_minimum_eigensolver.py @@ -20,7 +20,6 @@ from qiskit.opflow import PauliSumOp from qiskit.quantum_info.operators.base_operator import BaseOperator -from qiskit.utils.deprecation import deprecate_function # TODO this path will need updating from ..eigen_solvers.numpy_eigen_solver import NumPyEigensolver diff --git a/qiskit/algorithms/minimum_eigensolvers/vqe.py b/qiskit/algorithms/minimum_eigensolvers/vqe.py index 2c62bbd994db..379f1dc3cff4 100644 --- a/qiskit/algorithms/minimum_eigensolvers/vqe.py +++ b/qiskit/algorithms/minimum_eigensolvers/vqe.py @@ -32,7 +32,7 @@ from ..variational_algorithm import VariationalAlgorithm, VariationalResult from .minimum_eigensolver import MinimumEigensolver, MinimumEigensolverResult from ..observables_evaluator import estimate_observables -from ..utils import _validate_initial_point, _validate_bounds +from ..utils import validate_initial_point, validate_bounds logger = logging.getLogger(__name__) @@ -120,7 +120,7 @@ def __init__( initial_point: Sequence[float] | None = None, callback: Callable[[int, np.ndarray, float, dict], None] | None = None, ) -> None: - """ + r""" Args: estimator: The estimator primitive to compute the expectation value of the Hamiltonian operator. @@ -163,9 +163,9 @@ def compute_minimum_eigenvalue( ) -> VQEResult: self._check_operator_ansatz(operator) - initial_point = _validate_initial_point(self.initial_point, self.ansatz) + initial_point = validate_initial_point(self.initial_point, self.ansatz) - bounds = _validate_bounds(self.ansatz) + bounds = validate_bounds(self.ansatz) start_time = time() @@ -200,7 +200,7 @@ def compute_minimum_eigenvalue( else: aux_values = None - return _build_vqe_result(optimizer_result, aux_values, eval_time) + return self._build_vqe_result(optimizer_result, aux_values, eval_time) @classmethod def supports_aux_operators(cls) -> bool: diff --git a/qiskit/algorithms/utils/validate_bounds.py b/qiskit/algorithms/utils/validate_bounds.py index 515ccbd1822d..65f00cfcbd79 100644 --- a/qiskit/algorithms/utils/validate_bounds.py +++ b/qiskit/algorithms/utils/validate_bounds.py @@ -17,15 +17,28 @@ from qiskit.circuit import QuantumCircuit -def validate_bounds(ansatz: QuantumCircuit) -> list[tuple(float | None, float | None)]: - if hasattr(ansatz, "parameter_bounds") and ansatz.parameter_bounds is not None: - bounds = ansatz.parameter_bounds - if len(bounds) != ansatz.num_parameters: +def validate_bounds(circuit: QuantumCircuit) -> list[tuple(float | None, float | None)]: + """ + Validate the bounds provided by a quantum circuit against its number of parameters. + If no bounds are obtained, return ``None`` for all lower and upper bounds. + + Args: + circuit: A parameterized quantum circuit. + + Returns: + A list of tuples (lower_bound, upper_bound)). + + Raises: + ValueError: If the number of bounds does not the match the number of circuit parameters. + """ + if hasattr(circuit, "parameter_bounds") and circuit.parameter_bounds is not None: + bounds = circuit.parameter_bounds + if len(bounds) != circuit.num_parameters: raise ValueError( f"The number of bounds ({len(bounds)}) does not match the number of " - f"parameters in the circuit ({ansatz.num_parameters})." + f"parameters in the circuit ({circuit.num_parameters})." ) else: - bounds = [(None, None)] * ansatz.num_parameters + bounds = [(None, None)] * circuit.num_parameters return bounds diff --git a/qiskit/algorithms/utils/validate_initial_point.py b/qiskit/algorithms/utils/validate_initial_point.py index 31c078c2fc2a..15e871cdd473 100644 --- a/qiskit/algorithms/utils/validate_initial_point.py +++ b/qiskit/algorithms/utils/validate_initial_point.py @@ -23,13 +23,30 @@ def validate_initial_point( - point: Sequence[float] | None, ansatz: QuantumCircuit + point: Sequence[float] | None, circuit: QuantumCircuit ) -> Sequence[float]: - expected_size = ansatz.num_parameters + r""" + Validate a choice of initial point against a choice of circuit. If no point is provided, a + random point will be generated within certain parameter bounds. It will first look to the + circuit for these bounds. If the circuit does not specify bounds, bounds of :math:`-2\pi`, + :math:`2\pi` will be used. + + Args: + point: An initial point. + circuit: A parameterized quantum circuit. + + Returns: + A validated initial point. + + Raises: + ValueError: If the dimension of the initial point does not match the number of circuit + parameters. + """ + expected_size = circuit.num_parameters if point is None: - # get bounds if ansatz has them set, otherwise use [-2pi, 2pi] for each parameter - bounds = getattr(ansatz, "parameter_bounds", None) + # get bounds if circuit has them set, otherwise use [-2pi, 2pi] for each parameter + bounds = getattr(circuit, "parameter_bounds", None) if bounds is None: bounds = [(-2 * np.pi, 2 * np.pi)] * expected_size diff --git a/test/python/algorithms/utils/test_validate_bounds.py b/test/python/algorithms/utils/test_validate_bounds.py index 1f97aa1e4c48..80fe54ee0787 100644 --- a/test/python/algorithms/utils/test_validate_bounds.py +++ b/test/python/algorithms/utils/test_validate_bounds.py @@ -23,7 +23,7 @@ class TestValidateBounds(QiskitAlgorithmsTestCase): - """Test the ``_validate_bounds`` utility function.""" + """Test the ``validate_bounds`` utility function.""" def setUp(self): super().setUp() diff --git a/test/python/algorithms/utils/test_validate_initial_point.py b/test/python/algorithms/utils/test_validate_initial_point.py index 65fb5f79f8e0..32dd48cf2c95 100644 --- a/test/python/algorithms/utils/test_validate_initial_point.py +++ b/test/python/algorithms/utils/test_validate_initial_point.py @@ -18,12 +18,12 @@ import numpy as np -from qiskit.algorithms.utils import _validate_initial_point +from qiskit.algorithms.utils import validate_initial_point from qiskit.utils import algorithm_globals class TestValidateInitialPoint(QiskitAlgorithmsTestCase): - """Test the ``_validate_initial_point`` utility function.""" + """Test the ``validate_initial_point`` utility function.""" def setUp(self): super().setUp() From 4d7a0fd8d31122af5f2ba28f439b9defe50299d1 Mon Sep 17 00:00:00 2001 From: Declan Millar Date: Mon, 26 Sep 2022 13:35:39 +0100 Subject: [PATCH 077/100] Fix issues with building docs --- qiskit/algorithms/__init__.py | 1 + ...ers-and-setters-for-vqe-edc753591b368980.yaml | 6 ++++-- ...-dict-for-aux-operators-c3c9ad380c208afd.yaml | 6 ++++-- ...aper_empty_operator_fix-53ce20e5d2b68fd6.yaml | 16 ++++++++-------- 4 files changed, 17 insertions(+), 12 deletions(-) diff --git a/qiskit/algorithms/__init__.py b/qiskit/algorithms/__init__.py index 11ee475f8043..ff23c523eac9 100644 --- a/qiskit/algorithms/__init__.py +++ b/qiskit/algorithms/__init__.py @@ -212,6 +212,7 @@ .. autosummary:: :toctree: ../stubs/ + minimum_eigensolvers diff --git a/releasenotes/notes/0.19/add-getters-and-setters-for-vqe-edc753591b368980.yaml b/releasenotes/notes/0.19/add-getters-and-setters-for-vqe-edc753591b368980.yaml index a1f20e922017..1d6ca9b82665 100644 --- a/releasenotes/notes/0.19/add-getters-and-setters-for-vqe-edc753591b368980.yaml +++ b/releasenotes/notes/0.19/add-getters-and-setters-for-vqe-edc753591b368980.yaml @@ -3,8 +3,10 @@ features: - | Every attribute of the :class:`~qiskit.algorithms.VQE` class that is set at the initialization is now accessible with getters and setters. Further, the - default values of the VQE attributes :attr:`~.VQE.ansatz` and - :attr:`~.VQE.optimizer` can be reset by assigning ``None`` to them:: + default values of the VQE attributes + :attr:`~qiskit.algorithms.minimimum_eigen_solvers.VQE.ansatz` and + :attr:`~qiskit.algorithms.minimimum_eigen_solvers.VQE.optimizer` can be + reset by assigning ``None`` to them:: vqe = VQE(my_ansatz, my_optimizer) vqe.ansatz = None # reset to default: RealAmplitudes ansatz diff --git a/releasenotes/notes/0.19/support-dict-for-aux-operators-c3c9ad380c208afd.yaml b/releasenotes/notes/0.19/support-dict-for-aux-operators-c3c9ad380c208afd.yaml index 5fef8f5d6483..63af5ca0dc66 100644 --- a/releasenotes/notes/0.19/support-dict-for-aux-operators-c3c9ad380c208afd.yaml +++ b/releasenotes/notes/0.19/support-dict-for-aux-operators-c3c9ad380c208afd.yaml @@ -1,9 +1,11 @@ --- features: - | - The :obj:`.Eigensolver` and :obj:`.MinimumEigensolver` interfaces now support the type + The :obj:`.Eigensolver` and :obj:`~qiskit.algorithms.minimimum_eigen_solvers.MinimumEigensolver` + interfaces now support the type ``Dict[str, Optional[OperatorBase]]`` for the ``aux_operators`` parameter in their respective - :meth:`~.Eigensolver.compute_eigenvalues` and :meth:`~.MinimumEigensolver.compute_minimum_eigenvalue` methods. + :meth:`~.Eigensolver.compute_eigenvalues` and + :meth:`~qiskit.algorithms.minimimum_eigen_solvers.MinimumEigensolver.compute_minimum_eigenvalue` methods. In this case, the auxiliary eigenvalues are also stored in a dictionary under the same keys provided by the ``aux_operators`` dictionary. Keys that correspond to an operator that does not commute with the main operator are dropped. diff --git a/releasenotes/notes/0.19/taper_empty_operator_fix-53ce20e5d2b68fd6.yaml b/releasenotes/notes/0.19/taper_empty_operator_fix-53ce20e5d2b68fd6.yaml index b02a9dde71ac..a719f78310bc 100644 --- a/releasenotes/notes/0.19/taper_empty_operator_fix-53ce20e5d2b68fd6.yaml +++ b/releasenotes/notes/0.19/taper_empty_operator_fix-53ce20e5d2b68fd6.yaml @@ -1,11 +1,11 @@ --- fixes: - | - When tapering an empty zero operator in :mod:`qiskit.opflow`, the code, on detecting it was zero, logged a - warning and returned the original operator. Such operators are commonly found in - the auxiliary operators, when using Qiskit Nature, and the above behavior caused :obj:`.VQE` - to throw an exception as tapered non-zero operators were a different number of qubits - from the tapered zero operators (since taper has returned the input operator unchanged). - The code will now correctly taper a zero operator such that the number of qubits is - reduced as expected and matches to tapered non-zero operators e.g ```0*"IIII"``` when we are - tapering by 3 qubits will become ``0*"I"``. + When tapering an empty zero operator in :mod:`qiskit.opflow`, the code, on detecting it was + zero, logged a warning and returned the original operator. Such operators are commonly found in + the auxiliary operators, when using Qiskit Nature, and the above behavior caused + :obj:`~qiskit.algorithms.minimimum_eigen_solvers.VQE` to throw an exception as tapered non-zero + operators were a different number of qubits from the tapered zero operators (since taper has + returned the input operator unchanged). The code will now correctly taper a zero operator such + that the number of qubits is reduced as expected and matches to tapered non-zero operators e.g + ```0*"IIII"``` when we are tapering by 3 qubits will become ``0*"I"``. From 4a16903efb3de0057f778f80f3c4a724c8070952 Mon Sep 17 00:00:00 2001 From: Declan Millar Date: Mon, 26 Sep 2022 13:36:56 +0100 Subject: [PATCH 078/100] Include OptimizerResult in VQEResult --- qiskit/algorithms/minimum_eigensolvers/vqe.py | 8 ++------ qiskit/algorithms/variational_algorithm.py | 11 +++++++++++ .../algorithms/minimum_eigensolvers/test_vqe.py | 6 ++++++ 3 files changed, 19 insertions(+), 6 deletions(-) diff --git a/qiskit/algorithms/minimum_eigensolvers/vqe.py b/qiskit/algorithms/minimum_eigensolvers/vqe.py index 379f1dc3cff4..eda2f293d621 100644 --- a/qiskit/algorithms/minimum_eigensolvers/vqe.py +++ b/qiskit/algorithms/minimum_eigensolvers/vqe.py @@ -95,11 +95,6 @@ def my_minimizer(fun, x0, jac=None, bounds=None) -> OptimizerResult: :class:`.Minimizer` protocol. gradient (BaseEstimatorGradient | None): An optional estimator gradient to be used with the optimizer. - initial_point (Sequence[float] | None): An optional initial point (i.e. initial parameter - values) for the optimizer. The length of the initial point must match the number of - :attr:`ansatz` parameters. If ``None``, a random point will be generated within certain - parameter bounds. ``VQE`` will look to the ansatz for these bounds. If the ansatz does - not specify bounds, bounds of :math:`-2\pi`, :math:`2\pi` will be used. callback (Callable[[int, np.ndarray, float, dict], None] | None): A callback that can access the intermediate data at each optimization step. These data are: the evaluation count, the optimizer parameters for the ansatz, the evaluated mean, and the metadata @@ -107,7 +102,7 @@ def my_minimizer(fun, x0, jac=None, bounds=None) -> OptimizerResult: References: [1] Peruzzo et al, "A variational eigenvalue solver on a quantum processor" - `arXiv:1304.3061 https://arxiv.org/abs/1304.3061>`_ + `arXiv:1304.3061 `__ """ def __init__( @@ -317,6 +312,7 @@ def _build_vqe_result( result.optimal_value = optimizer_result.fun result.optimizer_time = eval_time result.aux_operator_eigenvalues = aux_values + result.optimizer_result = optimizer_result return result diff --git a/qiskit/algorithms/variational_algorithm.py b/qiskit/algorithms/variational_algorithm.py index 92d8976ffc9f..0be89abf6bef 100644 --- a/qiskit/algorithms/variational_algorithm.py +++ b/qiskit/algorithms/variational_algorithm.py @@ -31,6 +31,7 @@ import numpy as np from .algorithm_result import AlgorithmResult +from .optimizers import OptimizerResult class VariationalAlgorithm(ABC): @@ -109,3 +110,13 @@ def optimal_parameters(self) -> Optional[Dict]: def optimal_parameters(self, value: Dict) -> None: """Sets optimal parameters""" self._optimal_parameters = value + + @property + def optimizer_result(self) -> Optional[OptimizerResult]: + """Returns the optimizer result""" + return self._optimizer_result + + @optimizer_result.setter + def optimizer_result(self, value: OptimizerResult) -> None: + """Sets optimizer result""" + self._optimizer_result = value diff --git a/test/python/algorithms/minimum_eigensolvers/test_vqe.py b/test/python/algorithms/minimum_eigensolvers/test_vqe.py index 66655220d263..ad98f37bbbf8 100644 --- a/test/python/algorithms/minimum_eigensolvers/test_vqe.py +++ b/test/python/algorithms/minimum_eigensolvers/test_vqe.py @@ -101,6 +101,12 @@ def test_basic_aer_statevector(self, estimator): with self.subTest(msg="assert optimizer_time is set"): self.assertIsNotNone(result.optimizer_time) + with self.subTest(msg="assert optimizer_result is set"): + self.assertIsNotNone(result.optimizer_result) + + with self.subTest(msg="assert optimizer_result."): + self.assertAlmostEqual(result.optimizer_result.fun, self.h2_energy, places=5) + def test_invalid_initial_point(self): """Test the proper error is raised when the initial point has the wrong size.""" ansatz = self.ryrz_wavefunction From 6a1554006e8e43c6ad76861e086f6124c5c47c4a Mon Sep 17 00:00:00 2001 From: Declan Millar Date: Mon, 26 Sep 2022 14:00:24 +0100 Subject: [PATCH 079/100] Remove trailing whitespace --- qiskit/algorithms/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qiskit/algorithms/__init__.py b/qiskit/algorithms/__init__.py index ff23c523eac9..55648a040aa4 100644 --- a/qiskit/algorithms/__init__.py +++ b/qiskit/algorithms/__init__.py @@ -212,7 +212,7 @@ .. autosummary:: :toctree: ../stubs/ - + minimum_eigensolvers From 4fdf4f548b35f09448df6e90d8bc41aa2aabc589 Mon Sep 17 00:00:00 2001 From: Declan Millar Date: Mon, 26 Sep 2022 15:46:51 +0100 Subject: [PATCH 080/100] Fix math notation in docstring --- qiskit/algorithms/minimum_eigensolvers/vqe.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/qiskit/algorithms/minimum_eigensolvers/vqe.py b/qiskit/algorithms/minimum_eigensolvers/vqe.py index eda2f293d621..72e736c72393 100644 --- a/qiskit/algorithms/minimum_eigensolvers/vqe.py +++ b/qiskit/algorithms/minimum_eigensolvers/vqe.py @@ -41,7 +41,7 @@ class VQE(VariationalAlgorithm, MinimumEigensolver): r"""The variational quantum eigensolver (VQE) algorithm. VQE is a hybrid quantum-classical algorithm that uses a variational technique to find the - minimum eigenvalue of a given Hamiltonian operator :math:`\vec\theta`. + minimum eigenvalue of a given Hamiltonian operator :math:`H`. The VQE algorithm is executed using an :attr:`estimator` primitive, which must be specified as the first argument on instantiation. @@ -49,9 +49,9 @@ class VQE(VariationalAlgorithm, MinimumEigensolver): An instance of ``VQE`` also requires an :attr:`ansatz` parameterized :class:`.QuantumCircuit` to prepare the trial state :math:`|\psi(\vec\theta)\rangle`, as well as a classical :attr:`optimizer`. The optimizer varies the circuit parameters :math:`\vec\theta` such that the - expectation value of the operator :math:`H` on the corresponding state approaches a minimum, + expectation value of the operator on the corresponding state approaches a minimum, - ..math:: + .. math:: \min_{\vec\theta} \langle\psi(\vec\theta)|H|\psi(\vec\theta)\rangle. From 355d8877db7eb8dd5a1dcc2c4314e88e4bf23b92 Mon Sep 17 00:00:00 2001 From: Declan Millar Date: Mon, 26 Sep 2022 16:03:38 +0100 Subject: [PATCH 081/100] estimate_obervables to return metadata @ElePT +VQE --- qiskit/algorithms/observables_evaluator.py | 45 +-------------- .../minimum_eigensolvers/test_vqe.py | 56 +++++++------------ .../algorithms/test_observables_evaluator.py | 32 ++++++++++- 3 files changed, 53 insertions(+), 80 deletions(-) diff --git a/qiskit/algorithms/observables_evaluator.py b/qiskit/algorithms/observables_evaluator.py index 9f6f60104515..afc7afa95b56 100644 --- a/qiskit/algorithms/observables_evaluator.py +++ b/qiskit/algorithms/observables_evaluator.py @@ -18,7 +18,7 @@ from qiskit.opflow import PauliSumOp from .exceptions import AlgorithmError from .list_or_dict import ListOrDict -from ..primitives import EstimatorResult, BaseEstimator +from ..primitives import BaseEstimator from ..quantum_info.operators.base_operator import BaseOperator @@ -32,7 +32,6 @@ def estimate_observables( Accepts a sequence of operators and calculates their expectation values - means and standard deviations. They are calculated with respect to a quantum state provided. A user can optionally provide a threshold value which filters mean values falling below the threshold. - Args: estimator: An estimator primitive used for calculations. quantum_state: An unparametrized quantum circuit representing a quantum state that @@ -41,10 +40,8 @@ def estimate_observables( calculated. threshold: A threshold value that defines which mean values should be neglected (helpful for ignoring numerical instabilities close to 0). - Returns: A list or a dictionary of tuples (mean, (variance, shots)). - Raises: ValueError: If a ``quantum_state`` with free parameters is provided. AlgorithmError: If a primitive job is not successful. @@ -71,12 +68,11 @@ def estimate_observables( except Exception as exc: raise AlgorithmError("The primitive job failed!") from exc - variance_and_shots = _prep_variance_and_shots(estimator_job.result(), len(expectation_values)) - + metadata = estimator_job.result().metadata # Discard values below threshold observables_means = expectation_values * (np.abs(expectation_values) > threshold) # zip means and standard deviations into tuples - observables_results = list(zip(observables_means, variance_and_shots)) + observables_results = list(zip(observables_means, metadata)) return _prepare_result(observables_results, observables) @@ -101,12 +97,10 @@ def _prepare_result( """ Prepares a list of tuples of eigenvalues and (variance, shots) tuples from ``observables_results`` and ``observables``. - Args: observables_results: A list of tuples (mean, (variance, shots)). observables: A list or a dictionary of operators whose expectation values are to be calculated. - Returns: A list or a dictionary of tuples (mean, (variance, shots)). """ @@ -122,36 +116,3 @@ def _prepare_result( for key, value in key_value_iterator: observables_eigenvalues[key] = value return observables_eigenvalues - - -def _prep_variance_and_shots( - estimator_result: EstimatorResult, - results_length: int, -) -> list[tuple[complex, int]]: - """ - Prepares a list of tuples with variances and shots from results provided by expectation values - calculations. If there is no variance or shots data available from a primitive, the values will - be set to ``0``. - - Args: - estimator_result: An estimator result. - results_length: Number of expectation values calculated. - - Returns: - A list of tuples of the form (variance, shots). - """ - if not estimator_result.metadata: - return [(0, 0)] * results_length - - results = [] - for metadata in estimator_result.metadata: - variance, shots = 0.0, 0 - if metadata: - if "variance" in metadata.keys(): - variance = metadata["variance"] - if "shots" in metadata.keys(): - shots = metadata["shots"] - - results.append((variance, shots)) - - return results diff --git a/test/python/algorithms/minimum_eigensolvers/test_vqe.py b/test/python/algorithms/minimum_eigensolvers/test_vqe.py index ad98f37bbbf8..171b52b9bcca 100644 --- a/test/python/algorithms/minimum_eigensolvers/test_vqe.py +++ b/test/python/algorithms/minimum_eigensolvers/test_vqe.py @@ -377,35 +377,28 @@ def test_aux_operators_list(self): aux_op2 = PauliSumOp.from_list([("II", 0.5), ("ZZ", 0.5), ("YY", 0.5), ("XX", -0.5)]) aux_ops = [aux_op1, aux_op2] result = vqe.compute_minimum_eigenvalue(self.h2_op, aux_operators=aux_ops) - self.assertAlmostEqual(result.eigenvalue.real, self.h2_energy, places=6) + self.assertAlmostEqual(result.eigenvalue.real, self.h2_energy, places=5) self.assertEqual(len(result.aux_operator_eigenvalues), 2) # expectation values self.assertAlmostEqual(result.aux_operator_eigenvalues[0][0], 2.0, places=6) self.assertAlmostEqual(result.aux_operator_eigenvalues[1][0], 0.0, places=6) - # variances - self.assertEqual(result.aux_operator_eigenvalues[0][1][0], 0.0) - self.assertEqual(result.aux_operator_eigenvalues[1][1][0], 0.0) - # shots - self.assertEqual(result.aux_operator_eigenvalues[0][1][1], 0) - self.assertEqual(result.aux_operator_eigenvalues[1][1][1], 0) + # metadata + self.assertIsInstance(result.aux_operator_eigenvalues[0][1], dict) + self.assertIsInstance(result.aux_operator_eigenvalues[1][1], dict) with self.subTest("Test with additional zero operator."): extra_ops = [*aux_ops, 0] result = vqe.compute_minimum_eigenvalue(self.h2_op, aux_operators=extra_ops) - self.assertAlmostEqual(result.eigenvalue.real, self.h2_energy, places=6) + self.assertAlmostEqual(result.eigenvalue.real, self.h2_energy, places=5) self.assertEqual(len(result.aux_operator_eigenvalues), 3) # expectation values self.assertAlmostEqual(result.aux_operator_eigenvalues[0][0], 2.0, places=6) self.assertAlmostEqual(result.aux_operator_eigenvalues[1][0], 0.0, places=6) self.assertAlmostEqual(result.aux_operator_eigenvalues[2][0], 0.0) - # variances - self.assertEqual(result.aux_operator_eigenvalues[0][1][0], 0.0) - self.assertEqual(result.aux_operator_eigenvalues[1][1][0], 0.0) - self.assertEqual(result.aux_operator_eigenvalues[2][1][0], 0.0) - # shots - self.assertEqual(result.aux_operator_eigenvalues[0][1][1], 0) - self.assertEqual(result.aux_operator_eigenvalues[1][1][1], 0) - self.assertEqual(result.aux_operator_eigenvalues[2][1][1], 0) + # metadata + self.assertIsInstance(result.aux_operator_eigenvalues[0][1], dict) + self.assertIsInstance(result.aux_operator_eigenvalues[1][1], dict) + self.assertIsInstance(result.aux_operator_eigenvalues[2][1], dict) def test_aux_operators_dict(self): """Test dictionary compatibility of aux_operators""" @@ -426,14 +419,11 @@ def test_aux_operators_dict(self): self.assertEqual(len(result.aux_operator_eigenvalues), 2) # expectation values - self.assertAlmostEqual(result.aux_operator_eigenvalues["aux_op1"][0], 2, places=5) - self.assertAlmostEqual(result.aux_operator_eigenvalues["aux_op2"][0], 0, places=5) - # variances - self.assertEqual(result.aux_operator_eigenvalues["aux_op1"][1][0], 0.0) - self.assertEqual(result.aux_operator_eigenvalues["aux_op2"][1][0], 0.0) - # shots - self.assertEqual(result.aux_operator_eigenvalues["aux_op1"][1][1], 0) - self.assertEqual(result.aux_operator_eigenvalues["aux_op2"][1][1], 0) + self.assertAlmostEqual(result.aux_operator_eigenvalues["aux_op1"][0], 2.0, places=5) + self.assertAlmostEqual(result.aux_operator_eigenvalues["aux_op2"][0], 0.0, places=5) + # metadata + self.assertIsInstance(result.aux_operator_eigenvalues["aux_op1"][1], dict) + self.assertIsInstance(result.aux_operator_eigenvalues["aux_op2"][1], dict) with self.subTest("Test with additional zero operator."): extra_ops = {**aux_ops, "zero_operator": 0} @@ -441,19 +431,13 @@ def test_aux_operators_dict(self): self.assertAlmostEqual(result.eigenvalue.real, self.h2_energy, places=6) self.assertEqual(len(result.aux_operator_eigenvalues), 3) # expectation values - self.assertAlmostEqual(result.aux_operator_eigenvalues["aux_op1"][0], 2, places=5) - self.assertAlmostEqual(result.aux_operator_eigenvalues["aux_op2"][0], 0, places=5) + self.assertAlmostEqual(result.aux_operator_eigenvalues["aux_op1"][0], 2.0, places=5) + self.assertAlmostEqual(result.aux_operator_eigenvalues["aux_op2"][0], 0.0, places=5) self.assertAlmostEqual(result.aux_operator_eigenvalues["zero_operator"][0], 0.0) - # variances - self.assertEqual(result.aux_operator_eigenvalues["aux_op1"][1][0], 0.0) - self.assertEqual(result.aux_operator_eigenvalues["aux_op2"][1][0], 0.0) - self.assertEqual(result.aux_operator_eigenvalues["zero_operator"][1][0], 0.0) - # shots - self.assertEqual(result.aux_operator_eigenvalues["aux_op1"][1][1], 0) - self.assertEqual(result.aux_operator_eigenvalues["aux_op2"][1][1], 0) - self.assertEqual(result.aux_operator_eigenvalues["zero_operator"][1][1], 0) - - # TODO test with non-zero metadata. Affected by PR #8105. + # metadata + self.assertIsInstance(result.aux_operator_eigenvalues["aux_op1"][1], dict) + self.assertIsInstance(result.aux_operator_eigenvalues["aux_op2"][1], dict) + self.assertIsInstance(result.aux_operator_eigenvalues["zero_operator"][1], dict) if __name__ == "__main__": diff --git a/test/python/algorithms/test_observables_evaluator.py b/test/python/algorithms/test_observables_evaluator.py index bcff77cdd5b4..cb43dc275b2d 100644 --- a/test/python/algorithms/test_observables_evaluator.py +++ b/test/python/algorithms/test_observables_evaluator.py @@ -53,7 +53,7 @@ def get_exact_expectation( # the exact value is a list of (mean, (variance, shots)) where we expect 0 variance and # 0 shots exact = [ - (Statevector(ansatz).expectation_value(observable), (0, 0)) + (Statevector(ansatz).expectation_value(observable), {}) for observable in observables_list ] @@ -143,7 +143,7 @@ def test_estimate_observables_zero_op(self): estimator = Estimator() observables = [SparsePauliOp(["XX", "YY"]), 0] result = estimate_observables(estimator, state, observables, self.threshold) - expected_result = [(0.015607318055509564, (0, 0)), (0.0, (0, 0))] + expected_result = [(0.015607318055509564, {}), (0.0, {})] means = [element[0] for element in result] expected_means = [element[0] for element in expected_result] np.testing.assert_array_almost_equal(means, expected_means, decimal=0.01) @@ -152,6 +152,34 @@ def test_estimate_observables_zero_op(self): expected_vars_and_shots = [element[1] for element in expected_result] np.testing.assert_array_equal(vars_and_shots, expected_vars_and_shots) + def test_estimate_observables_shots(self): + """Tests that variances and shots are returned properly.""" + ansatz = EfficientSU2(2) + parameters = np.array( + [1.2, 4.2, 1.4, 2.0, 1.2, 4.2, 1.4, 2.0, 1.2, 4.2, 1.4, 2.0, 1.2, 4.2, 1.4, 2.0], + dtype=float, + ) + + bound_ansatz = ansatz.bind_parameters(parameters) + state = bound_ansatz + estimator = Estimator(options={"shots": 2048}) + observables = [PauliSumOp.from_list([("ZZ", 2.0)])] + result = estimate_observables(estimator, state, observables, self.threshold) + exact_result = self.get_exact_expectation(bound_ansatz, observables) + expected_result = [(exact_result[0][0], {"variance": 1.0898, "shots": 2048})] + + means = [element[0] for element in result] + expected_means = [element[0] for element in expected_result] + np.testing.assert_array_almost_equal(means, expected_means, decimal=0.01) + + vars_and_shots = [element[1] for element in result] + print(vars_and_shots) + expected_vars_and_shots = [element[1] for element in expected_result] + print(expected_vars_and_shots) + for computed, expected in zip(vars_and_shots, expected_vars_and_shots): + + self.assertAlmostEqual(computed, expected, 2) + if __name__ == "__main__": unittest.main() From b0b8f378a41ba58f41b02f8c760be931197671b0 Mon Sep 17 00:00:00 2001 From: Declan Millar Date: Mon, 26 Sep 2022 16:41:28 +0100 Subject: [PATCH 082/100] add example in release note --- qiskit/algorithms/observables_evaluator.py | 2 + ...-estimator-primitive-7cbcc462ad4dc593.yaml | 41 ++++++++++++++----- .../algorithms/test_observables_evaluator.py | 2 + 3 files changed, 35 insertions(+), 10 deletions(-) diff --git a/qiskit/algorithms/observables_evaluator.py b/qiskit/algorithms/observables_evaluator.py index afc7afa95b56..cb42ba646df0 100644 --- a/qiskit/algorithms/observables_evaluator.py +++ b/qiskit/algorithms/observables_evaluator.py @@ -9,7 +9,9 @@ # 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. + """Evaluator of observables for algorithms.""" + from __future__ import annotations import numpy as np diff --git a/releasenotes/notes/vqe-with-estimator-primitive-7cbcc462ad4dc593.yaml b/releasenotes/notes/vqe-with-estimator-primitive-7cbcc462ad4dc593.yaml index 63f88699ac05..b500ac205544 100644 --- a/releasenotes/notes/vqe-with-estimator-primitive-7cbcc462ad4dc593.yaml +++ b/releasenotes/notes/vqe-with-estimator-primitive-7cbcc462ad4dc593.yaml @@ -1,13 +1,34 @@ --- features: - | - Added :class:`qiskit.algorithms.minimum_eigensolvers` package to include - interfaces for primitive-enabled algorithms. Refactored - :class:`qiskit.algorithms.minimum_eigensolvers.VQE` to leverage primitives. -deprecations: - - | - :class:`qiskit.algorithms.minimum_eigen_solvers` package will now issue a - ``PendingDeprecationWarning``. It will be deprecated in a future release and subsequently - removed after that. This is being replaced by the new - :class:`qiskit.algorithms.minimum_eigensolvers` package that will host primitive-enabled - algorithms. \ No newline at end of file + Added the :class:`qiskit.algorithms.minimum_eigensolvers` package to include interfaces for + primitive-enabled algorithms. :class:`~qiskit.algorithms.minimum_eigensolvers.VQE` has been + refactored in this implementation to leverage primitives. + + To use the new implementation with a reference primitive, one can do, for example: + + .. code-block:: python + + from qiskit.algorithms.minimum_eigensolvers import VQE + from qiskit.algorithms.optimizers import SLSQP + from qiskit.circuit.library import TwoLocal + from qiskit.primitives import Estimator + + h2_op = SparsePauliOp( + ["II", "IZ", "ZI", "ZZ", "XX"], + coeffs=[ + -1.052373245772859, + 0.39793742484318045, + -0.39793742484318045, + -0.01128010425623538, + 0.18093119978423156, + ], + ) + + estimator = Estimator() + ansatz = TwoLocal(rotation_blocks=["ry", "rz"], entanglement_blocks="cz") + optimizer = SLSQP() + + vqe = VQE(estimator, ansatz, optimizer) + result = vqe.compute_minimum_eigenvalue(h2_op) + eigenvalue = result.eigenvalue diff --git a/test/python/algorithms/test_observables_evaluator.py b/test/python/algorithms/test_observables_evaluator.py index cb43dc275b2d..bd89e430c493 100644 --- a/test/python/algorithms/test_observables_evaluator.py +++ b/test/python/algorithms/test_observables_evaluator.py @@ -9,7 +9,9 @@ # 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. + """Tests evaluator of auxiliary operators for algorithms.""" + from __future__ import annotations import unittest from typing import Tuple From c14468846a4b13f833a7aa3af70adf6b9aef946e Mon Sep 17 00:00:00 2001 From: Declan Millar Date: Mon, 26 Sep 2022 17:17:41 +0100 Subject: [PATCH 083/100] Update evaluate_observables docstring --- qiskit/algorithms/observables_evaluator.py | 23 +++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/qiskit/algorithms/observables_evaluator.py b/qiskit/algorithms/observables_evaluator.py index cb42ba646df0..11665c211c2a 100644 --- a/qiskit/algorithms/observables_evaluator.py +++ b/qiskit/algorithms/observables_evaluator.py @@ -29,11 +29,12 @@ def estimate_observables( quantum_state: QuantumCircuit, observables: ListOrDict[BaseOperator | PauliSumOp], threshold: float = 1e-12, -) -> ListOrDict[tuple[complex, tuple[complex, int]]]: +) -> ListOrDict[tuple[complex, dict]]: """ Accepts a sequence of operators and calculates their expectation values - means - and standard deviations. They are calculated with respect to a quantum state provided. A user + and metadata. They are calculated with respect to a quantum state provided. A user can optionally provide a threshold value which filters mean values falling below the threshold. + Args: estimator: An estimator primitive used for calculations. quantum_state: An unparametrized quantum circuit representing a quantum state that @@ -42,8 +43,10 @@ def estimate_observables( calculated. threshold: A threshold value that defines which mean values should be neglected (helpful for ignoring numerical instabilities close to 0). + Returns: - A list or a dictionary of tuples (mean, (variance, shots)). + A list or a dictionary of tuples (mean, metadata). + Raises: ValueError: If a ``quantum_state`` with free parameters is provided. AlgorithmError: If a primitive job is not successful. @@ -73,7 +76,7 @@ def estimate_observables( metadata = estimator_job.result().metadata # Discard values below threshold observables_means = expectation_values * (np.abs(expectation_values) > threshold) - # zip means and standard deviations into tuples + # zip means and metadata into tuples observables_results = list(zip(observables_means, metadata)) return _prepare_result(observables_results, observables) @@ -93,18 +96,20 @@ def _handle_zero_ops( def _prepare_result( - observables_results: list[tuple[complex, tuple[complex, int]]], + observables_results: list[tuple[complex, dict]], observables: ListOrDict[BaseOperator | PauliSumOp], -) -> ListOrDict[tuple[complex, tuple[complex, int]]]: +) -> ListOrDict[tuple[complex, dict]]: """ - Prepares a list of tuples of eigenvalues and (variance, shots) tuples from + Prepares a list of tuples of eigenvalues and metadata tuples from ``observables_results`` and ``observables``. + Args: - observables_results: A list of tuples (mean, (variance, shots)). + observables_results: A list of tuples (mean, metadata). observables: A list or a dictionary of operators whose expectation values are to be calculated. + Returns: - A list or a dictionary of tuples (mean, (variance, shots)). + A list or a dictionary of tuples (mean, metadata). """ if isinstance(observables, list): From 8fed1523363cf203e250a8f4af3bd684314ecdf4 Mon Sep 17 00:00:00 2001 From: Declan Millar Date: Mon, 26 Sep 2022 17:59:15 +0100 Subject: [PATCH 084/100] Fix observables_evaluator tests. Co-authored-by: ElePT <57907331+ElePT@users.noreply.github.com> --- test/python/algorithms/test_observables_evaluator.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/python/algorithms/test_observables_evaluator.py b/test/python/algorithms/test_observables_evaluator.py index bd89e430c493..693c1d8b563e 100644 --- a/test/python/algorithms/test_observables_evaluator.py +++ b/test/python/algorithms/test_observables_evaluator.py @@ -179,8 +179,8 @@ def test_estimate_observables_shots(self): expected_vars_and_shots = [element[1] for element in expected_result] print(expected_vars_and_shots) for computed, expected in zip(vars_and_shots, expected_vars_and_shots): - - self.assertAlmostEqual(computed, expected, 2) + self.assertAlmostEqual(computed.pop("variance"), expected.pop("variance"), 2) + self.assertEqual(computed.pop("shots"), expected.pop("shots")) if __name__ == "__main__": From 682d3cb497bada7a4eabe5eb269c7d609c58422e Mon Sep 17 00:00:00 2001 From: Declan Millar Date: Mon, 26 Sep 2022 18:02:56 +0100 Subject: [PATCH 085/100] fix trotter_qrte tests and remove depreciation --- qiskit/algorithms/minimum_eigen_solvers/vqe.py | 7 ------- .../algorithms/time_evolvers/test_trotter_qrte.py | 10 ++++++++-- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/qiskit/algorithms/minimum_eigen_solvers/vqe.py b/qiskit/algorithms/minimum_eigen_solvers/vqe.py index 71f2a5496bd0..8c02ebf6ad7c 100755 --- a/qiskit/algorithms/minimum_eigen_solvers/vqe.py +++ b/qiskit/algorithms/minimum_eigen_solvers/vqe.py @@ -644,13 +644,6 @@ def _get_eigenstate(self, optimal_parameters) -> Union[List[float], Dict[str, in class VQEResult(VariationalResult, MinimumEigensolverResult): """VQE Result.""" - @deprecate_function( - "The VQEResult class has been superseded by the " - "qiskit.algorithms.minimum_eigensolvers.VQEResult class. " - "This class will be deprecated in a future release and subsequently " - "removed after that.", - category=PendingDeprecationWarning, - ) def __init__(self) -> None: super().__init__() self._cost_function_evals = None diff --git a/test/python/algorithms/time_evolvers/test_trotter_qrte.py b/test/python/algorithms/time_evolvers/test_trotter_qrte.py index fbdbb0161590..16efa06549e2 100644 --- a/test/python/algorithms/time_evolvers/test_trotter_qrte.py +++ b/test/python/algorithms/time_evolvers/test_trotter_qrte.py @@ -88,7 +88,10 @@ def test_trotter_qrte_trotter_single_qubit_aux_ops(self): ) aux_ops_result = evolution_result.aux_ops_evaluated - expected_aux_ops_result = [(0.078073, (0.0, 0.0)), (0.268286, (0.0, 0.0))] + expected_aux_ops_result = [ + (0.078073, {"variance": 0, "shots": 0}), + (0.268286, {"variance": 0, "shots": 0}), + ] means = [element[0] for element in aux_ops_result] expected_means = [element[0] for element in expected_aux_ops_result] @@ -96,7 +99,10 @@ def test_trotter_qrte_trotter_single_qubit_aux_ops(self): vars_and_shots = [element[1] for element in aux_ops_result] expected_vars_and_shots = [element[1] for element in expected_aux_ops_result] - np.testing.assert_array_equal(vars_and_shots, expected_vars_and_shots) + + for computed, expected in zip(vars_and_shots, expected_vars_and_shots): + self.assertAlmostEqual(computed.pop("variance", 0), expected["variance"], 2) + self.assertEqual(computed.pop("shots", 0), expected["shots"]) @data( ( From 2976ac94d8cc67c842ddd90278d584085c61dce9 Mon Sep 17 00:00:00 2001 From: Declan Millar Date: Mon, 26 Sep 2022 18:03:22 +0100 Subject: [PATCH 086/100] formatting --- test/python/algorithms/test_observables_evaluator.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/python/algorithms/test_observables_evaluator.py b/test/python/algorithms/test_observables_evaluator.py index 693c1d8b563e..b2b69ed8a288 100644 --- a/test/python/algorithms/test_observables_evaluator.py +++ b/test/python/algorithms/test_observables_evaluator.py @@ -179,8 +179,8 @@ def test_estimate_observables_shots(self): expected_vars_and_shots = [element[1] for element in expected_result] print(expected_vars_and_shots) for computed, expected in zip(vars_and_shots, expected_vars_and_shots): - self.assertAlmostEqual(computed.pop("variance"), expected.pop("variance"), 2) - self.assertEqual(computed.pop("shots"), expected.pop("shots")) + self.assertAlmostEqual(computed.pop("variance"), expected.pop("variance"), 2) + self.assertEqual(computed.pop("shots"), expected.pop("shots")) if __name__ == "__main__": From 998121ff8c8152471bcad0ab44fb378f76d75ffb Mon Sep 17 00:00:00 2001 From: Declan Millar Date: Mon, 26 Sep 2022 18:06:38 +0100 Subject: [PATCH 087/100] remove unused import --- qiskit/algorithms/minimum_eigen_solvers/vqe.py | 1 - 1 file changed, 1 deletion(-) diff --git a/qiskit/algorithms/minimum_eigen_solvers/vqe.py b/qiskit/algorithms/minimum_eigen_solvers/vqe.py index 8c02ebf6ad7c..87ab8707b0f1 100755 --- a/qiskit/algorithms/minimum_eigen_solvers/vqe.py +++ b/qiskit/algorithms/minimum_eigen_solvers/vqe.py @@ -40,7 +40,6 @@ from qiskit.utils import QuantumInstance, algorithm_globals from qiskit.utils.backend_utils import is_aer_provider from qiskit.utils.validation import validate_min -from qiskit.utils.deprecation import deprecate_function from ..aux_ops_evaluator import eval_observables from ..exceptions import AlgorithmError From 8aeed796a8869e947e9fc5ef1289ecc9181077eb Mon Sep 17 00:00:00 2001 From: Declan Millar Date: Mon, 26 Sep 2022 20:10:09 +0100 Subject: [PATCH 088/100] Apply suggestions to docstring from code review Co-authored-by: Steve Wood <40241007+woodsp-ibm@users.noreply.github.com> --- qiskit/algorithms/minimum_eigensolvers/vqe.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/qiskit/algorithms/minimum_eigensolvers/vqe.py b/qiskit/algorithms/minimum_eigensolvers/vqe.py index 72e736c72393..6d42cfc8434f 100644 --- a/qiskit/algorithms/minimum_eigensolvers/vqe.py +++ b/qiskit/algorithms/minimum_eigensolvers/vqe.py @@ -46,10 +46,10 @@ class VQE(VariationalAlgorithm, MinimumEigensolver): The VQE algorithm is executed using an :attr:`estimator` primitive, which must be specified as the first argument on instantiation. - An instance of ``VQE`` also requires an :attr:`ansatz` parameterized :class:`.QuantumCircuit` to - prepare the trial state :math:`|\psi(\vec\theta)\rangle`, as well as a classical - :attr:`optimizer`. The optimizer varies the circuit parameters :math:`\vec\theta` such that the - expectation value of the operator on the corresponding state approaches a minimum, + An instance of ``VQE`` also requires an :attr:`ansatz`, a parameterized :class:`.QuantumCircuit`, to + prepare the trial state :math:`|\psi(\vec\theta)\rangle`. It also needs a classical + :attr:`optimizer` which varies the circuit parameters :math:`\vec\theta` such that the + expectation value of the operator on the corresponding state approaches a minimum. .. math:: @@ -77,7 +77,7 @@ def my_minimizer(fun, x0, jac=None, bounds=None) -> OptimizerResult: result.fun = # optimal function value return result - The above signature also allows one to directly pass any SciPy minimizer, for instance as + The above signature also allows one to use any SciPy minimizer, for instance as .. code-block:: python From 483fbd2705cccadd1a41256d83570d3144dcdb17 Mon Sep 17 00:00:00 2001 From: Declan Millar Date: Mon, 26 Sep 2022 20:13:57 +0100 Subject: [PATCH 089/100] Update VQE docstring --- qiskit/algorithms/minimum_eigensolvers/vqe.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/qiskit/algorithms/minimum_eigensolvers/vqe.py b/qiskit/algorithms/minimum_eigensolvers/vqe.py index 6d42cfc8434f..28822869d900 100644 --- a/qiskit/algorithms/minimum_eigensolvers/vqe.py +++ b/qiskit/algorithms/minimum_eigensolvers/vqe.py @@ -43,19 +43,19 @@ class VQE(VariationalAlgorithm, MinimumEigensolver): VQE is a hybrid quantum-classical algorithm that uses a variational technique to find the minimum eigenvalue of a given Hamiltonian operator :math:`H`. - The VQE algorithm is executed using an :attr:`estimator` primitive, which must be specified as - the first argument on instantiation. + The ``VQE`` algorithm is executed using an :attr:`estimator` primitive, which computes + expectation values of operators (observables). - An instance of ``VQE`` also requires an :attr:`ansatz`, a parameterized :class:`.QuantumCircuit`, to - prepare the trial state :math:`|\psi(\vec\theta)\rangle`. It also needs a classical - :attr:`optimizer` which varies the circuit parameters :math:`\vec\theta` such that the - expectation value of the operator on the corresponding state approaches a minimum. + An instance of ``VQE`` also requires an :attr:`ansatz`, a parameterized + :class:`.QuantumCircuit`, to prepare the trial state :math:`|\psi(\vec\theta)\rangle`. It also + needs a classical :attr:`optimizer` which varies the circuit parameters :math:`\vec\theta` such + that the expectation value of the operator on the corresponding state approaches a minimum. .. math:: \min_{\vec\theta} \langle\psi(\vec\theta)|H|\psi(\vec\theta)\rangle. - The Estimator is used to compute this expectation value for every optimization step. + The :attr:`estimator` is used to compute this expectation value for every optimization step. The optimizer can either be one of Qiskit's optimizers, such as :class:`~qiskit.algorithms.optimizers.SPSA` or a callable with the following signature: From 7e2ee3ddc3d5ab7a2754f663d90e2b09deca920c Mon Sep 17 00:00:00 2001 From: Declan Millar Date: Mon, 26 Sep 2022 20:15:48 +0100 Subject: [PATCH 090/100] Remove printing Co-authored-by: ElePT <57907331+ElePT@users.noreply.github.com> --- test/python/algorithms/test_observables_evaluator.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/test/python/algorithms/test_observables_evaluator.py b/test/python/algorithms/test_observables_evaluator.py index b2b69ed8a288..6a7e61c834b7 100644 --- a/test/python/algorithms/test_observables_evaluator.py +++ b/test/python/algorithms/test_observables_evaluator.py @@ -175,9 +175,7 @@ def test_estimate_observables_shots(self): np.testing.assert_array_almost_equal(means, expected_means, decimal=0.01) vars_and_shots = [element[1] for element in result] - print(vars_and_shots) expected_vars_and_shots = [element[1] for element in expected_result] - print(expected_vars_and_shots) for computed, expected in zip(vars_and_shots, expected_vars_and_shots): self.assertAlmostEqual(computed.pop("variance"), expected.pop("variance"), 2) self.assertEqual(computed.pop("shots"), expected.pop("shots")) From 31219bfe5822172737efb84083ba53dda650c25d Mon Sep 17 00:00:00 2001 From: ElePT Date: Mon, 26 Sep 2022 21:56:06 +0200 Subject: [PATCH 091/100] Add parameters to estimate observables --- qiskit/algorithms/minimum_eigensolvers/vqe.py | 5 +++-- qiskit/algorithms/observables_evaluator.py | 18 +++++++----------- .../trotterization/trotter_qrte.py | 1 + 3 files changed, 11 insertions(+), 13 deletions(-) diff --git a/qiskit/algorithms/minimum_eigensolvers/vqe.py b/qiskit/algorithms/minimum_eigensolvers/vqe.py index 28822869d900..f5ab5aed40df 100644 --- a/qiskit/algorithms/minimum_eigensolvers/vqe.py +++ b/qiskit/algorithms/minimum_eigensolvers/vqe.py @@ -190,8 +190,9 @@ def compute_minimum_eigenvalue( ) if aux_operators is not None: - bound_ansatz = self.ansatz.bind_parameters(optimizer_result.x) - aux_values = estimate_observables(self.estimator, bound_ansatz, aux_operators) + aux_values = estimate_observables( + self.estimator, self.ansatz, aux_operators, optimizer_result.x + ) else: aux_values = None diff --git a/qiskit/algorithms/observables_evaluator.py b/qiskit/algorithms/observables_evaluator.py index 11665c211c2a..738702936ec3 100644 --- a/qiskit/algorithms/observables_evaluator.py +++ b/qiskit/algorithms/observables_evaluator.py @@ -13,6 +13,7 @@ """Evaluator of observables for algorithms.""" from __future__ import annotations +from collections.abc import Sequence import numpy as np @@ -28,6 +29,7 @@ def estimate_observables( estimator: BaseEstimator, quantum_state: QuantumCircuit, observables: ListOrDict[BaseOperator | PauliSumOp], + parameter_values: Sequence[float] | None = None, threshold: float = 1e-12, ) -> ListOrDict[tuple[complex, dict]]: """ @@ -37,10 +39,11 @@ def estimate_observables( Args: estimator: An estimator primitive used for calculations. - quantum_state: An unparametrized quantum circuit representing a quantum state that + quantum_state: An parametrized quantum circuit representing a quantum state that expectation values are computed against. observables: A list or a dictionary of operators whose expectation values are to be calculated. + parameter_values: Optional list of parameters values to evaluate the quantum circuit on. threshold: A threshold value that defines which mean values should be neglected (helpful for ignoring numerical instabilities close to 0). @@ -48,18 +51,9 @@ def estimate_observables( A list or a dictionary of tuples (mean, metadata). Raises: - ValueError: If a ``quantum_state`` with free parameters is provided. AlgorithmError: If a primitive job is not successful. """ - if ( - isinstance(quantum_state, QuantumCircuit) # State cannot be parametrized - and len(quantum_state.parameters) > 0 - ): - raise ValueError( - "A parametrized representation of a quantum_state was provided. It is not " - "allowed - it cannot have free parameters." - ) if isinstance(observables, dict): observables_list = list(observables.values()) else: @@ -67,8 +61,10 @@ def estimate_observables( observables_list = _handle_zero_ops(observables_list) quantum_state = [quantum_state] * len(observables) + if parameter_values is not None: + parameter_values = [parameter_values] * len(observables) try: - estimator_job = estimator.run(quantum_state, observables_list) + estimator_job = estimator.run(quantum_state, observables_list, parameter_values) expectation_values = estimator_job.result().values except Exception as exc: raise AlgorithmError("The primitive job failed!") from exc diff --git a/qiskit/algorithms/time_evolvers/trotterization/trotter_qrte.py b/qiskit/algorithms/time_evolvers/trotterization/trotter_qrte.py index d244f718cef4..8ee855d60c40 100644 --- a/qiskit/algorithms/time_evolvers/trotterization/trotter_qrte.py +++ b/qiskit/algorithms/time_evolvers/trotterization/trotter_qrte.py @@ -168,6 +168,7 @@ def evolve(self, evolution_problem: TimeEvolutionProblem) -> TimeEvolutionResult self.estimator, evolved_state, evolution_problem.aux_operators, + None, evolution_problem.truncation_threshold, ) From 511caca7b34075e6463db8ed8b1a5cfcb945cff9 Mon Sep 17 00:00:00 2001 From: ElePT Date: Mon, 26 Sep 2022 22:58:30 +0200 Subject: [PATCH 092/100] Fix estimate obs. unit test --- test/python/algorithms/test_observables_evaluator.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/python/algorithms/test_observables_evaluator.py b/test/python/algorithms/test_observables_evaluator.py index 6a7e61c834b7..9db5f02b312d 100644 --- a/test/python/algorithms/test_observables_evaluator.py +++ b/test/python/algorithms/test_observables_evaluator.py @@ -72,7 +72,7 @@ def _run_test( observables: ListOrDict[BaseOperator | PauliSumOp], estimator: Estimator, ): - result = estimate_observables(estimator, quantum_state, observables, self.threshold) + result = estimate_observables(estimator, quantum_state, observables, None, self.threshold) if isinstance(observables, dict): np.testing.assert_equal(list(result.keys()), list(expected_result.keys())) @@ -144,7 +144,7 @@ def test_estimate_observables_zero_op(self): state = bound_ansatz estimator = Estimator() observables = [SparsePauliOp(["XX", "YY"]), 0] - result = estimate_observables(estimator, state, observables, self.threshold) + result = estimate_observables(estimator, state, observables, None, self.threshold) expected_result = [(0.015607318055509564, {}), (0.0, {})] means = [element[0] for element in result] expected_means = [element[0] for element in expected_result] @@ -166,7 +166,7 @@ def test_estimate_observables_shots(self): state = bound_ansatz estimator = Estimator(options={"shots": 2048}) observables = [PauliSumOp.from_list([("ZZ", 2.0)])] - result = estimate_observables(estimator, state, observables, self.threshold) + result = estimate_observables(estimator, state, observables, None, self.threshold) exact_result = self.get_exact_expectation(bound_ansatz, observables) expected_result = [(exact_result[0][0], {"variance": 1.0898, "shots": 2048})] From 34246d36d7b0231adb3ef9cbb547e09d7b4e3959 Mon Sep 17 00:00:00 2001 From: Declan Millar Date: Mon, 26 Sep 2022 21:02:46 +0100 Subject: [PATCH 093/100] Update arg description --- qiskit/algorithms/observables_evaluator.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/qiskit/algorithms/observables_evaluator.py b/qiskit/algorithms/observables_evaluator.py index 738702936ec3..25a3f8481cbb 100644 --- a/qiskit/algorithms/observables_evaluator.py +++ b/qiskit/algorithms/observables_evaluator.py @@ -39,8 +39,8 @@ def estimate_observables( Args: estimator: An estimator primitive used for calculations. - quantum_state: An parametrized quantum circuit representing a quantum state that - expectation values are computed against. + quantum_state: A quantum circuit preparing a quantum state that expectation values are + computed against. observables: A list or a dictionary of operators whose expectation values are to be calculated. parameter_values: Optional list of parameters values to evaluate the quantum circuit on. From 12f6426b0875042e1f8c0159aaedd0b3a8d05cd7 Mon Sep 17 00:00:00 2001 From: Declan Millar Date: Mon, 26 Sep 2022 21:06:03 +0100 Subject: [PATCH 094/100] Update arg description --- qiskit/algorithms/observables_evaluator.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/qiskit/algorithms/observables_evaluator.py b/qiskit/algorithms/observables_evaluator.py index 25a3f8481cbb..546e1562ddce 100644 --- a/qiskit/algorithms/observables_evaluator.py +++ b/qiskit/algorithms/observables_evaluator.py @@ -39,8 +39,8 @@ def estimate_observables( Args: estimator: An estimator primitive used for calculations. - quantum_state: A quantum circuit preparing a quantum state that expectation values are - computed against. + quantum_state: A (parameterized) quantum circuit preparing a quantum state that expectation + values are computed against. observables: A list or a dictionary of operators whose expectation values are to be calculated. parameter_values: Optional list of parameters values to evaluate the quantum circuit on. From efc056562fc02f9c53150e983c92b314bd7b8a65 Mon Sep 17 00:00:00 2001 From: Declan Millar Date: Mon, 26 Sep 2022 21:19:10 +0100 Subject: [PATCH 095/100] keep equation part of sentence --- qiskit/algorithms/minimum_eigensolvers/vqe.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/qiskit/algorithms/minimum_eigensolvers/vqe.py b/qiskit/algorithms/minimum_eigensolvers/vqe.py index f5ab5aed40df..1cc7312f3926 100644 --- a/qiskit/algorithms/minimum_eigensolvers/vqe.py +++ b/qiskit/algorithms/minimum_eigensolvers/vqe.py @@ -49,7 +49,7 @@ class VQE(VariationalAlgorithm, MinimumEigensolver): An instance of ``VQE`` also requires an :attr:`ansatz`, a parameterized :class:`.QuantumCircuit`, to prepare the trial state :math:`|\psi(\vec\theta)\rangle`. It also needs a classical :attr:`optimizer` which varies the circuit parameters :math:`\vec\theta` such - that the expectation value of the operator on the corresponding state approaches a minimum. + that the expectation value of the operator on the corresponding state approaches a minimum, .. math:: @@ -209,11 +209,14 @@ def _get_evaluate_energy( ) -> Callable[[np.ndarray], np.ndarray | float]: """Returns a function handle to evaluate the energy at given parameters for the ansatz. This is the objective function to be passed to the optimizer that is used for evaluation. + Args: ansatz: The ansatz preparing the quantum state. operator: The operator whose energy to evaluate. + Returns: A callable that computes and returns the energy of the hamiltonian of each parameter. + Raises: AlgorithmError: If the primitive job to evaluate the energy fails. """ From b1c55ca4165a65e3a1ab5a547282f39040863dee Mon Sep 17 00:00:00 2001 From: Declan Millar Date: Mon, 26 Sep 2022 22:48:04 +0100 Subject: [PATCH 096/100] dict -> dict[str, Any] --- qiskit/algorithms/minimum_eigensolvers/vqe.py | 21 ++++++++++--------- qiskit/algorithms/observables_evaluator.py | 5 +++-- 2 files changed, 14 insertions(+), 12 deletions(-) diff --git a/qiskit/algorithms/minimum_eigensolvers/vqe.py b/qiskit/algorithms/minimum_eigensolvers/vqe.py index 1cc7312f3926..26195b704072 100644 --- a/qiskit/algorithms/minimum_eigensolvers/vqe.py +++ b/qiskit/algorithms/minimum_eigensolvers/vqe.py @@ -17,6 +17,7 @@ import logging from time import time from collections.abc import Callable, Sequence +from typing import Any import numpy as np @@ -95,10 +96,10 @@ def my_minimizer(fun, x0, jac=None, bounds=None) -> OptimizerResult: :class:`.Minimizer` protocol. gradient (BaseEstimatorGradient | None): An optional estimator gradient to be used with the optimizer. - callback (Callable[[int, np.ndarray, float, dict], None] | None): A callback that can access - the intermediate data at each optimization step. These data are: the evaluation count, - the optimizer parameters for the ansatz, the evaluated mean, and the metadata - dictionary. + callback (Callable[[int, np.ndarray, float, dict[str, Any]], None] | None): A callback that + can access the intermediate data at each optimization step. These data are: the + evaluation count, the optimizer parameters for the ansatz, the evaluated mean, and the + metadata dictionary. References: [1] Peruzzo et al, "A variational eigenvalue solver on a quantum processor" @@ -113,7 +114,7 @@ def __init__( *, gradient: BaseEstimatorGradient | None = None, initial_point: Sequence[float] | None = None, - callback: Callable[[int, np.ndarray, float, dict], None] | None = None, + callback: Callable[[int, np.ndarray, float, dict[str, Any]], None] | None = None, ) -> None: r""" Args: @@ -124,11 +125,11 @@ def __init__( can either be a Qiskit :class:`.Optimizer` or a callable implementing the :class:`.Minimizer` protocol. gradient: An optional estimator gradient to be used with the optimizer. - initial_point: An optional initial point (i.e. initial parameter values) for the optimizer. - The length of the initial point must match the number of :attr:`ansatz` parameters. - If ``None``, a random point will be generated within certain parameter bounds. - ``VQE`` will look to the ansatz for these bounds. If the ansatz does not specify - bounds, bounds of :math:`-2\pi`, :math:`2\pi` will be used. + initial_point: An optional initial point (i.e. initial parameter values) for the + optimizer. The length of the initial point must match the number of :attr:`ansatz` + parameters. If ``None``, a random point will be generated within certain parameter + bounds. ``VQE`` will look to the ansatz for these bounds. If the ansatz does not + specify bounds, bounds of :math:`-2\pi`, :math:`2\pi` will be used. callback: A callback that can access the intermediate data at each optimization step. These data are: the evaluation count, the optimizer parameters for the ansatz, the estimated value, and the metadata dictionary. diff --git a/qiskit/algorithms/observables_evaluator.py b/qiskit/algorithms/observables_evaluator.py index 546e1562ddce..f1ca00c4b5d2 100644 --- a/qiskit/algorithms/observables_evaluator.py +++ b/qiskit/algorithms/observables_evaluator.py @@ -14,6 +14,7 @@ from __future__ import annotations from collections.abc import Sequence +from typing import Any import numpy as np @@ -31,7 +32,7 @@ def estimate_observables( observables: ListOrDict[BaseOperator | PauliSumOp], parameter_values: Sequence[float] | None = None, threshold: float = 1e-12, -) -> ListOrDict[tuple[complex, dict]]: +) -> ListOrDict[tuple[complex, dict[str, Any]]]: """ Accepts a sequence of operators and calculates their expectation values - means and metadata. They are calculated with respect to a quantum state provided. A user @@ -94,7 +95,7 @@ def _handle_zero_ops( def _prepare_result( observables_results: list[tuple[complex, dict]], observables: ListOrDict[BaseOperator | PauliSumOp], -) -> ListOrDict[tuple[complex, dict]]: +) -> ListOrDict[tuple[complex, dict[str, Any]]]: """ Prepares a list of tuples of eigenvalues and metadata tuples from ``observables_results`` and ``observables``. From 18b5b63e14a7b560e575ccc3eaebe28d29c91403 Mon Sep 17 00:00:00 2001 From: ElePT <57907331+ElePT@users.noreply.github.com> Date: Tue, 27 Sep 2022 10:11:22 +0200 Subject: [PATCH 097/100] Update qiskit/algorithms/optimizers/spsa.py Apply suggestion Imamichi-san --- qiskit/algorithms/optimizers/spsa.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qiskit/algorithms/optimizers/spsa.py b/qiskit/algorithms/optimizers/spsa.py index b1caf2360816..f12945762cbf 100644 --- a/qiskit/algorithms/optimizers/spsa.py +++ b/qiskit/algorithms/optimizers/spsa.py @@ -742,7 +742,7 @@ def _batch_evaluate(function, points, max_evals_grouped, unpack_points=False): def _as_list(obj): """Convert a list or numpy array into a list.""" - return obj if isinstance(obj, list) else obj.tolist() + return obj.tolist() if isinstance(obj, np.ndarray) else obj def _repack_points(points): From 0e0ebac7762b15c031b9fbc837f6d0439b133de6 Mon Sep 17 00:00:00 2001 From: Declan Millar Date: Tue, 27 Sep 2022 12:40:05 +0100 Subject: [PATCH 098/100] introduce FilterType and aux_operator_eigenvalues -> aux_operators_evaluated --- .../minimum_eigensolver.py | 15 ++- .../numpy_minimum_eigensolver.py | 12 +-- qiskit/algorithms/minimum_eigensolvers/vqe.py | 18 ++-- .../test_numpy_minimum_eigensolver.py | 100 +++++++++--------- .../minimum_eigensolvers/test_vqe.py | 56 +++++----- 5 files changed, 100 insertions(+), 101 deletions(-) diff --git a/qiskit/algorithms/minimum_eigensolvers/minimum_eigensolver.py b/qiskit/algorithms/minimum_eigensolvers/minimum_eigensolver.py index 017639ab121b..30d3b49ce463 100644 --- a/qiskit/algorithms/minimum_eigensolvers/minimum_eigensolver.py +++ b/qiskit/algorithms/minimum_eigensolvers/minimum_eigensolver.py @@ -15,6 +15,7 @@ from __future__ import annotations from abc import ABC, abstractmethod +from typing import Any from qiskit.opflow import PauliSumOp from qiskit.quantum_info.operators.base_operator import BaseOperator @@ -74,7 +75,7 @@ class MinimumEigensolverResult(AlgorithmResult): def __init__(self) -> None: super().__init__() self._eigenvalue = None - self._aux_operator_eigenvalues = None + self._aux_operators_evaluated = None @property def eigenvalue(self) -> complex | None: @@ -86,15 +87,13 @@ def eigenvalue(self, value: complex) -> None: self._eigenvalue = value @property - def aux_operator_eigenvalues(self) -> ListOrDict[tuple[complex, tuple[complex, int]]] | None: + def aux_operators_evaluated(self) -> ListOrDict[tuple[complex, dict[str, Any]]] | None: """The aux operator expectation values. These values are in fact tuples formatted as (mean, (variance, shots)). """ - return self._aux_operator_eigenvalues + return self._aux_operators_evaluated - @aux_operator_eigenvalues.setter - def aux_operator_eigenvalues( - self, value: ListOrDict[tuple[complex, tuple[complex, int]]] - ) -> None: - self._aux_operator_eigenvalues = value + @aux_operators_evaluated.setter + def aux_operators_evaluated(self, value: ListOrDict[tuple[complex, dict[str, Any]]]) -> None: + self._aux_operators_evaluated = value diff --git a/qiskit/algorithms/minimum_eigensolvers/numpy_minimum_eigensolver.py b/qiskit/algorithms/minimum_eigensolvers/numpy_minimum_eigensolver.py index eb021f926109..3a4acb5375db 100644 --- a/qiskit/algorithms/minimum_eigensolvers/numpy_minimum_eigensolver.py +++ b/qiskit/algorithms/minimum_eigensolvers/numpy_minimum_eigensolver.py @@ -28,6 +28,8 @@ logger = logging.getLogger(__name__) +FilterType = Callable[[list | np.ndarray, float, ListOrDict[float] | None], bool] + class NumPyMinimumEigensolver(MinimumEigensolver): """ @@ -36,9 +38,7 @@ class NumPyMinimumEigensolver(MinimumEigensolver): def __init__( self, - filter_criterion: Callable[ - [list | np.ndarray, float, ListOrDict[float] | None], bool - ] = None, + filter_criterion: FilterType | None = None, ) -> None: """ Args: @@ -54,14 +54,14 @@ def __init__( @property def filter_criterion( self, - ) -> Callable[[list | np.ndarray, float, ListOrDict[float] | None], bool] | None: + ) -> FilterType | None: """Returns the criterion for filtering eigenstates/eigenvalues.""" return self._eigensolver.filter_criterion @filter_criterion.setter def filter_criterion( self, - filter_criterion: Callable[[list | np.ndarray, float, ListOrDict[float] | None], bool], + filter_criterion: FilterType, ) -> None: self._eigensolver.filter_criterion = filter_criterion @@ -81,7 +81,7 @@ def compute_minimum_eigenvalue( result.eigenvalue = eigensolver_result.eigenvalues[0] result.eigenstate = eigensolver_result.eigenstates[0] if eigensolver_result.aux_operator_eigenvalues: - result.aux_operator_eigenvalues = eigensolver_result.aux_operator_eigenvalues[0] + result.aux_operators_evaluated = eigensolver_result.aux_operator_eigenvalues[0] logger.debug("NumPy minimum eigensolver result: %s", result) diff --git a/qiskit/algorithms/minimum_eigensolvers/vqe.py b/qiskit/algorithms/minimum_eigensolvers/vqe.py index 26195b704072..87cbe9e5f4d3 100644 --- a/qiskit/algorithms/minimum_eigensolvers/vqe.py +++ b/qiskit/algorithms/minimum_eigensolvers/vqe.py @@ -182,22 +182,22 @@ def compute_minimum_eigenvalue( fun=evaluate_energy, x0=initial_point, jac=evaluate_gradient, bounds=bounds ) - eval_time = time() - start_time + optimizer_time = time() - start_time logger.info( "Optimization complete in %s seconds.\nFound optimal point %s", - eval_time, + optimizer_time, optimizer_result.x, ) if aux_operators is not None: - aux_values = estimate_observables( + aux_operators_evaluated = estimate_observables( self.estimator, self.ansatz, aux_operators, optimizer_result.x ) else: - aux_values = None + aux_operators_evaluated = None - return self._build_vqe_result(optimizer_result, aux_values, eval_time) + return self._build_vqe_result(optimizer_result, aux_operators_evaluated, optimizer_time) @classmethod def supports_aux_operators(cls) -> bool: @@ -306,8 +306,8 @@ def _check_operator_ansatz(self, operator: BaseOperator | PauliSumOp): def _build_vqe_result( self, optimizer_result: OptimizerResult, - aux_values: ListOrDict[tuple[complex, tuple[complex, int]]], - eval_time: float, + aux_operators_evaluated: ListOrDict[tuple[complex, tuple[complex, int]]], + optimizer_time: float, ) -> VQEResult: result = VQEResult() result.eigenvalue = optimizer_result.fun @@ -315,8 +315,8 @@ def _build_vqe_result( result.optimal_point = optimizer_result.x result.optimal_parameters = dict(zip(self.ansatz.parameters, optimizer_result.x)) result.optimal_value = optimizer_result.fun - result.optimizer_time = eval_time - result.aux_operator_eigenvalues = aux_values + result.optimizer_time = optimizer_time + result.aux_operators_evaluated = aux_operators_evaluated result.optimizer_result = optimizer_result return result diff --git a/test/python/algorithms/minimum_eigensolvers/test_numpy_minimum_eigensolver.py b/test/python/algorithms/minimum_eigensolvers/test_numpy_minimum_eigensolver.py index 3201712ff0b6..8d2212e1d7b3 100644 --- a/test/python/algorithms/minimum_eigensolvers/test_numpy_minimum_eigensolver.py +++ b/test/python/algorithms/minimum_eigensolvers/test_numpy_minimum_eigensolver.py @@ -50,9 +50,9 @@ def test_cme(self): operator=self.qubit_op, aux_operators=self.aux_ops_list ) self.assertAlmostEqual(result.eigenvalue, -1.85727503 + 0j) - self.assertEqual(len(result.aux_operator_eigenvalues), 2) - np.testing.assert_array_almost_equal(result.aux_operator_eigenvalues[0], [2, 0]) - np.testing.assert_array_almost_equal(result.aux_operator_eigenvalues[1], [0, 0]) + self.assertEqual(len(result.aux_operators_evaluated), 2) + np.testing.assert_array_almost_equal(result.aux_operators_evaluated[0], [2, 0]) + np.testing.assert_array_almost_equal(result.aux_operators_evaluated[1], [0, 0]) def test_cme_reuse(self): """Test reuse""" @@ -62,38 +62,38 @@ def test_cme_reuse(self): result = algo.compute_minimum_eigenvalue(operator=self.qubit_op) self.assertEqual(result.eigenvalue.dtype, np.float64) self.assertAlmostEqual(result.eigenvalue, -1.85727503) - self.assertIsNone(result.aux_operator_eigenvalues) + self.assertIsNone(result.aux_operators_evaluated) with self.subTest("Test with added aux_operators"): result = algo.compute_minimum_eigenvalue( operator=self.qubit_op, aux_operators=self.aux_ops_list ) self.assertAlmostEqual(result.eigenvalue, -1.85727503 + 0j) - self.assertEqual(len(result.aux_operator_eigenvalues), 2) - np.testing.assert_array_almost_equal(result.aux_operator_eigenvalues[0], [2, 0]) - np.testing.assert_array_almost_equal(result.aux_operator_eigenvalues[1], [0, 0]) + self.assertEqual(len(result.aux_operators_evaluated), 2) + np.testing.assert_array_almost_equal(result.aux_operators_evaluated[0], [2, 0]) + np.testing.assert_array_almost_equal(result.aux_operators_evaluated[1], [0, 0]) with self.subTest("Test with aux_operators removed"): result = algo.compute_minimum_eigenvalue(operator=self.qubit_op, aux_operators=[]) self.assertEqual(result.eigenvalue.dtype, np.float64) self.assertAlmostEqual(result.eigenvalue, -1.85727503) - self.assertIsNone(result.aux_operator_eigenvalues) + self.assertIsNone(result.aux_operators_evaluated) with self.subTest("Test with aux_operators set again"): result = algo.compute_minimum_eigenvalue( operator=self.qubit_op, aux_operators=self.aux_ops_list ) self.assertAlmostEqual(result.eigenvalue, -1.85727503 + 0j) - self.assertEqual(len(result.aux_operator_eigenvalues), 2) - np.testing.assert_array_almost_equal(result.aux_operator_eigenvalues[0], [2, 0]) - np.testing.assert_array_almost_equal(result.aux_operator_eigenvalues[1], [0, 0]) + self.assertEqual(len(result.aux_operators_evaluated), 2) + np.testing.assert_array_almost_equal(result.aux_operators_evaluated[0], [2, 0]) + np.testing.assert_array_almost_equal(result.aux_operators_evaluated[1], [0, 0]) with self.subTest("Test after setting first aux_operators as main operator"): result = algo.compute_minimum_eigenvalue( operator=self.aux_ops_list[0], aux_operators=[] ) self.assertAlmostEqual(result.eigenvalue, 2 + 0j) - self.assertIsNone(result.aux_operator_eigenvalues) + self.assertIsNone(result.aux_operators_evaluated) def test_cme_filter(self): """Basic test""" @@ -108,9 +108,9 @@ def criterion(x, v, a_v): operator=self.qubit_op, aux_operators=self.aux_ops_list ) self.assertAlmostEqual(result.eigenvalue, -0.22491125 + 0j) - self.assertEqual(len(result.aux_operator_eigenvalues), 2) - np.testing.assert_array_almost_equal(result.aux_operator_eigenvalues[0], [2, 0]) - np.testing.assert_array_almost_equal(result.aux_operator_eigenvalues[1], [0, 0]) + self.assertEqual(len(result.aux_operators_evaluated), 2) + np.testing.assert_array_almost_equal(result.aux_operators_evaluated[0], [2, 0]) + np.testing.assert_array_almost_equal(result.aux_operators_evaluated[1], [0, 0]) def test_cme_filter_empty(self): """Test with filter always returning False""" @@ -126,7 +126,7 @@ def criterion(x, v, a_v): ) self.assertEqual(result.eigenvalue, None) self.assertEqual(result.eigenstate, None) - self.assertEqual(result.aux_operator_eigenvalues, None) + self.assertEqual(result.aux_operators_evaluated, None) @data("X", "Y", "Z") def test_cme_1q(self, op): @@ -144,16 +144,16 @@ def test_cme_aux_ops_dict(self): with self.subTest("Test with an empty dictionary."): result = algo.compute_minimum_eigenvalue(operator=self.qubit_op, aux_operators={}) self.assertAlmostEqual(result.eigenvalue, -1.85727503 + 0j) - self.assertIsNone(result.aux_operator_eigenvalues) + self.assertIsNone(result.aux_operators_evaluated) with self.subTest("Test with two auxiliary operators."): result = algo.compute_minimum_eigenvalue( operator=self.qubit_op, aux_operators=self.aux_ops_dict ) self.assertAlmostEqual(result.eigenvalue, -1.85727503 + 0j) - self.assertEqual(len(result.aux_operator_eigenvalues), 2) - np.testing.assert_array_almost_equal(result.aux_operator_eigenvalues["aux_op1"], [2, 0]) - np.testing.assert_array_almost_equal(result.aux_operator_eigenvalues["aux_op2"], [0, 0]) + self.assertEqual(len(result.aux_operators_evaluated), 2) + np.testing.assert_array_almost_equal(result.aux_operators_evaluated["aux_op1"], [2, 0]) + np.testing.assert_array_almost_equal(result.aux_operators_evaluated["aux_op2"], [0, 0]) with self.subTest("Test with additional zero and None operators."): extra_ops = {"None_op": None, "zero_op": 0, **self.aux_ops_dict} @@ -161,10 +161,10 @@ def test_cme_aux_ops_dict(self): operator=self.qubit_op, aux_operators=extra_ops ) self.assertAlmostEqual(result.eigenvalue, -1.85727503 + 0j) - self.assertEqual(len(result.aux_operator_eigenvalues), 3) - np.testing.assert_array_almost_equal(result.aux_operator_eigenvalues["aux_op1"], [2, 0]) - np.testing.assert_array_almost_equal(result.aux_operator_eigenvalues["aux_op2"], [0, 0]) - self.assertEqual(result.aux_operator_eigenvalues["zero_op"], (0.0, 0)) + self.assertEqual(len(result.aux_operators_evaluated), 3) + np.testing.assert_array_almost_equal(result.aux_operators_evaluated["aux_op1"], [2, 0]) + np.testing.assert_array_almost_equal(result.aux_operators_evaluated["aux_op2"], [0, 0]) + self.assertEqual(result.aux_operators_evaluated["zero_op"], (0.0, 0)) def test_aux_operators_list(self): """Test list-based aux_operators.""" @@ -176,13 +176,13 @@ def test_aux_operators_list(self): with self.subTest("Test with two auxiliary operators."): result = algo.compute_minimum_eigenvalue(operator=self.qubit_op, aux_operators=aux_ops) self.assertAlmostEqual(result.eigenvalue, -1.85727503 + 0j) - self.assertEqual(len(result.aux_operator_eigenvalues), 2) + self.assertEqual(len(result.aux_operators_evaluated), 2) # expectation values - self.assertAlmostEqual(result.aux_operator_eigenvalues[0][0], 2, places=6) - self.assertAlmostEqual(result.aux_operator_eigenvalues[1][0], 0, places=6) + self.assertAlmostEqual(result.aux_operators_evaluated[0][0], 2, places=6) + self.assertAlmostEqual(result.aux_operators_evaluated[1][0], 0, places=6) # standard deviations - self.assertAlmostEqual(result.aux_operator_eigenvalues[0][1], 0.0) - self.assertAlmostEqual(result.aux_operator_eigenvalues[1][1], 0.0) + self.assertAlmostEqual(result.aux_operators_evaluated[0][1], 0.0) + self.assertAlmostEqual(result.aux_operators_evaluated[1][1], 0.0) with self.subTest("Test with additional zero and None operators."): extra_ops = [*aux_ops, None, 0] @@ -190,16 +190,16 @@ def test_aux_operators_list(self): operator=self.qubit_op, aux_operators=extra_ops ) self.assertAlmostEqual(result.eigenvalue, -1.85727503 + 0j) - self.assertEqual(len(result.aux_operator_eigenvalues), 4) + self.assertEqual(len(result.aux_operators_evaluated), 4) # expectation values - self.assertAlmostEqual(result.aux_operator_eigenvalues[0][0], 2, places=6) - self.assertAlmostEqual(result.aux_operator_eigenvalues[1][0], 0, places=6) - self.assertIsNone(result.aux_operator_eigenvalues[2], None) - self.assertEqual(result.aux_operator_eigenvalues[3][0], 0.0) + self.assertAlmostEqual(result.aux_operators_evaluated[0][0], 2, places=6) + self.assertAlmostEqual(result.aux_operators_evaluated[1][0], 0, places=6) + self.assertIsNone(result.aux_operators_evaluated[2], None) + self.assertEqual(result.aux_operators_evaluated[3][0], 0.0) # standard deviations - self.assertAlmostEqual(result.aux_operator_eigenvalues[0][1], 0.0) - self.assertAlmostEqual(result.aux_operator_eigenvalues[1][1], 0.0) - self.assertEqual(result.aux_operator_eigenvalues[3][1], 0.0) + self.assertAlmostEqual(result.aux_operators_evaluated[0][1], 0.0) + self.assertAlmostEqual(result.aux_operators_evaluated[1][1], 0.0) + self.assertEqual(result.aux_operators_evaluated[3][1], 0.0) def test_aux_operators_dict(self): """Test dict-based aux_operators.""" @@ -211,13 +211,13 @@ def test_aux_operators_dict(self): with self.subTest("Test with two auxiliary operators."): result = algo.compute_minimum_eigenvalue(operator=self.qubit_op, aux_operators=aux_ops) self.assertAlmostEqual(result.eigenvalue, -1.85727503 + 0j) - self.assertEqual(len(result.aux_operator_eigenvalues), 2) + self.assertEqual(len(result.aux_operators_evaluated), 2) # expectation values - self.assertAlmostEqual(result.aux_operator_eigenvalues["aux_op1"][0], 2, places=6) - self.assertAlmostEqual(result.aux_operator_eigenvalues["aux_op2"][0], 0, places=6) + self.assertAlmostEqual(result.aux_operators_evaluated["aux_op1"][0], 2, places=6) + self.assertAlmostEqual(result.aux_operators_evaluated["aux_op2"][0], 0, places=6) # standard deviations - self.assertAlmostEqual(result.aux_operator_eigenvalues["aux_op1"][1], 0.0) - self.assertAlmostEqual(result.aux_operator_eigenvalues["aux_op2"][1], 0.0) + self.assertAlmostEqual(result.aux_operators_evaluated["aux_op1"][1], 0.0) + self.assertAlmostEqual(result.aux_operators_evaluated["aux_op2"][1], 0.0) with self.subTest("Test with additional zero and None operators."): extra_ops = {**aux_ops, "None_operator": None, "zero_operator": 0} @@ -225,16 +225,16 @@ def test_aux_operators_dict(self): operator=self.qubit_op, aux_operators=extra_ops ) self.assertAlmostEqual(result.eigenvalue, -1.85727503 + 0j) - self.assertEqual(len(result.aux_operator_eigenvalues), 3) + self.assertEqual(len(result.aux_operators_evaluated), 3) # expectation values - self.assertAlmostEqual(result.aux_operator_eigenvalues["aux_op1"][0], 2, places=6) - self.assertAlmostEqual(result.aux_operator_eigenvalues["aux_op2"][0], 0, places=6) - self.assertEqual(result.aux_operator_eigenvalues["zero_operator"][0], 0.0) - self.assertTrue("None_operator" not in result.aux_operator_eigenvalues.keys()) + self.assertAlmostEqual(result.aux_operators_evaluated["aux_op1"][0], 2, places=6) + self.assertAlmostEqual(result.aux_operators_evaluated["aux_op2"][0], 0, places=6) + self.assertEqual(result.aux_operators_evaluated["zero_operator"][0], 0.0) + self.assertTrue("None_operator" not in result.aux_operators_evaluated.keys()) # standard deviations - self.assertAlmostEqual(result.aux_operator_eigenvalues["aux_op1"][1], 0.0) - self.assertAlmostEqual(result.aux_operator_eigenvalues["aux_op2"][1], 0.0) - self.assertAlmostEqual(result.aux_operator_eigenvalues["zero_operator"][1], 0.0) + self.assertAlmostEqual(result.aux_operators_evaluated["aux_op1"][1], 0.0) + self.assertAlmostEqual(result.aux_operators_evaluated["aux_op2"][1], 0.0) + self.assertAlmostEqual(result.aux_operators_evaluated["zero_operator"][1], 0.0) if __name__ == "__main__": diff --git a/test/python/algorithms/minimum_eigensolvers/test_vqe.py b/test/python/algorithms/minimum_eigensolvers/test_vqe.py index 171b52b9bcca..1ecc870237ec 100644 --- a/test/python/algorithms/minimum_eigensolvers/test_vqe.py +++ b/test/python/algorithms/minimum_eigensolvers/test_vqe.py @@ -369,8 +369,8 @@ def test_aux_operators_list(self): with self.subTest("Test with an empty list."): result = vqe.compute_minimum_eigenvalue(self.h2_op, aux_operators=[]) self.assertAlmostEqual(result.eigenvalue.real, self.h2_energy, places=6) - self.assertIsInstance(result.aux_operator_eigenvalues, list) - self.assertEqual(len(result.aux_operator_eigenvalues), 0) + self.assertIsInstance(result.aux_operators_evaluated, list) + self.assertEqual(len(result.aux_operators_evaluated), 0) with self.subTest("Test with two auxiliary operators."): aux_op1 = PauliSumOp.from_list([("II", 2.0)]) @@ -378,27 +378,27 @@ def test_aux_operators_list(self): aux_ops = [aux_op1, aux_op2] result = vqe.compute_minimum_eigenvalue(self.h2_op, aux_operators=aux_ops) self.assertAlmostEqual(result.eigenvalue.real, self.h2_energy, places=5) - self.assertEqual(len(result.aux_operator_eigenvalues), 2) + self.assertEqual(len(result.aux_operators_evaluated), 2) # expectation values - self.assertAlmostEqual(result.aux_operator_eigenvalues[0][0], 2.0, places=6) - self.assertAlmostEqual(result.aux_operator_eigenvalues[1][0], 0.0, places=6) + self.assertAlmostEqual(result.aux_operators_evaluated[0][0], 2.0, places=6) + self.assertAlmostEqual(result.aux_operators_evaluated[1][0], 0.0, places=6) # metadata - self.assertIsInstance(result.aux_operator_eigenvalues[0][1], dict) - self.assertIsInstance(result.aux_operator_eigenvalues[1][1], dict) + self.assertIsInstance(result.aux_operators_evaluated[0][1], dict) + self.assertIsInstance(result.aux_operators_evaluated[1][1], dict) with self.subTest("Test with additional zero operator."): extra_ops = [*aux_ops, 0] result = vqe.compute_minimum_eigenvalue(self.h2_op, aux_operators=extra_ops) self.assertAlmostEqual(result.eigenvalue.real, self.h2_energy, places=5) - self.assertEqual(len(result.aux_operator_eigenvalues), 3) + self.assertEqual(len(result.aux_operators_evaluated), 3) # expectation values - self.assertAlmostEqual(result.aux_operator_eigenvalues[0][0], 2.0, places=6) - self.assertAlmostEqual(result.aux_operator_eigenvalues[1][0], 0.0, places=6) - self.assertAlmostEqual(result.aux_operator_eigenvalues[2][0], 0.0) + self.assertAlmostEqual(result.aux_operators_evaluated[0][0], 2.0, places=6) + self.assertAlmostEqual(result.aux_operators_evaluated[1][0], 0.0, places=6) + self.assertAlmostEqual(result.aux_operators_evaluated[2][0], 0.0) # metadata - self.assertIsInstance(result.aux_operator_eigenvalues[0][1], dict) - self.assertIsInstance(result.aux_operator_eigenvalues[1][1], dict) - self.assertIsInstance(result.aux_operator_eigenvalues[2][1], dict) + self.assertIsInstance(result.aux_operators_evaluated[0][1], dict) + self.assertIsInstance(result.aux_operators_evaluated[1][1], dict) + self.assertIsInstance(result.aux_operators_evaluated[2][1], dict) def test_aux_operators_dict(self): """Test dictionary compatibility of aux_operators""" @@ -407,8 +407,8 @@ def test_aux_operators_dict(self): with self.subTest("Test with an empty dictionary."): result = vqe.compute_minimum_eigenvalue(self.h2_op, aux_operators={}) self.assertAlmostEqual(result.eigenvalue.real, self.h2_energy, places=6) - self.assertIsInstance(result.aux_operator_eigenvalues, dict) - self.assertEqual(len(result.aux_operator_eigenvalues), 0) + self.assertIsInstance(result.aux_operators_evaluated, dict) + self.assertEqual(len(result.aux_operators_evaluated), 0) with self.subTest("Test with two auxiliary operators."): aux_op1 = PauliSumOp.from_list([("II", 2.0)]) @@ -416,28 +416,28 @@ def test_aux_operators_dict(self): aux_ops = {"aux_op1": aux_op1, "aux_op2": aux_op2} result = vqe.compute_minimum_eigenvalue(self.h2_op, aux_operators=aux_ops) self.assertAlmostEqual(result.eigenvalue.real, self.h2_energy, places=6) - self.assertEqual(len(result.aux_operator_eigenvalues), 2) + self.assertEqual(len(result.aux_operators_evaluated), 2) # expectation values - self.assertAlmostEqual(result.aux_operator_eigenvalues["aux_op1"][0], 2.0, places=5) - self.assertAlmostEqual(result.aux_operator_eigenvalues["aux_op2"][0], 0.0, places=5) + self.assertAlmostEqual(result.aux_operators_evaluated["aux_op1"][0], 2.0, places=5) + self.assertAlmostEqual(result.aux_operators_evaluated["aux_op2"][0], 0.0, places=5) # metadata - self.assertIsInstance(result.aux_operator_eigenvalues["aux_op1"][1], dict) - self.assertIsInstance(result.aux_operator_eigenvalues["aux_op2"][1], dict) + self.assertIsInstance(result.aux_operators_evaluated["aux_op1"][1], dict) + self.assertIsInstance(result.aux_operators_evaluated["aux_op2"][1], dict) with self.subTest("Test with additional zero operator."): extra_ops = {**aux_ops, "zero_operator": 0} result = vqe.compute_minimum_eigenvalue(self.h2_op, aux_operators=extra_ops) self.assertAlmostEqual(result.eigenvalue.real, self.h2_energy, places=6) - self.assertEqual(len(result.aux_operator_eigenvalues), 3) + self.assertEqual(len(result.aux_operators_evaluated), 3) # expectation values - self.assertAlmostEqual(result.aux_operator_eigenvalues["aux_op1"][0], 2.0, places=5) - self.assertAlmostEqual(result.aux_operator_eigenvalues["aux_op2"][0], 0.0, places=5) - self.assertAlmostEqual(result.aux_operator_eigenvalues["zero_operator"][0], 0.0) + self.assertAlmostEqual(result.aux_operators_evaluated["aux_op1"][0], 2.0, places=5) + self.assertAlmostEqual(result.aux_operators_evaluated["aux_op2"][0], 0.0, places=5) + self.assertAlmostEqual(result.aux_operators_evaluated["zero_operator"][0], 0.0) # metadata - self.assertIsInstance(result.aux_operator_eigenvalues["aux_op1"][1], dict) - self.assertIsInstance(result.aux_operator_eigenvalues["aux_op2"][1], dict) - self.assertIsInstance(result.aux_operator_eigenvalues["zero_operator"][1], dict) + self.assertIsInstance(result.aux_operators_evaluated["aux_op1"][1], dict) + self.assertIsInstance(result.aux_operators_evaluated["aux_op2"][1], dict) + self.assertIsInstance(result.aux_operators_evaluated["zero_operator"][1], dict) if __name__ == "__main__": From 3357ebefa3417af3a0da3e9257acd953fb1173d8 Mon Sep 17 00:00:00 2001 From: Declan Millar Date: Tue, 27 Sep 2022 12:43:36 +0100 Subject: [PATCH 099/100] Correct typehint Co-authored-by: Ikko Hamamura --- qiskit/algorithms/minimum_eigensolvers/vqe.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qiskit/algorithms/minimum_eigensolvers/vqe.py b/qiskit/algorithms/minimum_eigensolvers/vqe.py index 87cbe9e5f4d3..5f60b070fdb2 100644 --- a/qiskit/algorithms/minimum_eigensolvers/vqe.py +++ b/qiskit/algorithms/minimum_eigensolvers/vqe.py @@ -257,7 +257,7 @@ def _get_evaluate_gradient( self, ansatz: QuantumCircuit, operator: BaseOperator | PauliSumOp, - ) -> tuple[Callable[[np.ndarray], np.ndarray]]: + ) -> Callable[[np.ndarray], np.ndarray]: """Get a function handle to evaluate the gradient at given parameters for the ansatz. Args: From 66ba3463ab286b6eba06e37718777224be79f031 Mon Sep 17 00:00:00 2001 From: Declan Millar Date: Tue, 27 Sep 2022 13:12:11 +0100 Subject: [PATCH 100/100] update vqe docstring and use old typing for type alias --- .../minimum_eigensolvers/numpy_minimum_eigensolver.py | 7 ++++--- qiskit/algorithms/minimum_eigensolvers/vqe.py | 3 +++ 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/qiskit/algorithms/minimum_eigensolvers/numpy_minimum_eigensolver.py b/qiskit/algorithms/minimum_eigensolvers/numpy_minimum_eigensolver.py index 3a4acb5375db..ec11f27eb2b0 100644 --- a/qiskit/algorithms/minimum_eigensolvers/numpy_minimum_eigensolver.py +++ b/qiskit/algorithms/minimum_eigensolvers/numpy_minimum_eigensolver.py @@ -14,21 +14,22 @@ from __future__ import annotations -from typing import Callable +from typing import Callable, List, Union, Optional import logging import numpy as np from qiskit.opflow import PauliSumOp from qiskit.quantum_info.operators.base_operator import BaseOperator -# TODO this path will need updating +# TODO this path will need updating when VQD is merged from ..eigen_solvers.numpy_eigen_solver import NumPyEigensolver from .minimum_eigensolver import MinimumEigensolver, MinimumEigensolverResult from ..list_or_dict import ListOrDict logger = logging.getLogger(__name__) -FilterType = Callable[[list | np.ndarray, float, ListOrDict[float] | None], bool] +# future type annotations not supported in type aliases in 3.8 +FilterType = Callable[[Union[List, np.ndarray], float, Optional[ListOrDict[float]]], bool] class NumPyMinimumEigensolver(MinimumEigensolver): diff --git a/qiskit/algorithms/minimum_eigensolvers/vqe.py b/qiskit/algorithms/minimum_eigensolvers/vqe.py index 5f60b070fdb2..7d4c76d98160 100644 --- a/qiskit/algorithms/minimum_eigensolvers/vqe.py +++ b/qiskit/algorithms/minimum_eigensolvers/vqe.py @@ -87,6 +87,9 @@ def my_minimizer(fun, x0, jac=None, bounds=None) -> OptimizerResult: optimizer = partial(minimize, method="L-BFGS-B") + The following attributes can be set via the initializer but can also be read and updated once + the VQE object has been constructed. + Attributes: estimator (BaseEstimator): The estimator primitive to compute the expectation value of the Hamiltonian operator.