diff --git a/.pylintdict b/.pylintdict index 1dae4034b..1872e7329 100644 --- a/.pylintdict +++ b/.pylintdict @@ -10,6 +10,7 @@ ansatzes apidocs apl applegate +arg args arxiv atol @@ -26,18 +27,22 @@ bixby bool boolean boyd +brassard bravyi +byteorder callables -catol cartan +catol chu +chuang chvátal +clbits cobyla coeff coeffs combinatorial -conv const +conv cplex cplexoptimizer crs @@ -73,17 +78,17 @@ et eval evals exponentiated +f'spsa failsafe farhi fmin formatter frac +fred func functools -fred fval fx -f'spsa gambella geq getter @@ -99,6 +104,7 @@ gurobi gurobioptimizer gurobipy gutmann +hadamard hadfield hamilton hamiltonian @@ -124,8 +130,10 @@ july kandala karimi kirkpatrick +kwarg kwargs lagrangian +langle len leq lhs @@ -142,6 +150,7 @@ macos makefile marecek masahito +mathcal matplotlib maxcut maxfev @@ -151,8 +160,9 @@ mdl milp minimizer minimumeigenoptimizer -modelspace mmp +modelspace +mosca mpm multiset mypy @@ -160,13 +170,13 @@ nannicini natively ndarray ndarrays -nones -noop nelder networkx neven nfev nft +nones +noop nosignatures np num @@ -179,6 +189,7 @@ optimality optimizationresult optimizationresultstatus optimizers +otimes packagebut panchenko param @@ -189,6 +200,7 @@ passmanager pauli paulis peleato +peruzzo pmm polyfit pooya @@ -196,14 +208,14 @@ pos ppp pre preconditioner -preprint prepend +preprint presolver prettyprint princeton probabilistically -py pxd +py qaoa qasm qiskit @@ -221,6 +233,7 @@ quantuminstance qubit qubits qubo +rangle readme repr representable @@ -262,10 +275,11 @@ subclasses subcollection subgraph submodules -subspaces -sys subproblem +subspaces summands +sys +tapp tavernelli terra th @@ -285,21 +299,22 @@ troyer tsplib undirected upperbound +utils variational vartype +vec vqe vqeresult -utils -writelines -xatol -xixj -xopt wavefunction wecker whitespace wiesner williamson woerner +writelines +xatol +xixj +xopt xs ys zemlin diff --git a/README.md b/README.md index 02f25679b..d77acf659 100644 --- a/README.md +++ b/README.md @@ -16,8 +16,7 @@ The Optimization module enables easy, efficient modeling of optimization problem 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 QAOA, to Grover Adaptive Search using the -GroverOptimizer, leveraging fundamental algorithms provided by -[Qiskit Algorithms](https://qiskit-community.github.io/qiskit-algorithms/). Furthermore, the modular design +GroverOptimizer, leveraging fundamental algorithms. Furthermore, the modular design of the optimization module allows it to be easily extended and facilitates rapid development and testing of new algorithms. Compatible classical optimizers are also provided for testing, validation, and benchmarking. diff --git a/qiskit_optimization/__init__.py b/qiskit_optimization/__init__.py index 18c947967..0e743694b 100644 --- a/qiskit_optimization/__init__.py +++ b/qiskit_optimization/__init__.py @@ -26,13 +26,10 @@ 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 -`_ -provided by -`Qiskit Algorithms `_. +(:class:`~qiskit_optimization.algorithms.GroverOptimizer`), leveraging fundamental minimum +eigensolvers (:class:`~qiskit_optimization.minimum_eigensolvers.MinimumEigensolver`). Furthermore, the modular design of the optimization module allows it to be easily extended and facilitates rapid development and testing of new algorithms. Compatible classical optimizers are also provided for testing, @@ -82,10 +79,15 @@ converters problems translators + # modules copied from qiskit_algorithms + eigensolvers + minimum_eigensolvers + optimizers + utils """ -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 646ea95ef..041592f1c 100644 --- a/qiskit_optimization/algorithms/admm_optimizer.py +++ b/qiskit_optimization/algorithms/admm_optimizer.py @@ -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..8dcc1a729 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__) @@ -191,7 +190,7 @@ def solve(self, problem: QuadraticProgram) -> OptimizationResult: while not improvement_found: # Determine the number of rotations. loops_with_no_improvement += 1 - rotation_count = algorithm_globals.random.integers(0, m) + rotation_count = int(algorithm_globals.random.integers(0, m)) rotations += rotation_count # Apply Grover's Algorithm to find values below the threshold. # TODO: Utilize Grover's incremental feature - requires changes to Grover. 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..2ced66a8a --- /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 = int(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..c23abe507 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 @@ -140,7 +140,7 @@ def create_random_instance(n: int, low: int = 0, high: int = 100, seed: int = No """ if seed: algorithm_globals.random_seed = seed - coord = algorithm_globals.random.uniform(low, high, (n, 2)) + coord = algorithm_globals.random.uniform(low, high, (n, 2)).tolist() pos = {i: (coord_[0], coord_[1]) for i, coord_ in enumerate(coord)} graph = nx.random_geometric_graph(n, np.hypot(high - low, high - low) + 1, pos=pos) for w, v in graph.edges: 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..5092e5368 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 30086dfc0..da11895b1 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,4 @@ qiskit>=0.44,<2 -qiskit-algorithms>=0.2.0 scipy>=1.9.0 numpy>=1.17 docplex>=2.21.207,!=2.24.231 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()