diff --git a/constraints.txt b/constraints.txt index bba3992ca..d72e7eedf 100644 --- a/constraints.txt +++ b/constraints.txt @@ -1 +1,2 @@ numpy>=1.20.0 +jedi<0.19 diff --git a/qiskit_optimization/__init__.py b/qiskit_optimization/__init__.py index 18c947967..548010ebc 100644 --- a/qiskit_optimization/__init__.py +++ b/qiskit_optimization/__init__.py @@ -26,7 +26,7 @@ A uniform interface as well as automatic conversion between different problem representations allows users to solve problems using a large set of algorithms, from variational quantum algorithms, such as the Quantum Approximate Optimization Algorithm -(:class:`~qiskit_algorithms.QAOA`), to +(:class:`~qiskit_optimization.minimum_eigensolvers.QAOA`), to `Grover Adaptive Search `_ (:class:`~algorithms.GroverOptimizer`), leveraging fundamental `minimum eigensolvers @@ -85,7 +85,7 @@ """ -from .exceptions import QiskitOptimizationError, AlgorithmError +from .exceptions import AlgorithmError, QiskitOptimizationError from .infinity import INFINITY # must be at the top of the file from .problems.quadratic_program import QuadraticProgram from .version import __version__ diff --git a/qiskit_optimization/algorithms/admm_optimizer.py b/qiskit_optimization/algorithms/admm_optimizer.py index 0def1c9bf..7c6979fa6 100644 --- a/qiskit_optimization/algorithms/admm_optimizer.py +++ b/qiskit_optimization/algorithms/admm_optimizer.py @@ -1,6 +1,6 @@ # This code is part of a Qiskit project. # -# (C) Copyright IBM 2020, 2024. +# (C) Copyright IBM 2020, 2025. # # 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 @@ -17,9 +17,9 @@ from typing import List, Optional, Tuple, cast import numpy as np -from qiskit_algorithms import NumPyMinimumEigensolver from ..converters import MaximizeToMinimize +from ..minimum_eigensolvers import NumPyMinimumEigensolver from ..problems.constraint import Constraint from ..problems.linear_constraint import LinearConstraint from ..problems.linear_expression import LinearExpression diff --git a/qiskit_optimization/algorithms/grover_optimizer.py b/qiskit_optimization/algorithms/grover_optimizer.py index 548ef5da9..d40598761 100644 --- a/qiskit_optimization/algorithms/grover_optimizer.py +++ b/qiskit_optimization/algorithms/grover_optimizer.py @@ -1,6 +1,6 @@ # This code is part of a Qiskit project. # -# (C) Copyright IBM 2020, 2024. +# (C) Copyright IBM 2020, 2025. # # 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 @@ -21,9 +21,6 @@ from qiskit import QuantumCircuit, QuantumRegister from qiskit.circuit.library import QuadraticForm from qiskit.primitives import BaseSampler -from qiskit_algorithms import AmplificationProblem -from qiskit_algorithms.amplitude_amplifiers.grover import Grover -from qiskit_algorithms.utils import algorithm_globals from qiskit_optimization.algorithms.optimization_algorithm import ( OptimizationAlgorithm, @@ -31,9 +28,11 @@ OptimizationResultStatus, SolutionSample, ) +from qiskit_optimization.amplitude_amplifiers.grover import AmplificationProblem, Grover from qiskit_optimization.converters import QuadraticProgramConverter, QuadraticProgramToQubo from qiskit_optimization.exceptions import QiskitOptimizationError from qiskit_optimization.problems import QuadraticProgram, Variable +from qiskit_optimization.utils import algorithm_globals logger = logging.getLogger(__name__) diff --git a/qiskit_optimization/algorithms/minimum_eigen_optimizer.py b/qiskit_optimization/algorithms/minimum_eigen_optimizer.py index 9ca8512ed..8ed8cb14c 100644 --- a/qiskit_optimization/algorithms/minimum_eigen_optimizer.py +++ b/qiskit_optimization/algorithms/minimum_eigen_optimizer.py @@ -15,15 +15,15 @@ import numpy as np from qiskit.quantum_info import SparsePauliOp + +from ..converters.quadratic_program_to_qubo import QuadraticProgramConverter, QuadraticProgramToQubo +from ..exceptions import QiskitOptimizationError from ..minimum_eigensolvers import ( NumPyMinimumEigensolver, NumPyMinimumEigensolverResult, SamplingMinimumEigensolver, SamplingMinimumEigensolverResult, ) - -from ..converters.quadratic_program_to_qubo import QuadraticProgramConverter, QuadraticProgramToQubo -from ..exceptions import QiskitOptimizationError from ..problems.quadratic_program import QuadraticProgram, Variable from .optimization_algorithm import ( OptimizationAlgorithm, @@ -109,7 +109,7 @@ class MinimumEigenOptimizer(OptimizationAlgorithm): .. code-block:: - from qiskit_algorithms import QAOA + from qiskit_optimization.minimum_eigensolvers import QAOA from qiskit_optimization.problems import QuadraticProgram from qiskit_optimization.algorithms import MinimumEigenOptimizer problem = QuadraticProgram() @@ -222,7 +222,8 @@ def _solve_internal( raise QiskitOptimizationError( "MinimumEigenOptimizer does not support this minimum eigensolver " f"{type(self._min_eigen_solver)}. " - "You can use qiskit_algorithms.SamplingMinimumEigensolver instead." + "You can use qiskit_optimization.minimum_eigensolvers." + "SamplingMinimumEigensolver instead." ) if eigen_result.eigenstate is not None: raw_samples = self._eigenvector_to_solutions( diff --git a/qiskit_optimization/algorithms/qrao/__init__.py b/qiskit_optimization/algorithms/qrao/__init__.py index ad7a6ce8e..82b57aff2 100644 --- a/qiskit_optimization/algorithms/qrao/__init__.py +++ b/qiskit_optimization/algorithms/qrao/__init__.py @@ -1,6 +1,6 @@ # This code is part of a Qiskit project. # -# (C) Copyright IBM 2023. +# (C) Copyright IBM 2023, 2025. # # 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 @@ -48,8 +48,8 @@ .. code-block:: python - from qiskit_algorithms.optimizers import COBYLA - from qiskit_algorithms import VQE + from qiskit_optimization.optimizers import COBYLA + from qiskit_optimization.minimum_eigensolvers import VQE from qiskit.circuit.library import RealAmplitudes from qiskit.primitives import Estimator diff --git a/qiskit_optimization/algorithms/qrao/magic_rounding.py b/qiskit_optimization/algorithms/qrao/magic_rounding.py index 4fc67ab28..28f86c1c3 100644 --- a/qiskit_optimization/algorithms/qrao/magic_rounding.py +++ b/qiskit_optimization/algorithms/qrao/magic_rounding.py @@ -1,6 +1,6 @@ # This code is part of a Qiskit project. # -# (C) Copyright IBM 2023, 2024. +# (C) Copyright IBM 2023, 2025. # # 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 @@ -19,10 +19,9 @@ from qiskit import QuantumCircuit from qiskit.primitives import BaseSampler from qiskit.quantum_info import SparsePauliOp -from qiskit_algorithms.exceptions import AlgorithmError from qiskit_optimization.algorithms import OptimizationResultStatus, SolutionSample -from qiskit_optimization.exceptions import QiskitOptimizationError +from qiskit_optimization.exceptions import AlgorithmError, QiskitOptimizationError from .quantum_random_access_encoding import ( _z_to_21p_qrac_basis_circuit, diff --git a/qiskit_optimization/algorithms/qrao/quantum_random_access_optimizer.py b/qiskit_optimization/algorithms/qrao/quantum_random_access_optimizer.py index 238434bb5..34934a47b 100644 --- a/qiskit_optimization/algorithms/qrao/quantum_random_access_optimizer.py +++ b/qiskit_optimization/algorithms/qrao/quantum_random_access_optimizer.py @@ -1,6 +1,6 @@ # This code is part of a Qiskit project. # -# (C) Copyright IBM 2023. +# (C) Copyright IBM 2023, 2025. # # 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 @@ -13,16 +13,10 @@ """Quantum Random Access Optimizer class.""" from __future__ import annotations -from typing import cast, List +from typing import List, cast import numpy as np from qiskit import QuantumCircuit -from qiskit_algorithms import ( - MinimumEigensolver, - MinimumEigensolverResult, - NumPyMinimumEigensolverResult, - VariationalResult, -) from qiskit_optimization.algorithms import ( OptimizationAlgorithm, @@ -31,7 +25,13 @@ SolutionSample, ) from qiskit_optimization.converters import QuadraticProgramToQubo +from qiskit_optimization.minimum_eigensolvers import ( + MinimumEigensolver, + MinimumEigensolverResult, + NumPyMinimumEigensolverResult, +) from qiskit_optimization.problems import QuadraticProgram, Variable +from qiskit_optimization.variational_algorithm import VariationalResult from .quantum_random_access_encoding import QuantumRandomAccessEncoding from .rounding_common import RoundingContext, RoundingResult, RoundingScheme diff --git a/qiskit_optimization/algorithms/recursive_minimum_eigen_optimizer.py b/qiskit_optimization/algorithms/recursive_minimum_eigen_optimizer.py index add88c0a3..8e392e1c7 100644 --- a/qiskit_optimization/algorithms/recursive_minimum_eigen_optimizer.py +++ b/qiskit_optimization/algorithms/recursive_minimum_eigen_optimizer.py @@ -1,6 +1,6 @@ # This code is part of a Qiskit project. # -# (C) Copyright IBM 2020, 2024. +# (C) Copyright IBM 2020, 2025. # # 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 @@ -17,13 +17,13 @@ from typing import Dict, List, Optional, Tuple, Union, cast import numpy as np -from qiskit_algorithms import NumPyMinimumEigensolver -from qiskit_algorithms.utils.validation import validate_min from ..converters.quadratic_program_to_qubo import QuadraticProgramConverter, QuadraticProgramToQubo from ..exceptions import QiskitOptimizationError +from ..minimum_eigensolvers import NumPyMinimumEigensolver from ..problems import Variable from ..problems.quadratic_program import QuadraticProgram +from ..utils.validation import validate_min from .minimum_eigen_optimizer import MinimumEigenOptimizationResult, MinimumEigenOptimizer from .optimization_algorithm import ( OptimizationAlgorithm, @@ -117,7 +117,7 @@ class RecursiveMinimumEigenOptimizer(OptimizationAlgorithm): .. code-block:: python - from qiskit_algorithms import QAOA + from qiskit_optimization.minimum_eigensolvers import QAOA from qiskit_optimization.problems import QuadraticProgram from qiskit_optimization.algorithms import ( MinimumEigenOptimizer, RecursiveMinimumEigenOptimizer @@ -161,7 +161,7 @@ def __init__( # pylint: disable=too-many-positional-arguments min_num_vars_optimizer: This optimizer is used after the recursive scheme for the problem with the remaining variables. Default value is :class:`~qiskit_optimization.algorithms.MinimumEigenOptimizer` created on top of - :class:`~qiskit_algorithms.NumPyMinimumEigensolver`. + :class:`~qiskit_optimization.minimum_eigensolvers.NumPyMinimumEigensolver`. penalty: The factor that is used to scale the penalty terms corresponding to linear equality constraints. history: Whether the intermediate results are stored. diff --git a/qiskit_optimization/algorithms/warm_start_qaoa_optimizer.py b/qiskit_optimization/algorithms/warm_start_qaoa_optimizer.py index b57cc7101..acaf4c94d 100644 --- a/qiskit_optimization/algorithms/warm_start_qaoa_optimizer.py +++ b/qiskit_optimization/algorithms/warm_start_qaoa_optimizer.py @@ -1,6 +1,6 @@ # This code is part of a Qiskit project. # -# (C) Copyright IBM 2021, 2024. +# (C) Copyright IBM 2021, 2025. # # 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 @@ -19,10 +19,10 @@ import numpy as np from qiskit import QuantumCircuit from qiskit.circuit import Parameter -from qiskit_algorithms import QAOA from ..converters.quadratic_program_converter import QuadraticProgramConverter from ..exceptions import QiskitOptimizationError +from ..minimum_eigensolvers import QAOA from ..problems.quadratic_program import QuadraticProgram from ..problems.variable import VarType from .minimum_eigen_optimizer import MinimumEigenOptimizationResult, MinimumEigenOptimizer diff --git a/qiskit_optimization/amplitude_amplifiers/__init__.py b/qiskit_optimization/amplitude_amplifiers/__init__.py new file mode 100644 index 000000000..c23f498eb --- /dev/null +++ b/qiskit_optimization/amplitude_amplifiers/__init__.py @@ -0,0 +1,25 @@ +# This code is part of a Qiskit project. +# +# (C) Copyright IBM 2020, 2025. +# +# 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. + +"""Amplitude Amplifiers Package""" + +from .amplification_problem import AmplificationProblem +from .amplitude_amplifier import AmplitudeAmplifier, AmplitudeAmplifierResult +from .grover import Grover, GroverResult + +__all__ = [ + "AmplitudeAmplifier", + "AmplitudeAmplifierResult", + "AmplificationProblem", + "Grover", + "GroverResult", +] diff --git a/qiskit_optimization/amplitude_amplifiers/amplification_problem.py b/qiskit_optimization/amplitude_amplifiers/amplification_problem.py new file mode 100644 index 000000000..175fbcd83 --- /dev/null +++ b/qiskit_optimization/amplitude_amplifiers/amplification_problem.py @@ -0,0 +1,214 @@ +# This code is part of a Qiskit project. +# +# (C) Copyright IBM 2021, 2025. +# +# 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 Amplification problem class.""" +from __future__ import annotations + +from collections.abc import Callable +from typing import Any, List, cast + +from qiskit.circuit import QuantumCircuit +from qiskit.circuit.library import GroverOperator +from qiskit.quantum_info import Statevector + + +class AmplificationProblem: + """The amplification problem is the input to amplitude amplification algorithms, like Grover. + + This class contains all problem-specific information required to run an amplitude amplification + algorithm. It minimally contains the Grover operator. It can further hold some post processing + on the optimal bitstring. + """ + + # pylint: disable=too-many-positional-arguments + def __init__( + self, + oracle: QuantumCircuit | Statevector, + state_preparation: QuantumCircuit | None = None, + grover_operator: QuantumCircuit | None = None, + post_processing: Callable[[str], Any] | None = None, + objective_qubits: int | list[int] | None = None, + is_good_state: Callable[[str], bool] | list[int] | list[str] | Statevector | None = None, + ) -> None: + r""" + Args: + oracle: The oracle reflecting about the bad states. + state_preparation: A circuit preparing the input state, referred to as + :math:`\mathcal{A}`. If None, a layer of Hadamard gates is used. + grover_operator: The Grover operator :math:`\mathcal{Q}` used as unitary in the + phase estimation circuit. If None, this operator is constructed from the ``oracle`` + and ``state_preparation``. + post_processing: A mapping applied to the most likely bitstring. + objective_qubits: If set, specifies the indices of the qubits that should be measured. + If None, all qubits will be measured. The ``is_good_state`` function will be + applied on the measurement outcome of these qubits. + is_good_state: A function to check whether a string represents a good state. By default + if the ``oracle`` argument has an ``evaluate_bitstring`` method (currently only + provided by the :class:`~qiskit.circuit.library.PhaseOracle` class) this will be + used, otherwise this kwarg is required and **must** be specified. + """ + self._oracle = oracle + self._state_preparation = state_preparation + self._grover_operator = grover_operator + self._post_processing = post_processing + self._objective_qubits = objective_qubits + if is_good_state is not None: + self._is_good_state = is_good_state + elif hasattr(oracle, "evaluate_bitstring"): + self._is_good_state = oracle.evaluate_bitstring + else: + self._is_good_state = None + + @property + def oracle(self) -> QuantumCircuit | Statevector: + """Return the oracle. + + Returns: + The oracle. + """ + return self._oracle + + @oracle.setter + def oracle(self, oracle: QuantumCircuit | Statevector) -> None: + """Set the oracle. + + Args: + oracle: The oracle. + """ + self._oracle = oracle + + @property + def state_preparation(self) -> QuantumCircuit: + r"""Get the state preparation operator :math:`\mathcal{A}`. + + Returns: + The :math:`\mathcal{A}` operator as `QuantumCircuit`. + """ + if self._state_preparation is None: + state_preparation = QuantumCircuit(self.oracle.num_qubits) + state_preparation.h(state_preparation.qubits) + return state_preparation + + return self._state_preparation + + @state_preparation.setter + def state_preparation(self, state_preparation: QuantumCircuit | None) -> None: + r"""Set the :math:`\mathcal{A}` operator. If None, a layer of Hadamard gates is used. + + Args: + state_preparation: The new :math:`\mathcal{A}` operator or None. + """ + self._state_preparation = state_preparation + + @property + def post_processing(self) -> Callable[[str], Any]: + """Apply post processing to the input value. + + Returns: + A handle to the post processing function. Acts as identity by default. + """ + if self._post_processing is None: + return lambda x: x + + return self._post_processing + + @post_processing.setter + def post_processing(self, post_processing: Callable[[str], Any]) -> None: + """Set the post processing function. + + Args: + post_processing: A handle to the post processing function. + """ + self._post_processing = post_processing + + @property + def objective_qubits(self) -> list[int]: + """The indices of the objective qubits. + + Returns: + The indices of the objective qubits as list of integers. + """ + if self._objective_qubits is None: + return list(range(self.oracle.num_qubits)) + + if isinstance(self._objective_qubits, int): + return [self._objective_qubits] + + return self._objective_qubits + + @objective_qubits.setter + def objective_qubits(self, objective_qubits: int | list[int] | None) -> None: + """Set the objective qubits. + + Args: + objective_qubits: The indices of the qubits that should be measured. + If None, all qubits will be measured. The ``is_good_state`` function will be + applied on the measurement outcome of these qubits. + """ + self._objective_qubits = objective_qubits + + @property + def is_good_state(self) -> Callable[[str], bool]: + """Check whether a provided bitstring is a good state or not. + + Returns: + A callable that takes in a bitstring and returns True if the measurement is a good + state, False otherwise. + """ + if (self._is_good_state is None) or callable(self._is_good_state): + return self._is_good_state # returns None if no is_good_state arg has been set + elif isinstance(self._is_good_state, list): + if all(isinstance(good_bitstr, str) for good_bitstr in self._is_good_state): + return lambda bitstr: bitstr in cast(List[str], self._is_good_state) + else: + return lambda bitstr: all( + bitstr[good_index] == "1" for good_index in cast(List[int], self._is_good_state) + ) + + return lambda bitstr: bitstr in cast(Statevector, self._is_good_state).probabilities_dict() + + @is_good_state.setter + def is_good_state( + self, is_good_state: Callable[[str], bool] | list[int] | list[str] | Statevector + ) -> None: + """Set the ``is_good_state`` function. + + Args: + is_good_state: A function to determine whether a bitstring represents a good state. + """ + self._is_good_state = is_good_state + + @property + def grover_operator(self) -> QuantumCircuit | None: + r"""Get the :math:`\mathcal{Q}` operator, or Grover operator. + + If the Grover operator is not set, we try to build it from the :math:`\mathcal{A}` operator + and `objective_qubits`. This only works if `objective_qubits` is a list of integers. + + Returns: + The Grover operator, or None if neither the Grover operator nor the + :math:`\mathcal{A}` operator is set. + """ + if self._grover_operator is None: + return GroverOperator(self.oracle, self.state_preparation) + return self._grover_operator + + @grover_operator.setter + def grover_operator(self, grover_operator: QuantumCircuit | None) -> None: + r"""Set the :math:`\mathcal{Q}` operator. + + If None, this operator is constructed from the ``oracle`` and ``state_preparation``. + + Args: + grover_operator: The new :math:`\mathcal{Q}` operator or None. + """ + self._grover_operator = grover_operator diff --git a/qiskit_optimization/amplitude_amplifiers/amplitude_amplifier.py b/qiskit_optimization/amplitude_amplifiers/amplitude_amplifier.py new file mode 100644 index 000000000..6b19f3074 --- /dev/null +++ b/qiskit_optimization/amplitude_amplifiers/amplitude_amplifier.py @@ -0,0 +1,125 @@ +# This code is part of a Qiskit project. +# +# (C) Copyright IBM 2021, 2025. +# +# 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 interface for amplification algorithms and results.""" +from __future__ import annotations + +from abc import ABC, abstractmethod +from typing import Any + +from ..algorithm_result import AlgorithmResult +from .amplification_problem import AmplificationProblem + + +class AmplitudeAmplifier(ABC): + """The interface for amplification algorithms.""" + + @abstractmethod + def amplify(self, amplification_problem: AmplificationProblem) -> "AmplitudeAmplifierResult": + """Run the amplification algorithm. + + Args: + amplification_problem: The amplification problem. + + Returns: + The result as a ``AmplificationResult``, where e.g. the most likely state can be queried + as ``result.top_measurement``. + """ + raise NotImplementedError + + +class AmplitudeAmplifierResult(AlgorithmResult): + """The amplification result base class.""" + + def __init__(self) -> None: + super().__init__() + self._top_measurement: str | None = None + self._assignment = None + self._oracle_evaluation: bool | None = None + self._circuit_results: list[dict[str, int]] | None = None + self._max_probability: float | None = None + + @property + def top_measurement(self) -> str | None: + """The most frequently measured output as bitstring. + + Returns: + The most frequently measured output state. + """ + return self._top_measurement + + @top_measurement.setter + def top_measurement(self, value: str) -> None: + """Set the most frequently measured bitstring. + + Args: + value: A new value for the top measurement. + """ + self._top_measurement = value + + @property + def assignment(self) -> Any: + """The post-processed value of the most likely bitstring. + + Returns: + The output of the ``post_processing`` function of the respective + ``AmplificationProblem``, where the input is the ``top_measurement``. The type + is the same as the return type of the post-processing function. + """ + return self._assignment + + @assignment.setter + def assignment(self, value: Any) -> None: + """Set the value for the assignment. + + Args: + value: A new value for the assignment/solution. + """ + self._assignment = value + + @property + def oracle_evaluation(self) -> bool: + """Whether the classical oracle evaluation of the top measurement was True or False. + + Returns: + The classical oracle evaluation of the top measurement. + """ + return self._oracle_evaluation + + @oracle_evaluation.setter + def oracle_evaluation(self, value: bool) -> None: + """Set the classical oracle evaluation of the top measurement. + + Args: + value: A new value for the classical oracle evaluation. + """ + self._oracle_evaluation = value + + @property + def circuit_results(self) -> list[dict[str, int]] | None: + """Return the circuit results.""" + return self._circuit_results + + @circuit_results.setter + def circuit_results(self, value: list[dict[str, int]]) -> None: + """Set the circuit results.""" + self._circuit_results = value + + @property + def max_probability(self) -> float: + """Return the maximum sampling probability.""" + return self._max_probability + + @max_probability.setter + def max_probability(self, value: float) -> None: + """Set the maximum sampling probability.""" + self._max_probability = value diff --git a/qiskit_optimization/amplitude_amplifiers/grover.py b/qiskit_optimization/amplitude_amplifiers/grover.py new file mode 100644 index 000000000..6c53b753b --- /dev/null +++ b/qiskit_optimization/amplitude_amplifiers/grover.py @@ -0,0 +1,355 @@ +# This code is part of a Qiskit project. +# +# (C) Copyright IBM 2018, 2025. +# +# 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. + +"""Grover's search algorithm.""" +from __future__ import annotations + +import itertools +from collections.abc import Generator, Iterator +from typing import Any + +import numpy as np +from qiskit import ClassicalRegister, QuantumCircuit +from qiskit.primitives import BaseSampler +from qiskit.quantum_info import Statevector + +from qiskit_optimization.exceptions import AlgorithmError +from qiskit_optimization.utils import algorithm_globals + +from .amplification_problem import AmplificationProblem +from .amplitude_amplifier import AmplitudeAmplifier, AmplitudeAmplifierResult + + +class Grover(AmplitudeAmplifier): + r"""Grover's Search algorithm. + + .. note:: + + If you want to learn more about the theory behind Grover's Search algorithm, check + out the `Qiskit Textbook `_. + or the `Qiskit Tutorials + `_ + for more concrete how-to examples. + + Grover's Search [1, 2] is a well known quantum algorithm that can be used for + searching through unstructured collections of records for particular targets + with quadratic speedup compared to classical algorithms. + + Given a set :math:`X` of :math:`N` elements :math:`X=\{x_1,x_2,\ldots,x_N\}` + and a boolean function :math:`f : X \rightarrow \{0,1\}`, the goal of an + unstructured-search problem is to find an element :math:`x^* \in X` such + that :math:`f(x^*)=1`. + + The search is called *unstructured* because there are no guarantees as to how + the database is ordered. On a sorted database, for instance, one could perform + binary search to find an element in :math:`\mathbb{O}(\log N)` worst-case time. + Instead, in an unstructured-search problem, there is no prior knowledge about + the contents of the database. With classical circuits, there is no alternative + but to perform a linear number of queries to find the target element. + Conversely, Grover's Search algorithm allows to solve the unstructured-search + problem on a quantum computer in :math:`\mathcal{O}(\sqrt{N})` queries. + + To carry out this search a so-called oracle is required, that flags a good element/state. + The action of the oracle :math:`\mathcal{S}_f` is + + .. math:: + + \mathcal{S}_f |x\rangle = (-1)^{f(x)} |x\rangle, + + i.e. it flips the phase of the state :math:`|x\rangle` if :math:`x` is a hit. + The details of how :math:`S_f` works are unimportant to the algorithm; Grover's + search algorithm treats the oracle as a black box. + + This class supports oracles in form of a :class:`~qiskit.circuit.QuantumCircuit`. + + With the given oracle, Grover's Search constructs the Grover operator to amplify the + amplitudes of the good states: + + .. math:: + + \mathcal{Q} = H^{\otimes n} \mathcal{S}_0 H^{\otimes n} \mathcal{S}_f + = D \mathcal{S}_f, + + where :math:`\mathcal{S}_0` flips the phase of the all-zero state and acts as identity + on all other states. Sometimes the first three operands are summarized as diffusion operator, + which implements a reflection over the equal superposition state. + + If the number of solutions is known, we can calculate how often :math:`\mathcal{Q}` should be + applied to find a solution with very high probability, see the method + `optimal_num_iterations`. If the number of solutions is unknown, the algorithm tries different + powers of Grover's operator, see the `iterations` argument, and after each iteration checks + if a good state has been measured using `good_state`. + + The generalization of Grover's Search, Quantum Amplitude Amplification [3], uses a modified + version of :math:`\mathcal{Q}` where the diffusion operator does not reflect about the + equal superposition state, but another state specified via an operator :math:`\mathcal{A}`: + + .. math:: + + \mathcal{Q} = \mathcal{A} \mathcal{S}_0 \mathcal{A}^\dagger \mathcal{S}_f. + + For more information, see the :class:`~qiskit.circuit.library.GroverOperator` in the + circuit library. + + References: + [1]: L. K. Grover (1996), A fast quantum mechanical algorithm for database search, + `arXiv:quant-ph/9605043 `_. + [2]: I. Chuang & M. Nielsen, Quantum Computation and Quantum Information, + Cambridge: Cambridge University Press, 2000. Chapter 6.1.2. + [3]: Brassard, G., Hoyer, P., Mosca, M., & Tapp, A. (2000). + Quantum Amplitude Amplification and Estimation. + `arXiv:quant-ph/0005055 `_. + """ + + def __init__( + self, + iterations: list[int] | Iterator[int] | int | None = None, + growth_rate: float | None = None, + sample_from_iterations: bool = False, + sampler: BaseSampler | None = None, + ) -> None: + r""" + Args: + iterations: Specify the number of iterations/power of Grover's operator to be checked. + * If an int, only one circuit is run with that power of the Grover operator. + If the number of solutions is known, this option should be used with the optimal + power. The optimal power can be computed with ``Grover.optimal_num_iterations``. + * If a list, all the powers in the list are run in the specified order. + * If an iterator, the powers yielded by the iterator are checked, until a maximum + number of iterations or maximum power is reached. + * If ``None``, the :obj:`AmplificationProblem` provided must have an ``is_good_state``, + and circuits are run until that good state is reached. + growth_rate: If specified, the iterator is set to increasing powers of ``growth_rate``, + i.e. to ``int(growth_rate ** 1), int(growth_rate ** 2), ...`` until a maximum + number of iterations is reached. + sample_from_iterations: If True, instead of taking the values in ``iterations`` as + powers of the Grover operator, a random integer sample between 0 and smaller value + than the iteration is used as a power, see [1], Section 4. + sampler: A Sampler to use for sampling the results of the circuits. + + Raises: + ValueError: If ``growth_rate`` is a float but not larger than 1. + ValueError: If both ``iterations`` and ``growth_rate`` is set. + + References: + [1]: Boyer et al., Tight bounds on quantum searching + ``_ + """ + # set default value + if growth_rate is None and iterations is None: + growth_rate = 1.2 + + if growth_rate is not None and iterations is not None: + raise ValueError("Pass either a value for iterations or growth_rate, not both.") + + if growth_rate is not None: + # yield iterations ** 1, iterations ** 2, etc. and casts to int + self._iterations: Generator[int, None, None] | list[int] = ( + int(growth_rate**x) for x in itertools.count(1) + ) + elif isinstance(iterations, int): + self._iterations = [iterations] + else: + self._iterations = iterations # type: ignore[assignment] + + self._sampler = sampler + self._sample_from_iterations = sample_from_iterations + self._iterations_arg = iterations + + @property + def sampler(self) -> BaseSampler | None: + """Get the sampler. + + Returns: + The sampler used to run this algorithm. + """ + return self._sampler + + @sampler.setter + def sampler(self, sampler: BaseSampler) -> None: + """Set the sampler. + + Args: + sampler: The sampler used to run this algorithm. + """ + self._sampler = sampler + + def amplify(self, amplification_problem: AmplificationProblem) -> "GroverResult": + """Run the Grover algorithm. + + Args: + amplification_problem: The amplification problem. + + Returns: + The result as a ``GroverResult``, where e.g. the most likely state can be queried + as ``result.top_measurement``. + + Raises: + ValueError: If sampler is not set. + AlgorithmError: If sampler job fails. + TypeError: If ``is_good_state`` is not provided and is required (i.e. when iterations + is ``None`` or a ``list``) + """ + if self._sampler is None: + raise ValueError("A sampler must be provided.") + + if isinstance(self._iterations, list): + max_iterations = len(self._iterations) + max_power = np.inf # no cap on the power + iterator: Iterator[int] = iter(self._iterations) + else: + max_iterations = max(10, 2**amplification_problem.oracle.num_qubits) + max_power = np.ceil( + 2 ** (len(amplification_problem.grover_operator.reflection_qubits) / 2) + ) + iterator = self._iterations + + result = GroverResult() + + iterations = [] + top_measurement = "0" * len(amplification_problem.objective_qubits) + oracle_evaluation = False + all_circuit_results = [] + max_probability = 0 + + for _ in range(max_iterations): # iterate at most to the max number of iterations + # get next power and check if allowed + power = next(iterator) + + if power > max_power: + break + + iterations.append(power) # store power + + # sample from [0, power) if specified + if self._sample_from_iterations: + power = algorithm_globals.random.integers(power) + # Run a grover experiment for a given power of the Grover operator. + if self._sampler is not None: + qc = self.construct_circuit(amplification_problem, power, measurement=True) + job = self._sampler.run([qc]) + + try: + results = job.result() + except Exception as exc: + raise AlgorithmError("Sampler job failed.") from exc + + num_bits = len(amplification_problem.objective_qubits) + circuit_results: dict[str, Any] | Statevector | np.ndarray = { + np.binary_repr(k, num_bits): v for k, v in results.quasi_dists[0].items() + } + top_measurement, max_probability = max( + circuit_results.items(), key=lambda x: x[1] # type: ignore[union-attr] + ) + + all_circuit_results.append(circuit_results) + + if (isinstance(self._iterations_arg, int)) and ( + amplification_problem.is_good_state is None + ): + oracle_evaluation = None # cannot check for good state without is_good_state arg + break + + # is_good_state arg must be provided if iterations arg is not an integer + if ( + self._iterations_arg is None or isinstance(self._iterations_arg, list) + ) and amplification_problem.is_good_state is None: + raise TypeError("An is_good_state function is required with the provided oracle") + + # only check if top measurement is a good state if an is_good_state arg is provided + oracle_evaluation = amplification_problem.is_good_state(top_measurement) + + if oracle_evaluation is True: + break # we found a solution + + result.iterations = iterations + result.top_measurement = top_measurement + result.assignment = amplification_problem.post_processing(top_measurement) + result.oracle_evaluation = oracle_evaluation + result.circuit_results = all_circuit_results # type: ignore[assignment] + result.max_probability = max_probability + + return result + + @staticmethod + def optimal_num_iterations(num_solutions: int, num_qubits: int) -> int: + """Return the optimal number of iterations, if the number of solutions is known. + + Args: + num_solutions: The number of solutions. + num_qubits: The number of qubits used to encode the states. + + Returns: + The optimal number of iterations for Grover's algorithm to succeed. + """ + amplitude = np.sqrt(num_solutions / 2**num_qubits) + return round(np.arccos(amplitude) / (2 * np.arcsin(amplitude))) + + def construct_circuit( + self, problem: AmplificationProblem, power: int | None = None, measurement: bool = False + ) -> QuantumCircuit: + """Construct the circuit for Grover's algorithm with ``power`` Grover operators. + + Args: + problem: The amplification problem for the algorithm. + power: The number of times the Grover operator is repeated. If None, this argument + is set to the first item in ``iterations``. + measurement: Boolean flag to indicate if measurement should be included in the circuit. + + Returns: + QuantumCircuit: the QuantumCircuit object for the constructed circuit + + Raises: + ValueError: If no power is passed and the iterations are not an integer. + """ + if power is None: + if len(self._iterations) > 1: # type: ignore[arg-type] + raise ValueError("Please pass ``power`` if the iterations are not an integer.") + power = self._iterations[0] # type: ignore[index] + + qc = QuantumCircuit(problem.oracle.num_qubits, name="Grover circuit") + qc.compose(problem.state_preparation, inplace=True) + if power > 0: + qc.compose(problem.grover_operator.power(power), inplace=True) + + if measurement: + measurement_cr = ClassicalRegister(len(problem.objective_qubits)) + qc.add_register(measurement_cr) + qc.measure(problem.objective_qubits, measurement_cr) + + return qc + + +class GroverResult(AmplitudeAmplifierResult): + """Grover Result.""" + + def __init__(self) -> None: + super().__init__() + self._iterations: list[int] | None = None + + @property + def iterations(self) -> list[int]: + """All the powers of the Grover operator that have been tried. + + Returns: + The powers of the Grover operator tested. + """ + return self._iterations + + @iterations.setter + def iterations(self, value: list[int]) -> None: + """Set the powers of the Grover operator that have been tried. + + Args: + value: A new value for the powers. + """ + self._iterations = value diff --git a/qiskit_optimization/applications/tsp.py b/qiskit_optimization/applications/tsp.py index f19e23388..da8492b12 100644 --- a/qiskit_optimization/applications/tsp.py +++ b/qiskit_optimization/applications/tsp.py @@ -1,6 +1,6 @@ # This code is part of a Qiskit project. # -# (C) Copyright IBM 2018, 2024. +# (C) Copyright IBM 2018, 2025. # # 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 @@ -16,12 +16,12 @@ import networkx as nx import numpy as np from docplex.mp.model import Model -from qiskit_algorithms.utils import algorithm_globals from qiskit_optimization.algorithms import OptimizationResult from qiskit_optimization.exceptions import QiskitOptimizationError from qiskit_optimization.problems import QuadraticProgram from qiskit_optimization.translators import from_docplex_mp +from qiskit_optimization.utils import algorithm_globals from .graph_optimization_application import GraphOptimizationApplication diff --git a/qiskit_optimization/minimum_eigensolvers/__init__.py b/qiskit_optimization/minimum_eigensolvers/__init__.py index 255e1950a..39e05a02e 100644 --- a/qiskit_optimization/minimum_eigensolvers/__init__.py +++ b/qiskit_optimization/minimum_eigensolvers/__init__.py @@ -12,11 +12,12 @@ """The Minimum Eigensolvers package.""" -from .sampling_mes import SamplingMinimumEigensolver, SamplingMinimumEigensolverResult from .minimum_eigensolver import MinimumEigensolver, MinimumEigensolverResult from .numpy_minimum_eigensolver import NumPyMinimumEigensolver, NumPyMinimumEigensolverResult from .qaoa import QAOA +from .sampling_mes import SamplingMinimumEigensolver, SamplingMinimumEigensolverResult from .sampling_vqe import SamplingVQE +from .vqe import VQE, VQEResult __all__ = [ "SamplingMinimumEigensolver", @@ -27,4 +28,6 @@ "NumPyMinimumEigensolverResult", "SamplingVQE", "QAOA", + "VQE", + "VQEResult", ] diff --git a/qiskit_optimization/minimum_eigensolvers/sampling_vqe.py b/qiskit_optimization/minimum_eigensolvers/sampling_vqe.py index 1d84232cd..f4e416310 100644 --- a/qiskit_optimization/minimum_eigensolvers/sampling_vqe.py +++ b/qiskit_optimization/minimum_eigensolvers/sampling_vqe.py @@ -20,7 +20,6 @@ from typing import Any import numpy as np - from qiskit.circuit import QuantumCircuit from qiskit.passmanager import BasePassManager from qiskit.primitives import BaseSamplerV1, BaseSamplerV2 @@ -28,20 +27,19 @@ from qiskit.quantum_info.operators.base_operator import BaseOperator from qiskit.result import QuasiDistribution -from ..variational_algorithm import VariationalAlgorithm, VariationalResult from ..exceptions import AlgorithmError from ..list_or_dict import ListOrDict from ..minimum_eigensolvers.sampling_mes import ( SamplingMinimumEigensolver, SamplingMinimumEigensolverResult, ) - from ..observables_evaluator import estimate_observables from ..optimizers.optimizer import Minimizer, Optimizer, OptimizerResult from ..utils import validate_bounds, validate_initial_point # private function as we expect this to be updated in the next released from ..utils.set_batching import _set_default_batchsize +from ..variational_algorithm import VariationalAlgorithm, VariationalResult from .diagonal_estimator import _DiagonalEstimator logger = logging.getLogger(__name__) @@ -51,16 +49,16 @@ class SamplingVQE(VariationalAlgorithm, SamplingMinimumEigensolver): r"""The Variational Quantum Eigensolver algorithm, optimized for diagonal Hamiltonians. VQE is a hybrid quantum-classical algorithm that uses a variational technique to find the minimum eigenvalue of a given diagonal Hamiltonian operator :math:`H_{\text{diag}}`. - In contrast to the :class:`~qiskit_algorithms.minimum_eigensolvers.VQE` class, the + In contrast to the :class:`~qiskit_optimization.minimum_eigensolvers.VQE` class, the ``SamplingVQE`` algorithm is executed using a :attr:`sampler` primitive. An instance of ``SamplingVQE`` 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` to minimize the objective function, which depends on the chosen :attr:`aggregation`. The optimizer can either be one of Qiskit's optimizers, such as - :class:`~qiskit_algorithms.optimizers.SPSA` or a callable with the following signature: + :class:`~qiskit_optimization.optimizers.SPSA` or a callable with the following signature: .. code-block:: python - from qiskit_algorithms.optimizers import OptimizerResult + from qiskit_optimization.optimizers import OptimizerResult def my_minimizer(fun, x0, jac=None, bounds=None) -> OptimizerResult: # Note that the callable *must* have these argument names! # Args: diff --git a/qiskit_optimization/minimum_eigensolvers/vqe.py b/qiskit_optimization/minimum_eigensolvers/vqe.py new file mode 100644 index 000000000..c68570610 --- /dev/null +++ b/qiskit_optimization/minimum_eigensolvers/vqe.py @@ -0,0 +1,367 @@ +# This code is part of a Qiskit project. +# +# (C) Copyright IBM 2022, 2025. +# +# 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 + +import logging +from collections.abc import Callable +from time import time +from typing import Any + +import numpy as np +from qiskit.circuit import QuantumCircuit +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 ..observables_evaluator import estimate_observables +from ..optimizers import Minimizer, Optimizer, OptimizerResult +from ..utils import validate_bounds, validate_initial_point + +# private function as we expect this to be updated in the next released +from ..utils.set_batching import _set_default_batchsize +from ..variational_algorithm import VariationalAlgorithm, VariationalResult +from .minimum_eigensolver import MinimumEigensolver, MinimumEigensolverResult + +logger = logging.getLogger(__name__) + + +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:`H`. + + 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, + + .. math:: + + \min_{\vec\theta} \langle\psi(\vec\theta)|H|\psi(\vec\theta)\rangle. + + 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_optimization.optimizers.SPSA` or a callable with the following signature: + + .. code-block:: python + + from qiskit_optimization.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 one to use 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") + + 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. + 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. + 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, A., et al, "A variational eigenvalue solver on a quantum processor" + `arXiv:1304.3061 `__ + """ + + def __init__( + self, + estimator: BaseEstimator, + ansatz: QuantumCircuit, + optimizer: Optimizer | Minimizer, + *, + gradient: None = None, + initial_point: np.ndarray | None = None, + callback: Callable[[int, np.ndarray, float, dict[str, Any]], None] | None = None, + ) -> None: + r""" + Args: + 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. 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. + """ + super().__init__() + + self.estimator = estimator + 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) -> np.ndarray | None: + return self._initial_point + + @initial_point.setter + def initial_point(self, value: np.ndarray | None) -> None: + self._initial_point = value + + def compute_minimum_eigenvalue( + self, + operator: BaseOperator, + aux_operators: ListOrDict[BaseOperator] | None = None, + ) -> VQEResult: + self._check_operator_ansatz(operator) + + initial_point = validate_initial_point(self.initial_point, self.ansatz) + + bounds = validate_bounds(self.ansatz) + + start_time = time() + + evaluate_energy = self._get_evaluate_energy(self.ansatz, operator) + + if self.gradient is not None: + evaluate_gradient = self._get_evaluate_gradient(self.ansatz, operator) + else: + evaluate_gradient = None + + # perform optimization + if callable(self.optimizer): + optimizer_result = self.optimizer( + fun=evaluate_energy, # type: ignore[arg-type] + x0=initial_point, + jac=evaluate_gradient, + bounds=bounds, + ) + else: + # we always want to submit as many estimations per job as possible for minimal + # overhead on the hardware + was_updated = _set_default_batchsize(self.optimizer) + + optimizer_result = self.optimizer.minimize( + fun=evaluate_energy, # type: ignore[arg-type] + x0=initial_point, + jac=evaluate_gradient, # type: ignore[arg-type] + bounds=bounds, + ) + + # reset to original value + if was_updated: + self.optimizer.set_max_evals_grouped(None) + + optimizer_time = time() - start_time + + logger.info( + "Optimization complete in %s seconds.\nFound optimal point %s", + optimizer_time, + optimizer_result.x, + ) + + if aux_operators is not None: + aux_operators_evaluated = estimate_observables( + self.estimator, + self.ansatz, + aux_operators, + optimizer_result.x, # type: ignore[arg-type] + ) + else: + aux_operators_evaluated = None + + return self._build_vqe_result( + self.ansatz, + optimizer_result, + aux_operators_evaluated, # type: ignore[arg-type] + optimizer_time, + ) + + @classmethod + def supports_aux_operators(cls) -> bool: + return True + + def _get_evaluate_energy( + self, + ansatz: QuantumCircuit, + operator: BaseOperator, + ) -> 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. + """ + num_parameters = ansatz.num_parameters + + # avoid creating an instance variable to remain stateless regarding results + eval_count = 0 + + def evaluate_energy(parameters: np.ndarray) -> np.ndarray | float: + nonlocal eval_count + + # handle broadcasting: ensure parameters is of shape [array, array, ...] + parameters = np.reshape(parameters, (-1, num_parameters)).tolist() + batch_size = len(parameters) + + try: + 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 + + values = estimator_result.values + + if self.callback is not None: + metadata = estimator_result.metadata + for params, value, meta in zip(parameters, values, metadata): + eval_count += 1 + self.callback(eval_count, params, value, meta) + + energy = values[0] if len(values) == 1 else values + + return energy + + return evaluate_energy + + def _get_evaluate_gradient( + self, + ansatz: QuantumCircuit, + operator: BaseOperator, + ) -> Callable[[np.ndarray], np.ndarray]: + """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. + """ + + def evaluate_gradient(parameters: np.ndarray) -> np.ndarray: + # broadcasting not required for the estimator gradients + try: + 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 + + return gradients[0] + + return evaluate_gradient + + def _check_operator_ansatz(self, operator: BaseOperator): + """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: + try: + logger.info( + "Trying to resize ansatz to match operator on %s 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 " + "operator, and the ansatz does not allow setting the " + "number of qubits using `num_qubits`." + ) from error + + if self.ansatz.num_parameters == 0: + raise AlgorithmError("The ansatz must be parameterized, but has no free parameters.") + + def _build_vqe_result( + self, + ansatz: QuantumCircuit, + optimizer_result: OptimizerResult, + aux_operators_evaluated: ListOrDict[tuple[complex, tuple[complex, int]]], + optimizer_time: float, + ) -> VQEResult: + result = VQEResult() + result.optimal_circuit = ansatz.copy() + result.eigenvalue = optimizer_result.fun + result.cost_function_evals = optimizer_result.nfev + result.optimal_point = optimizer_result.x # type: ignore[assignment] + result.optimal_parameters = dict( + zip(self.ansatz.parameters, optimizer_result.x) # type: ignore[arg-type] + ) + result.optimal_value = optimizer_result.fun + result.optimizer_time = optimizer_time + result.aux_operators_evaluated = aux_operators_evaluated # type: ignore[assignment] + result.optimizer_result = optimizer_result + return result + + +class VQEResult(VariationalResult, MinimumEigensolverResult): + """The Variational Quantum Eigensolver (VQE) result.""" + + def __init__(self) -> None: + super().__init__() + self._cost_function_evals: int | None = None + + @property + def cost_function_evals(self) -> int | None: + """The number of cost optimizer evaluations.""" + return self._cost_function_evals + + @cost_function_evals.setter + def cost_function_evals(self, value: int) -> None: + self._cost_function_evals = value diff --git a/qiskit_optimization/optimizers/__init__.py b/qiskit_optimization/optimizers/__init__.py index c321a985b..c833da331 100644 --- a/qiskit_optimization/optimizers/__init__.py +++ b/qiskit_optimization/optimizers/__init__.py @@ -11,23 +11,20 @@ # that they have been altered from the originals. """ -Optimizers (:mod:`qiskit_algorithms.optimizers`) +Optimizers (:mod:`qiskit_optimization.optimizers`) ================================================ Classical Optimizers. This package contains a variety of classical optimizers and were designed for use by -qiskit_algorithm's quantum variational algorithms, such as :class:`~qiskit_algorithms.VQE`. +qiskit_optimization's quantum variational algorithms, such as +:class:`~qiskit_optimization.minimum_eigensolvers.SamplingVQE`. Logically, these optimizers can be divided into two categories: `Local Optimizers`_ Given an optimization problem, a **local optimizer** is a function that attempts to find an optimal value within the neighboring set of a candidate solution. -`Global Optimizers`_ - Given an optimization problem, a **global optimizer** is a function - that attempts to find an optimal value among all possible solutions. - -.. currentmodule:: qiskit_algorithms.optimizers +.. currentmodule:: qiskit_optimization.optimizers Optimizer Base Classes ---------------------- @@ -36,28 +33,11 @@ :toctree: ../stubs/ :nosignatures: - OptimizerResult Optimizer + OptimizerResult + OptimizerSupportLevel Minimizer -Steppable Optimization ----------------------- - -.. autosummary:: - :toctree: ../stubs/ - - optimizer_utils - -.. autosummary:: - :toctree: ../stubs/ - :nosignatures: - - SteppableOptimizer - AskData - TellData - OptimizerState - - Local Optimizers ---------------- @@ -65,62 +45,18 @@ :toctree: ../stubs/ :nosignatures: - ADAM - AQGD - CG COBYLA - L_BFGS_B - GSLS - GradientDescent - GradientDescentState NELDER_MEAD - NFT - P_BFGS - POWELL - SLSQP SPSA - QNSPSA - TNC SciPyOptimizer - UMDA - -Qiskit also provides the following optimizers, which are built-out using the optimizers from -`scikit-quant `_. The ``scikit-quant`` package -is not installed by default but must be explicitly installed, if desired, by the user. The -optimizers therein are provided under various licenses, hence it has been made an optional install. -To install the ``scikit-quant`` dependent package you can use ``pip install scikit-quant``. - -.. autosummary:: - :toctree: ../stubs/ - :nosignatures: - - BOBYQA - IMFIL - SNOBFIT - -Global Optimizers ------------------ -The global optimizers here all use `NLOpt `_ for their -core function and can only be used if the optional dependent ``NLOpt`` package is installed. -To install the ``NLOpt`` dependent package you can use ``pip install nlopt``. - -.. autosummary:: - :toctree: ../stubs/ - :nosignatures: - - CRS - DIRECT_L - DIRECT_L_RAND - ESCH - ISRES """ -from .optimizer import Minimizer, Optimizer, OptimizerResult, OptimizerSupportLevel -from .spsa import SPSA from .cobyla import COBYLA from .nelder_mead import NELDER_MEAD +from .optimizer import Minimizer, Optimizer, OptimizerResult, OptimizerSupportLevel from .scipy_optimizer import SciPyOptimizer +from .spsa import SPSA __all__ = [ "Optimizer", diff --git a/qiskit_optimization/optimizers/spsa.py b/qiskit_optimization/optimizers/spsa.py index 9d09165e0..11a27395b 100644 --- a/qiskit_optimization/optimizers/spsa.py +++ b/qiskit_optimization/optimizers/spsa.py @@ -16,19 +16,18 @@ """ from __future__ import annotations -from collections import deque -from collections.abc import Iterator -from typing import Callable, Any, SupportsFloat import logging import warnings +from collections import deque +from collections.abc import Iterator from time import time +from typing import Any, Callable, SupportsFloat -import scipy import numpy as np +import scipy from ..utils import algorithm_globals - -from .optimizer import Optimizer, OptimizerSupportLevel, OptimizerResult, POINT +from .optimizer import POINT, Optimizer, OptimizerResult, OptimizerSupportLevel # number of function evaluations, parameters, loss, stepsize, accepted CALLBACK = Callable[[int, np.ndarray, float, SupportsFloat, bool], None] @@ -77,7 +76,7 @@ class SPSA(Optimizer): This component has some function that is normally random. If you want to reproduce behavior then you should set the random number generator seed in the algorithm_globals - (``qiskit_algorithms.utils.algorithm_globals.random_seed = seed``). + (``qiskit_optimization.utils.algorithm_globals.random_seed = seed``). Examples: @@ -88,7 +87,7 @@ class SPSA(Optimizer): .. code-block:: python import numpy as np - from qiskit_algorithms.optimizers import SPSA + from qiskit_optimization.optimizers import SPSA from qiskit.circuit.library import PauliTwoDesign from qiskit.primitives import Estimator from qiskit.quantum_info import SparsePauliOp @@ -118,7 +117,7 @@ def loss(x): .. code-block:: python import numpy as np - from qiskit_algorithms.optimizers import SPSA + from qiskit_optimization.optimizers import SPSA def objective(x): return np.linalg.norm(x) + .04*np.random.rand(1) diff --git a/qiskit_optimization/utils/__init__.py b/qiskit_optimization/utils/__init__.py index 74eda534f..284e5e973 100644 --- a/qiskit_optimization/utils/__init__.py +++ b/qiskit_optimization/utils/__init__.py @@ -10,11 +10,11 @@ # copyright notice, and modified files need to carry a notice indicating # that they have been altered from the originals. -"""Common qiskit_algorithms utility functions.""" +"""Common qiskit optimization algorithms utility functions.""" from .algorithm_globals import algorithm_globals -from .validate_initial_point import validate_initial_point from .validate_bounds import validate_bounds +from .validate_initial_point import validate_initial_point __all__ = [ "algorithm_globals", diff --git a/qiskit_optimization/utils/algorithm_globals.py b/qiskit_optimization/utils/algorithm_globals.py index 26bc07ab0..58bf9d1f1 100644 --- a/qiskit_optimization/utils/algorithm_globals.py +++ b/qiskit_optimization/utils/algorithm_globals.py @@ -13,9 +13,9 @@ """ utils.algorithm_globals ======================= -Common (global) properties used across qiskit_algorithms. +Common (global) properties used across qiskit_optimization. -.. currentmodule:: qiskit_algorithms.utils.algorithm_globals +.. currentmodule:: qiskit_optimization.utils.algorithm_globals Includes: @@ -50,7 +50,7 @@ class QiskitAlgorithmGlobals: # calls off to it). In the future when that does not exist this has similar code # in the except blocks here, as noted above, that will take over. By delegating # to the Qiskit instance it means that any existing code that uses that continues - # to work. Logic here in qiskit_algorithms though uses this instance and the + # to work. Logic here in qiskit_optimization though uses this instance and the # random check here has logic to warn if the seed here is not the same as the Qiskit # version so we can detect direct usage of the Qiskit version and alert the user to # change their code to use this. So simply changing from: @@ -114,7 +114,7 @@ def random(self) -> np.random.Generator: warnings.warn( "Using random that is seeded via qiskit.utils algorithm_globals is deprecated " "since version 0.2.0. Instead set random_seed directly to " - "qiskit_algorithms.utils algorithm_globals.", + "qiskit_optimization.utils algorithm_globals.", category=DeprecationWarning, stacklevel=2, ) diff --git a/qiskit_optimization/variational_algorithm.py b/qiskit_optimization/variational_algorithm.py index c53c5014c..e51890538 100644 --- a/qiskit_optimization/variational_algorithm.py +++ b/qiskit_optimization/variational_algorithm.py @@ -23,13 +23,14 @@ This component has some function that is normally random. If you want to reproduce behavior then you should set the random number generator seed in the algorithm_globals - (``qiskit_algorithms.utils.algorithm_globals.random_seed = seed``). + (``qiskit_optimization.utils.algorithm_globals.random_seed = seed``). """ from __future__ import annotations + from abc import ABC, abstractmethod -import numpy as np +import numpy as np from qiskit.circuit import QuantumCircuit from .algorithm_result import AlgorithmResult diff --git a/requirements.txt b/requirements.txt index d42210b37..589cf487f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,5 @@ qiskit>=0.44 -qiskit-algorithms>=0.2.0 -scipy>=1.9.0,<1.14 +scipy>=1.9.0 numpy>=1.17,<2.2.0 docplex>=2.21.207,!=2.24.231 setuptools>=40.1.0 diff --git a/test/algorithms/qrao/test_magic_rounding.py b/test/algorithms/qrao/test_magic_rounding.py index 98bdf5832..39d07d885 100644 --- a/test/algorithms/qrao/test_magic_rounding.py +++ b/test/algorithms/qrao/test_magic_rounding.py @@ -1,6 +1,6 @@ # This code is part of a Qiskit project. # -# (C) Copyright IBM 2023, 2024. +# (C) Copyright IBM 2023, 2025. # # 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 @@ -18,7 +18,6 @@ import numpy as np from qiskit.circuit import QuantumCircuit from qiskit.primitives import Sampler -from qiskit_algorithms import NumPyMinimumEigensolver from qiskit_optimization.algorithms import OptimizationResultStatus, SolutionSample from qiskit_optimization.algorithms.qrao import ( @@ -29,6 +28,7 @@ RoundingResult, ) from qiskit_optimization.applications import Maxcut +from qiskit_optimization.minimum_eigensolvers import NumPyMinimumEigensolver from qiskit_optimization.problems import QuadraticProgram diff --git a/test/algorithms/qrao/test_quantum_random_access_optimizer.py b/test/algorithms/qrao/test_quantum_random_access_optimizer.py index 30d0cbbf8..a871c60ad 100644 --- a/test/algorithms/qrao/test_quantum_random_access_optimizer.py +++ b/test/algorithms/qrao/test_quantum_random_access_optimizer.py @@ -1,6 +1,6 @@ # This code is part of a Qiskit project. # -# (C) Copyright IBM 2023, 2024. +# (C) Copyright IBM 2023, 2025. # # 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 @@ -17,9 +17,6 @@ import numpy as np from qiskit.circuit.library import RealAmplitudes from qiskit.primitives import Estimator -from qiskit_algorithms import VQE, NumPyMinimumEigensolver, NumPyMinimumEigensolverResult, VQEResult -from qiskit_algorithms.optimizers import COBYLA -from qiskit_algorithms.utils import algorithm_globals from qiskit_optimization.algorithms import SolutionSample from qiskit_optimization.algorithms.optimization_algorithm import OptimizationResultStatus @@ -30,7 +27,15 @@ RoundingContext, RoundingResult, ) +from qiskit_optimization.minimum_eigensolvers import ( + VQE, + NumPyMinimumEigensolver, + NumPyMinimumEigensolverResult, + VQEResult, +) +from qiskit_optimization.optimizers import COBYLA from qiskit_optimization.problems import QuadraticProgram +from qiskit_optimization.utils import algorithm_globals class TestQuantumRandomAccessOptimizer(QiskitOptimizationTestCase): diff --git a/test/algorithms/test_grover_optimizer.py b/test/algorithms/test_grover_optimizer.py index b16d2b2a3..ea4d164cc 100644 --- a/test/algorithms/test_grover_optimizer.py +++ b/test/algorithms/test_grover_optimizer.py @@ -1,6 +1,6 @@ # This code is part of a Qiskit project. # -# (C) Copyright IBM 2020, 2023. +# (C) Copyright IBM 2020, 2025. # # 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 @@ -19,8 +19,6 @@ from docplex.mp.model import Model from qiskit.utils import optionals from qiskit_aer.primitives import Sampler -from qiskit_algorithms import NumPyMinimumEigensolver -from qiskit_algorithms.utils import algorithm_globals from qiskit_optimization.algorithms import ( GroverOptimizer, @@ -34,8 +32,10 @@ MaximizeToMinimize, QuadraticProgramToQubo, ) +from qiskit_optimization.minimum_eigensolvers import NumPyMinimumEigensolver from qiskit_optimization.problems import QuadraticProgram from qiskit_optimization.translators import from_docplex_mp +from qiskit_optimization.utils import algorithm_globals class TestGroverOptimizer(QiskitOptimizationTestCase): diff --git a/test/algorithms/test_min_eigen_optimizer.py b/test/algorithms/test_min_eigen_optimizer.py index c1251d6bb..b8b6328a8 100644 --- a/test/algorithms/test_min_eigen_optimizer.py +++ b/test/algorithms/test_min_eigen_optimizer.py @@ -1,6 +1,6 @@ # This code is part of a Qiskit project. # -# (C) Copyright IBM 2018, 2023. +# (C) Copyright IBM 2018, 2025. # # 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 @@ -19,9 +19,6 @@ from ddt import data, ddt, unpack from qiskit.circuit.library import TwoLocal from qiskit.primitives import Estimator, Sampler -from qiskit_algorithms import QAOA, VQE, NumPyMinimumEigensolver, SamplingVQE -from qiskit_algorithms.optimizers import COBYLA, SPSA -from qiskit_algorithms.utils import algorithm_globals import qiskit_optimization.optionals as _optionals from qiskit_optimization.algorithms import CplexOptimizer, MinimumEigenOptimizer @@ -34,7 +31,10 @@ QuadraticProgramToQubo, ) from qiskit_optimization.exceptions import QiskitOptimizationError +from qiskit_optimization.minimum_eigensolvers import QAOA, VQE, NumPyMinimumEigensolver, SamplingVQE +from qiskit_optimization.optimizers import COBYLA, SPSA from qiskit_optimization.problems import QuadraticProgram +from qiskit_optimization.utils import algorithm_globals @ddt diff --git a/test/algorithms/test_recursive_optimization.py b/test/algorithms/test_recursive_optimization.py index b3970563a..3a02f45f4 100644 --- a/test/algorithms/test_recursive_optimization.py +++ b/test/algorithms/test_recursive_optimization.py @@ -1,6 +1,6 @@ # This code is part of a Qiskit project. # -# (C) Copyright IBM 2018, 2023. +# (C) Copyright IBM 2018, 2025. # # 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 @@ -17,9 +17,6 @@ import numpy as np from qiskit.primitives import Sampler -from qiskit_algorithms import QAOA, NumPyMinimumEigensolver -from qiskit_algorithms.optimizers import SLSQP -from qiskit_algorithms.utils import algorithm_globals import qiskit_optimization.optionals as _optionals from qiskit_optimization.algorithms import ( @@ -36,7 +33,10 @@ LinearEqualityToPenalty, QuadraticProgramToQubo, ) +from qiskit_optimization.minimum_eigensolvers import QAOA, NumPyMinimumEigensolver +from qiskit_optimization.optimizers import COBYLA from qiskit_optimization.problems import QuadraticProgram +from qiskit_optimization.utils import algorithm_globals class TestRecursiveMinEigenOptimizer(QiskitOptimizationTestCase): @@ -132,7 +132,7 @@ def test_recursive_warm_qaoa(self): algorithm_globals.random_seed = seed qaoa = QAOA( sampler=Sampler(), - optimizer=SLSQP(), + optimizer=COBYLA(), reps=1, ) warm_qaoa = WarmStartQAOAOptimizer( diff --git a/test/algorithms/test_warm_start_qaoa.py b/test/algorithms/test_warm_start_qaoa.py index f40d9a361..3ddfccf7a 100644 --- a/test/algorithms/test_warm_start_qaoa.py +++ b/test/algorithms/test_warm_start_qaoa.py @@ -1,6 +1,6 @@ # This code is part of a Qiskit project. # -# (C) Copyright IBM 2021, 2023. +# (C) Copyright IBM 2021, 2025. # # 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 @@ -18,8 +18,6 @@ import numpy as np from docplex.mp.model import Model from qiskit.primitives.sampler import Sampler -from qiskit_algorithms import QAOA -from qiskit_algorithms.optimizers import SLSQP import qiskit_optimization.optionals as _optionals from qiskit_optimization.algorithms import SlsqpOptimizer @@ -29,6 +27,8 @@ WarmStartQAOAOptimizer, ) from qiskit_optimization.applications.max_cut import Maxcut +from qiskit_optimization.minimum_eigensolvers import QAOA +from qiskit_optimization.optimizers import COBYLA from qiskit_optimization.translators import from_docplex_mp @@ -50,7 +50,7 @@ def test_max_cut(self): presolver = GoemansWilliamsonOptimizer(num_cuts=10) problem = Maxcut(graph).to_quadratic_program() - qaoa = QAOA(sampler=Sampler(), optimizer=SLSQP(), reps=1) + qaoa = QAOA(sampler=Sampler(), optimizer=COBYLA(), reps=1) aggregator = MeanAggregator() optimizer = WarmStartQAOAOptimizer( pre_solver=presolver, @@ -82,7 +82,7 @@ def test_constrained_binary(self): problem = from_docplex_mp(model) - qaoa = QAOA(sampler=Sampler(), optimizer=SLSQP(), reps=1) + qaoa = QAOA(sampler=Sampler(), optimizer=COBYLA(), reps=1) aggregator = MeanAggregator() optimizer = WarmStartQAOAOptimizer( pre_solver=SlsqpOptimizer(), @@ -109,7 +109,7 @@ def test_simple_qubo(self): model.minimize((u - v + 2) ** 2) problem = from_docplex_mp(model) - qaoa = QAOA(sampler=Sampler(), optimizer=SLSQP(), reps=1) + qaoa = QAOA(sampler=Sampler(), optimizer=COBYLA(), reps=1) optimizer = WarmStartQAOAOptimizer( pre_solver=SlsqpOptimizer(), relax_for_pre_solver=True, diff --git a/test/amplitude_amplifiers/test_grover.py b/test/amplitude_amplifiers/test_grover.py new file mode 100644 index 000000000..850d29f6c --- /dev/null +++ b/test/amplitude_amplifiers/test_grover.py @@ -0,0 +1,320 @@ +# This code is part of a Qiskit project. +# +# (C) Copyright IBM 2018, 2025. +# +# 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 Grover's algorithm.""" + +import itertools +import unittest +from test import QiskitAlgorithmsTestCase + +import numpy as np +from ddt import data, ddt, idata, unpack +from qiskit import QuantumCircuit +from qiskit.circuit.library import GroverOperator, PhaseOracle +from qiskit.primitives import Sampler +from qiskit.quantum_info import Operator, Statevector +from qiskit.utils.optionals import HAS_TWEEDLEDUM + +from qiskit_optimization.amplitude_amplifiers import AmplificationProblem, Grover + + +@ddt +class TestAmplificationProblem(QiskitAlgorithmsTestCase): + """Test the amplification problem.""" + + def setUp(self): + super().setUp() + oracle = QuantumCircuit(2) + oracle.cz(0, 1) + self._expected_grover_op = GroverOperator(oracle=oracle) + + @data("oracle_only", "oracle_and_stateprep") + def test_groverop_getter(self, kind): + """Test the default construction of the Grover operator.""" + oracle = QuantumCircuit(2) + oracle.cz(0, 1) + + if kind == "oracle_only": + problem = AmplificationProblem(oracle, is_good_state=["11"]) + expected = GroverOperator(oracle) + else: + stateprep = QuantumCircuit(2) + stateprep.ry(0.2, [0, 1]) + problem = AmplificationProblem( + oracle, state_preparation=stateprep, is_good_state=["11"] + ) + expected = GroverOperator(oracle, stateprep) + + self.assertEqual(Operator(expected), Operator(problem.grover_operator)) + + @data("list_str", "list_int", "statevector", "callable") + def test_is_good_state(self, kind): + """Test is_good_state works on different input types.""" + if kind == "list_str": + is_good_state = ["01", "11"] + elif kind == "list_int": + is_good_state = [1] # means bitstr[1] == '1' + elif kind == "statevector": + is_good_state = Statevector(np.array([0, 1, 0, 1]) / np.sqrt(2)) + else: + + def is_good_state(bitstr): + # same as ``bitstr in ['01', '11']`` + return bitstr[1] == "1" + + possible_states = [ + "".join(list(map(str, item))) for item in itertools.product([0, 1], repeat=2) + ] + + oracle = QuantumCircuit(2) + problem = AmplificationProblem(oracle, is_good_state=is_good_state) + + expected = [state in ["01", "11"] for state in possible_states] + actual = [problem.is_good_state(state) for state in possible_states] + + self.assertListEqual(expected, actual) + + +@ddt +class TestGrover(QiskitAlgorithmsTestCase): + """Test for the functionality of Grover""" + + def setUp(self): + super().setUp() + self._sampler = Sampler() + self._sampler_with_shots = Sampler(options={"shots": 1024, "seed": 123}) + + @unittest.skipUnless(HAS_TWEEDLEDUM, "tweedledum required for this test") + @data("ideal", "shots") + def test_implicit_phase_oracle_is_good_state(self, use_sampler): + """Test implicit default for is_good_state with PhaseOracle.""" + grover = self._prepare_grover(use_sampler) + oracle = PhaseOracle("x & y") + problem = AmplificationProblem(oracle) + result = grover.amplify(problem) + self.assertEqual(result.top_measurement, "11") + + @idata(itertools.product(["ideal", "shots"], [[1, 2, 3], None, 2])) + @unpack + def test_iterations_with_good_state(self, use_sampler, iterations): + """Test the algorithm with different iteration types and with good state""" + grover = self._prepare_grover(use_sampler, iterations) + problem = AmplificationProblem(Statevector.from_label("111"), is_good_state=["111"]) + result = grover.amplify(problem) + self.assertEqual(result.top_measurement, "111") + + @idata(itertools.product(["shots"], [[1, 2, 3], None, 2])) + @unpack + def test_iterations_with_good_state_sample_from_iterations(self, use_sampler, iterations): + """Test the algorithm with different iteration types and with good state""" + grover = self._prepare_grover(use_sampler, iterations, sample_from_iterations=True) + problem = AmplificationProblem(Statevector.from_label("111"), is_good_state=["111"]) + result = grover.amplify(problem) + self.assertEqual(result.top_measurement, "111") + + @data("ideal", "shots") + def test_fixed_iterations_without_good_state(self, use_sampler): + """Test the algorithm with iterations as an int and without good state""" + grover = self._prepare_grover(use_sampler, iterations=2) + problem = AmplificationProblem(Statevector.from_label("111")) + result = grover.amplify(problem) + self.assertEqual(result.top_measurement, "111") + + @idata(itertools.product(["ideal", "shots"], [[1, 2, 3], None])) + @unpack + def test_iterations_without_good_state(self, use_sampler, iterations): + """Test the correct error is thrown for none/list of iterations and without good state""" + grover = self._prepare_grover(use_sampler, iterations=iterations) + problem = AmplificationProblem(Statevector.from_label("111")) + + with self.assertRaisesRegex( + TypeError, "An is_good_state function is required with the provided oracle" + ): + grover.amplify(problem) + + @data("ideal", "shots") + def test_iterator(self, use_sampler): + """Test running the algorithm on an iterator.""" + + # step-function iterator + def iterator(): + wait, value, count = 3, 1, 0 + while True: + yield value + count += 1 + if count % wait == 0: + value += 1 + + grover = self._prepare_grover(use_sampler, iterations=iterator()) + problem = AmplificationProblem(Statevector.from_label("111"), is_good_state=["111"]) + result = grover.amplify(problem) + self.assertEqual(result.top_measurement, "111") + + @data("ideal", "shots") + def test_growth_rate(self, use_sampler): + """Test running the algorithm on a growth rate""" + grover = self._prepare_grover(use_sampler, growth_rate=8 / 7) + problem = AmplificationProblem(Statevector.from_label("111"), is_good_state=["111"]) + result = grover.amplify(problem) + self.assertEqual(result.top_measurement, "111") + + @data("ideal", "shots") + def test_max_num_iterations(self, use_sampler): + """Test the iteration stops when the maximum number of iterations is reached.""" + + def zero(): + while True: + yield 0 + + grover = self._prepare_grover(use_sampler, iterations=zero()) + n = 5 + problem = AmplificationProblem(Statevector.from_label("1" * n), is_good_state=["1" * n]) + result = grover.amplify(problem) + self.assertEqual(len(result.iterations), 2**n) + + @data("ideal", "shots") + def test_max_power(self, use_sampler): + """Test the iteration stops when the maximum power is reached.""" + lam = 10.0 + grover = self._prepare_grover(use_sampler, growth_rate=lam) + problem = AmplificationProblem(Statevector.from_label("111"), is_good_state=["111"]) + result = grover.amplify(problem) + self.assertEqual(len(result.iterations), 0) + + @data("ideal", "shots") + def test_run_circuit_oracle(self, use_sampler): + """Test execution with a quantum circuit oracle""" + oracle = QuantumCircuit(2) + oracle.cz(0, 1) + problem = AmplificationProblem(oracle, is_good_state=["11"]) + grover = self._prepare_grover(use_sampler) + result = grover.amplify(problem) + self.assertIn(result.top_measurement, ["11"]) + + @data("ideal", "shots") + def test_run_state_vector_oracle(self, use_sampler): + """Test execution with a state vector oracle""" + mark_state = Statevector.from_label("11") + problem = AmplificationProblem(mark_state, is_good_state=["11"]) + grover = self._prepare_grover(use_sampler) + result = grover.amplify(problem) + self.assertIn(result.top_measurement, ["11"]) + + @data("ideal", "shots") + def test_run_custom_grover_operator(self, use_sampler): + """Test execution with a grover operator oracle""" + oracle = QuantumCircuit(2) + oracle.cz(0, 1) + grover_op = GroverOperator(oracle) + problem = AmplificationProblem( + oracle=oracle, grover_operator=grover_op, is_good_state=["11"] + ) + grover = self._prepare_grover(use_sampler) + result = grover.amplify(problem) + self.assertIn(result.top_measurement, ["11"]) + + def test_optimal_num_iterations(self): + """Test optimal_num_iterations""" + num_qubits = 7 + for num_solutions in range(1, 2**num_qubits): + amplitude = np.sqrt(num_solutions / 2**num_qubits) + expected = round(np.arccos(amplitude) / (2 * np.arcsin(amplitude))) + actual = Grover.optimal_num_iterations(num_solutions, num_qubits) + self.assertEqual(actual, expected) + + def test_construct_circuit(self): + """Test construct_circuit""" + oracle = QuantumCircuit(2) + oracle.cz(0, 1) + problem = AmplificationProblem(oracle, is_good_state=["11"]) + grover = Grover() + constructed = grover.construct_circuit(problem, 2, measurement=False) + + grover_op = GroverOperator(oracle) + expected = QuantumCircuit(2) + expected.h([0, 1]) + expected.compose(grover_op.power(2), inplace=True) + + self.assertTrue(Operator(constructed).equiv(Operator(expected))) + + @data("ideal", "shots") + def test_circuit_result(self, use_sampler): + """Test circuit_result""" + oracle = QuantumCircuit(2) + oracle.cz(0, 1) + # is_good_state=['00'] is intentionally selected to obtain a list of results + problem = AmplificationProblem(oracle, is_good_state=["00"]) + grover = self._prepare_grover(use_sampler, iterations=[1, 2, 3, 4]) + + result = grover.amplify(problem) + + for i, dist in enumerate(result.circuit_results): + keys, values = zip(*sorted(dist.items())) + if i in (0, 3): + self.assertTupleEqual(keys, ("11",)) + np.testing.assert_allclose(values, [1], atol=0.2) + else: + self.assertTupleEqual(keys, ("00", "01", "10", "11")) + np.testing.assert_allclose(values, [0.25, 0.25, 0.25, 0.25], atol=0.2) + + @data("ideal", "shots") + def test_max_probability(self, use_sampler): + """Test max_probability""" + oracle = QuantumCircuit(2) + oracle.cz(0, 1) + problem = AmplificationProblem(oracle, is_good_state=["11"]) + grover = self._prepare_grover(use_sampler) + result = grover.amplify(problem) + self.assertAlmostEqual(result.max_probability, 1.0) + + @unittest.skipUnless(HAS_TWEEDLEDUM, "tweedledum required for this test") + @data("ideal", "shots") + def test_oracle_evaluation(self, use_sampler): + """Test oracle_evaluation for PhaseOracle""" + oracle = PhaseOracle("x1 & x2 & (not x3)") + problem = AmplificationProblem(oracle, is_good_state=oracle.evaluate_bitstring) + grover = self._prepare_grover(use_sampler) + result = grover.amplify(problem) + self.assertTrue(result.oracle_evaluation) + self.assertEqual("011", result.top_measurement) + + def test_sampler_setter(self): + """Test sampler setter""" + grover = Grover() + grover.sampler = self._sampler + self.assertEqual(grover.sampler, self._sampler) + + def _prepare_grover( + self, use_sampler, iterations=None, growth_rate=None, sample_from_iterations=False + ): + """Prepare Grover instance for test""" + if use_sampler == "ideal": + grover = Grover( + sampler=self._sampler, + iterations=iterations, + growth_rate=growth_rate, + sample_from_iterations=sample_from_iterations, + ) + elif use_sampler == "shots": + grover = Grover( + sampler=self._sampler_with_shots, + iterations=iterations, + growth_rate=growth_rate, + sample_from_iterations=sample_from_iterations, + ) + else: + raise RuntimeError("Unexpected `use_sampler` value {use_sampler}") + return grover + + +if __name__ == "__main__": + unittest.main() diff --git a/test/converters/test_converters.py b/test/converters/test_converters.py index f993fd9cb..1d9a3c6e0 100644 --- a/test/converters/test_converters.py +++ b/test/converters/test_converters.py @@ -1,6 +1,6 @@ # This code is part of a Qiskit project. # -# (C) Copyright IBM 2020, 2023. +# (C) Copyright IBM 2020, 2025. # # 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 @@ -18,7 +18,6 @@ import numpy as np from docplex.mp.model import Model from qiskit.quantum_info import SparsePauliOp -from qiskit_algorithms import NumPyMinimumEigensolver import qiskit_optimization.optionals as _optionals from qiskit_optimization import QiskitOptimizationError, QuadraticProgram @@ -30,6 +29,7 @@ LinearEqualityToPenalty, MaximizeToMinimize, ) +from qiskit_optimization.minimum_eigensolvers import NumPyMinimumEigensolver from qiskit_optimization.problems import Constraint, Variable from qiskit_optimization.translators import from_docplex_mp diff --git a/test/minimum_eigensolvers/test_vqe.py b/test/minimum_eigensolvers/test_vqe.py new file mode 100644 index 000000000..5fab95854 --- /dev/null +++ b/test/minimum_eigensolvers/test_vqe.py @@ -0,0 +1,347 @@ +# This code is part of a Qiskit project. +# +# (C) Copyright IBM 2022, 2025. +# +# 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 unittest +from functools import partial +from test import QiskitAlgorithmsTestCase + +import numpy as np +from ddt import data, ddt +from qiskit import QuantumCircuit +from qiskit.circuit.library import RealAmplitudes, TwoLocal +from qiskit.primitives import Estimator +from qiskit.quantum_info import Operator, Pauli, SparsePauliOp +from scipy.optimize import minimize as scipy_minimize + +from qiskit_optimization import AlgorithmError +from qiskit_optimization.minimum_eigensolvers import VQE +from qiskit_optimization.optimizers import ( + COBYLA, + NELDER_MEAD, + SPSA, + OptimizerResult, +) +from qiskit_optimization.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() + 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 + + +@ddt +class TestVQE(QiskitAlgorithmsTestCase): + """Test VQE""" + + def setUp(self): + super().setUp() + self.seed = 50 + algorithm_globals.random_seed = self.seed + 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 + + self.ryrz_wavefunction = TwoLocal(rotation_blocks=["ry", "rz"], entanglement_blocks="cz") + self.ry_wavefunction = TwoLocal(rotation_blocks="ry", entanglement_blocks="cz") + + @data(COBYLA()) + def test_using_ref_estimator(self, optimizer): + """Test VQE using reference Estimator.""" + vqe = VQE(Estimator(), self.ryrz_wavefunction, optimizer) + + result = vqe.compute_minimum_eigenvalue(operator=self.h2_op) + + with self.subTest(msg="test eigenvalue"): + 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) + + 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) + + 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) + + with self.subTest(msg="assert return ansatz is set"): + estimator = Estimator() + job = estimator.run(result.optimal_circuit, self.h2_op, result.optimal_point) + np.testing.assert_array_almost_equal(job.result().values, result.eigenvalue, 6) + + 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( + Estimator(), + ansatz, + COBYLA(), + 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(Estimator(), ansatz, COBYLA()) + 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(Estimator(), ansatz, COBYLA()) + + 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(Estimator(), ansatz, COBYLA()) + with self.assertRaises(AlgorithmError): + vqe.compute_minimum_eigenvalue(operator=self.h2_op) + + def test_max_evals_grouped(self): + """Test with COBYLA with max_evals_grouped.""" + optimizer = COBYLA(maxiter=200, max_evals_grouped=5) + vqe = VQE( + Estimator(), + self.ryrz_wavefunction, + optimizer, + ) + result = vqe.compute_minimum_eigenvalue(operator=self.h2_op) + self.assertAlmostEqual(result.eigenvalue.real, self.h2_energy, places=5) + + def test_callback(self): + """Test the callback on VQE.""" + history = {"eval_count": [], "parameters": [], "mean": [], "metadata": []} + + def store_intermediate_result(eval_count, parameters, mean, metadata): + history["eval_count"].append(eval_count) + history["parameters"].append(parameters) + history["mean"].append(mean) + history["metadata"].append(metadata) + + optimizer = COBYLA(maxiter=3) + wavefunction = self.ry_wavefunction + + estimator = Estimator() + + 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(metadata, dict) for metadata in history["metadata"])) + 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") + vqe = VQE(Estimator(), ansatz, COBYLA(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) + + operator = Operator(np.array([[1, 0, 0, 0], [0, -1, 0, 0], [0, 0, 2, 0], [0, 0, 0, 3]])) + operator = SparsePauliOp.from_operator(operator) + + 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_reuse(self): + """Test running same VQE twice to re-use optimizer, then switch optimizer""" + vqe = VQE( + Estimator(), + self.ryrz_wavefunction, + COBYLA(), + ) + + 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 = NELDER_MEAD() + run_check() + + def test_default_batch_evaluation_on_spsa(self): + """Test the default batching works.""" + ansatz = TwoLocal(2, rotation_blocks=["ry", "rz"], entanglement_blocks="cz") + + wrapped_estimator = Estimator() + inner_estimator = Estimator() + + callcount = {"estimator": 0} + + def wrapped_estimator_run(*args, **kwargs): + kwargs["callcount"]["estimator"] += 1 + return inner_estimator.run(*args, **kwargs) + + wrapped_estimator.run = partial(wrapped_estimator_run, callcount=callcount) + + spsa = SPSA(maxiter=5) + + vqe = VQE(wrapped_estimator, ansatz, spsa) + _ = vqe.compute_minimum_eigenvalue(Pauli("ZZ")) + + # 1 calibration + 5 loss + 1 return loss + expected_estimator_runs = 1 + 5 + 1 + + with self.subTest(msg="check callcount"): + self.assertEqual(callcount["estimator"], expected_estimator_runs) + + with self.subTest(msg="check reset to original max evals grouped"): + self.assertIsNone(spsa._max_evals_grouped) + + def test_optimizer_scipy_callable(self): + """Test passing a SciPy optimizer directly as callable.""" + vqe = VQE( + 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) + + def test_optimizer_callable(self): + """Test passing a optimizer directly as callable.""" + ansatz = RealAmplitudes(1, reps=1) + 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))) + + def test_aux_operators_list(self): + """Test list-based aux_operators.""" + vqe = VQE(Estimator(), self.ry_wavefunction, COBYLA(maxiter=300)) + + 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_operators_evaluated, list) + self.assertEqual(len(result.aux_operators_evaluated), 0) + + with self.subTest("Test with two auxiliary operators."): + aux_op1 = SparsePauliOp.from_list([("II", 2.0)]) + aux_op2 = SparsePauliOp.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=5) + self.assertEqual(len(result.aux_operators_evaluated), 2) + # expectation values + 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_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_operators_evaluated), 3) + # expectation values + 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_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""" + vqe = VQE(Estimator(), self.ry_wavefunction, COBYLA(maxiter=300)) + + 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_operators_evaluated, dict) + self.assertEqual(len(result.aux_operators_evaluated), 0) + + with self.subTest("Test with two auxiliary operators."): + aux_op1 = SparsePauliOp.from_list([("II", 2.0)]) + aux_op2 = SparsePauliOp.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_operators_evaluated), 2) + + # expectation values + 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_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_operators_evaluated), 3) + # expectation values + 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_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__": + unittest.main()