diff --git a/docs/apidoc/algorithms.rst b/docs/apidoc/algorithms.rst deleted file mode 100644 index 25d1bd4a412b..000000000000 --- a/docs/apidoc/algorithms.rst +++ /dev/null @@ -1,6 +0,0 @@ -.. _qiskit-algorithms: - -.. automodule:: qiskit.algorithms - :no-members: - :no-inherited-members: - :no-special-members: diff --git a/docs/apidoc/index.rst b/docs/apidoc/index.rst index 60c30286fa8c..e957cabb12f3 100644 --- a/docs/apidoc/index.rst +++ b/docs/apidoc/index.rst @@ -57,5 +57,4 @@ Deprecated Modules .. toctree:: :maxdepth: 1 - algorithms opflow diff --git a/docs/index.rst b/docs/index.rst index ef0b28d777b0..b82e864ccfca 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -3,7 +3,7 @@ Qiskit |version| documentation ############################## Qiskit is open-source software for working with quantum computers -at the level of circuits, pulses, and algorithms. +at the level of circuits and pulses. The central goal of Qiskit is to build a software stack that makes it easy for anyone to use quantum computers, regardless of their skill level or diff --git a/qiskit/algorithms/__init__.py b/qiskit/algorithms/__init__.py deleted file mode 100644 index 18767f1fdd01..000000000000 --- a/qiskit/algorithms/__init__.py +++ /dev/null @@ -1,430 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2018, 2022. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. - -""" -===================================== -Algorithms (:mod:`qiskit.algorithms`) -===================================== - -.. deprecated:: 0.25.0 - - The :mod:`qiskit.algorithms` module has been migrated to an independent package: - https://github.com/qiskit-community/qiskit-algorithms. - The current import path is deprecated and will be removed no earlier - than 3 months after the release date. If your code uses primitives, you can run - ``pip install qiskit_algorithms`` and import ``from qiskit_algorithms`` instead. - If you use opflow/quantum instance-based algorithms, please update your code to - use primitives following: https://qisk.it/algo_migration before migrating to - the new package. - -It contains a collection of quantum algorithms, for use with quantum computers, to -carry out research and investigate how to solve problems in different domains on -near-term quantum devices with short depth circuits. - -Algorithms configuration includes the use of :mod:`~qiskit.algorithms.optimizers` which -were designed to be swappable sub-parts of an algorithm. Any component and may be exchanged for -a different implementation of the same component type in order to potentially alter the behavior -and outcome of the algorithm. - -Quantum algorithms are run via a :class:`~qiskit.algorithms.QuantumInstance` -which must be set with the -desired backend where the algorithm's circuits will be executed and be configured with a number of -compile and runtime parameters controlling circuit compilation and execution. It ultimately uses -`Terra `__ for the actual compilation and execution of the quantum -circuits created by the algorithm and its components. - -.. currentmodule:: qiskit.algorithms - -Algorithms -========== - -It contains a variety of quantum algorithms and these have been grouped by logical function such -as minimum eigensolvers and amplitude amplifiers. - - -Amplitude Amplifiers --------------------- - -.. autosummary:: - :toctree: ../stubs/ - :nosignatures: - - AmplificationProblem - AmplitudeAmplifier - Grover - GroverResult - - -Amplitude Estimators --------------------- - -.. autosummary:: - :toctree: ../stubs/ - :nosignatures: - - AmplitudeEstimator - AmplitudeEstimatorResult - AmplitudeEstimation - AmplitudeEstimationResult - EstimationProblem - FasterAmplitudeEstimation - FasterAmplitudeEstimationResult - IterativeAmplitudeEstimation - IterativeAmplitudeEstimationResult - MaximumLikelihoodAmplitudeEstimation - MaximumLikelihoodAmplitudeEstimationResult - - -Eigensolvers ------------- - -Algorithms to find eigenvalues of an operator. For chemistry these can be used to find excited -states of a molecule, and ``qiskit-nature`` has some algorithms that leverage chemistry specific -knowledge to do this in that application domain. - -Primitive-based Eigensolvers -++++++++++++++++++++++++++++ - -These algorithms are based on the Qiskit Primitives, a new execution paradigm that replaces the use -of :class:`.QuantumInstance` in algorithms. To ensure continued support and development, we recommend -using the primitive-based Eigensolvers in place of the legacy :class:`.QuantumInstance`-based ones. - -.. autosummary:: - :toctree: ../stubs/ - - eigensolvers - - -Legacy Eigensolvers -+++++++++++++++++++ - -These algorithms, still based on the :class:`.QuantumInstance`, are superseded -by the primitive-based versions in the section above but are still supported for now. - -.. autosummary:: - :toctree: ../stubs/ - :nosignatures: - - Eigensolver - EigensolverResult - NumPyEigensolver - VQD - VQDResult - - -Time Evolvers -------------- - -Algorithms to evolve quantum states in time. Both real and imaginary time evolution is possible -with algorithms that support them. For machine learning, Quantum Imaginary Time Evolution might be -used to train Quantum Boltzmann Machine Neural Networks for example. - -Primitive-based Time Evolvers -+++++++++++++++++++++++++++++ - -These algorithms are based on the Qiskit Primitives, a new execution paradigm that replaces the use -of :class:`.QuantumInstance` in algorithms. To ensure continued support and development, we recommend -using the primitive-based Time Evolvers in place of the legacy :class:`.QuantumInstance`-based ones. - -.. autosummary:: - :toctree: ../stubs/ - :nosignatures: - - RealTimeEvolver - ImaginaryTimeEvolver - TimeEvolutionResult - TimeEvolutionProblem - PVQD - PVQDResult - SciPyImaginaryEvolver - SciPyRealEvolver - VarQITE - VarQRTE - -Legacy Time Evolvers -++++++++++++++++++++ - -These algorithms, still based on the :class:`.QuantumInstance`, are superseded -by the primitive-based versions in the section above but are still supported for now. - -.. autosummary:: - :toctree: ../stubs/ - :nosignatures: - - RealEvolver - ImaginaryEvolver - TrotterQRTE - EvolutionResult - EvolutionProblem - - -Variational Quantum Time Evolution -++++++++++++++++++++++++++++++++++ - -Classes used by variational quantum time evolution algorithms - :class:`.VarQITE` and -:class:`.VarQRTE`. - -.. autosummary:: - :toctree: ../stubs/ - - time_evolvers.variational - - -Trotterization-based Quantum Real Time Evolution -++++++++++++++++++++++++++++++++++++++++++++++++ - -Package for primitives-enabled Trotterization-based quantum time evolution -algorithm - :class:`~.time_evolvers.TrotterQRTE`. - -.. autosummary:: - :toctree: ../stubs/ - - time_evolvers.trotterization - - -Gradients ----------- - -Algorithms to calculate the gradient of a quantum circuit. - -.. autosummary:: - :toctree: ../stubs/ - - gradients - - -Minimum Eigensolvers ---------------------- - -Algorithms that can find the minimum eigenvalue of an operator. - -Primitive-based Minimum Eigensolvers -++++++++++++++++++++++++++++++++++++ - -These algorithms are based on the Qiskit Primitives, a new execution paradigm that replaces the use -of :class:`.QuantumInstance` in algorithms. To ensure continued support and development, we recommend -using the primitive-based Minimum Eigensolvers in place of the legacy :class:`.QuantumInstance`-based -ones. - -.. autosummary:: - :toctree: ../stubs/ - - minimum_eigensolvers - - -Legacy Minimum Eigensolvers -+++++++++++++++++++++++++++ - -These algorithms, still based on the :class:`.QuantumInstance`, are superseded -by the primitive-based versions in the section above but are still supported for now. - -.. autosummary:: - :toctree: ../stubs/ - :nosignatures: - - MinimumEigensolver - MinimumEigensolverResult - NumPyMinimumEigensolver - QAOA - VQE - - -Optimizers ----------- - -Classical optimizers for use by quantum variational algorithms. - -.. autosummary:: - :toctree: ../stubs/ - - optimizers - - -Phase Estimators ----------------- - -Algorithms that estimate the phases of eigenstates of a unitary. - -.. autosummary:: - :toctree: ../stubs/ - :nosignatures: - - HamiltonianPhaseEstimation - HamiltonianPhaseEstimationResult - PhaseEstimationScale - PhaseEstimation - PhaseEstimationResult - IterativePhaseEstimation - - -State Fidelities ----------------- - -Algorithms that compute the fidelity of pairs of quantum states. - -.. autosummary:: - :toctree: ../stubs/ - - state_fidelities - - -Exceptions ----------- - -.. autoexception:: AlgorithmError - -Utility classes ---------------- - -Utility classes used by algorithms (mainly for type-hinting purposes). - -.. autosummary:: - :toctree: ../stubs/ - - AlgorithmJob - -Utility functions ------------------ - -Utility functions used by algorithms. - -.. autofunction:: eval_observables -.. autofunction:: estimate_observables - -""" -import warnings - -from .algorithm_job import AlgorithmJob -from .algorithm_result import AlgorithmResult -from .evolvers import EvolutionResult, EvolutionProblem -from .evolvers.real_evolver import RealEvolver -from .evolvers.imaginary_evolver import ImaginaryEvolver -from .variational_algorithm import VariationalAlgorithm, VariationalResult -from .amplitude_amplifiers import Grover, GroverResult, AmplificationProblem, AmplitudeAmplifier -from .amplitude_estimators import ( - AmplitudeEstimator, - AmplitudeEstimatorResult, - AmplitudeEstimation, - AmplitudeEstimationResult, - FasterAmplitudeEstimation, - FasterAmplitudeEstimationResult, - IterativeAmplitudeEstimation, - IterativeAmplitudeEstimationResult, - MaximumLikelihoodAmplitudeEstimation, - MaximumLikelihoodAmplitudeEstimationResult, - EstimationProblem, -) -from .eigen_solvers import NumPyEigensolver, Eigensolver, EigensolverResult, VQD, VQDResult -from .minimum_eigen_solvers import ( - VQE, - VQEResult, - QAOA, - NumPyMinimumEigensolver, - MinimumEigensolver, - MinimumEigensolverResult, -) -from .phase_estimators import ( - HamiltonianPhaseEstimation, - HamiltonianPhaseEstimationResult, - PhaseEstimationScale, - PhaseEstimation, - PhaseEstimationResult, - IterativePhaseEstimation, -) -from .exceptions import AlgorithmError -from .aux_ops_evaluator import eval_observables -from .observables_evaluator import estimate_observables -from .evolvers.trotterization import TrotterQRTE - -from .time_evolvers import ( - ImaginaryTimeEvolver, - RealTimeEvolver, - TimeEvolutionProblem, - TimeEvolutionResult, - PVQD, - PVQDResult, - SciPyImaginaryEvolver, - SciPyRealEvolver, - VarQITE, - VarQRTE, - VarQTE, - VarQTEResult, -) - -__all__ = [ - "AlgorithmJob", - "AlgorithmResult", - "VariationalAlgorithm", - "VariationalResult", - "AmplitudeAmplifier", - "AmplificationProblem", - "Grover", - "GroverResult", - "AmplitudeEstimator", - "AmplitudeEstimatorResult", - "AmplitudeEstimation", - "AmplitudeEstimationResult", - "FasterAmplitudeEstimation", - "FasterAmplitudeEstimationResult", - "IterativeAmplitudeEstimation", - "IterativeAmplitudeEstimationResult", - "MaximumLikelihoodAmplitudeEstimation", - "MaximumLikelihoodAmplitudeEstimationResult", - "EstimationProblem", - "NumPyEigensolver", - "RealEvolver", - "ImaginaryEvolver", - "RealTimeEvolver", - "ImaginaryTimeEvolver", - "TrotterQRTE", - "EvolutionResult", - "EvolutionProblem", - "TimeEvolutionResult", - "TimeEvolutionProblem", - "Eigensolver", - "EigensolverResult", - "VQE", - "VQEResult", - "QAOA", - "NumPyMinimumEigensolver", - "MinimumEigensolver", - "MinimumEigensolverResult", - "HamiltonianPhaseEstimation", - "HamiltonianPhaseEstimationResult", - "VQD", - "VQDResult", - "PhaseEstimationScale", - "PhaseEstimation", - "PhaseEstimationResult", - "PVQD", - "PVQDResult", - "SciPyRealEvolver", - "SciPyImaginaryEvolver", - "IterativePhaseEstimation", - "AlgorithmError", - "eval_observables", - "estimate_observables", - "VarQITE", - "VarQRTE", - "VarQTE", - "VarQTEResult", -] - -warnings.warn( - "``qiskit.algorithms`` has been migrated to an independent package: " - "https://github.com/qiskit-community/qiskit-algorithms. " - "The ``qiskit.algorithms`` import path is deprecated as of qiskit-terra 0.25.0 and " - "will be removed no earlier than 3 months after the release date. " - "Please run ``pip install qiskit_algorithms`` and use ``import qiskit_algorithms`` instead.", - category=DeprecationWarning, - stacklevel=2, -) diff --git a/qiskit/algorithms/algorithm_job.py b/qiskit/algorithms/algorithm_job.py deleted file mode 100644 index 16db4df93dfc..000000000000 --- a/qiskit/algorithms/algorithm_job.py +++ /dev/null @@ -1,24 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2022. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. - -""" -AlgorithmJob class -""" -from qiskit.primitives.primitive_job import PrimitiveJob - - -class AlgorithmJob(PrimitiveJob): - """ - This empty class is introduced for typing purposes. - """ - - pass diff --git a/qiskit/algorithms/algorithm_result.py b/qiskit/algorithms/algorithm_result.py deleted file mode 100644 index 0804303a4ef6..000000000000 --- a/qiskit/algorithms/algorithm_result.py +++ /dev/null @@ -1,65 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2020, 2021. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. - -""" -This module implements the abstract base class for algorithm results. -""" - -from abc import ABC -import inspect -import pprint - - -class AlgorithmResult(ABC): - """Abstract Base Class for algorithm results.""" - - def __str__(self) -> str: - result = {} - for name, value in inspect.getmembers(self): - if ( - not name.startswith("_") - and not inspect.ismethod(value) - and not inspect.isfunction(value) - and hasattr(self, name) - ): - - result[name] = value - - return pprint.pformat(result, indent=4) - - def combine(self, result: "AlgorithmResult") -> None: - """ - Any property from the argument that exists in the receiver is - updated. - Args: - result: Argument result with properties to be set. - Raises: - TypeError: Argument is None - """ - if result is None: - raise TypeError("Argument result expected.") - if result == self: - return - - # find any result public property that exists in the receiver - for name, value in inspect.getmembers(result): - if ( - not name.startswith("_") - and not inspect.ismethod(value) - and not inspect.isfunction(value) - and hasattr(self, name) - ): - try: - setattr(self, name, value) - except AttributeError: - # some attributes may be read only - pass diff --git a/qiskit/algorithms/amplitude_amplifiers/__init__.py b/qiskit/algorithms/amplitude_amplifiers/__init__.py deleted file mode 100644 index bc45f18106bd..000000000000 --- a/qiskit/algorithms/amplitude_amplifiers/__init__.py +++ /dev/null @@ -1,25 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2020. -# -# 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 .amplitude_amplifier import AmplitudeAmplifier, AmplitudeAmplifierResult -from .amplification_problem import AmplificationProblem -from .grover import Grover, GroverResult - -__all__ = [ - "AmplitudeAmplifier", - "AmplitudeAmplifierResult", - "AmplificationProblem", - "Grover", - "GroverResult", -] diff --git a/qiskit/algorithms/amplitude_amplifiers/amplification_problem.py b/qiskit/algorithms/amplitude_amplifiers/amplification_problem.py deleted file mode 100644 index 67b20751c417..000000000000 --- a/qiskit/algorithms/amplitude_amplifiers/amplification_problem.py +++ /dev/null @@ -1,213 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2021. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. - -"""The Amplification problem class.""" -from __future__ import annotations - -from collections.abc import Callable -from typing import Any - -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. - """ - - 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 self._is_good_state - else: - return lambda bitstr: all( - bitstr[good_index] == "1" for good_index in self._is_good_state - ) - - return lambda bitstr: bitstr in 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/algorithms/amplitude_amplifiers/amplitude_amplifier.py b/qiskit/algorithms/amplitude_amplifiers/amplitude_amplifier.py deleted file mode 100644 index 33ef90cb624e..000000000000 --- a/qiskit/algorithms/amplitude_amplifiers/amplitude_amplifier.py +++ /dev/null @@ -1,127 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2021. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. - -"""The interface for amplification algorithms and results.""" -from __future__ import annotations - -from abc import ABC, abstractmethod -from typing import Any - -import numpy as np - -from .amplification_problem import AmplificationProblem -from ..algorithm_result import AlgorithmResult - - -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[np.ndarray] | 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[np.ndarray] | list[dict[str, int]] | None: - """Return the circuit results. Can be a statevector or counts dictionary.""" - return self._circuit_results - - @circuit_results.setter - def circuit_results(self, value: list[np.ndarray] | 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/algorithms/amplitude_amplifiers/grover.py b/qiskit/algorithms/amplitude_amplifiers/grover.py deleted file mode 100644 index 70cf4e1606fe..000000000000 --- a/qiskit/algorithms/amplitude_amplifiers/grover.py +++ /dev/null @@ -1,452 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2018, 2022. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. - -"""Grover's search algorithm.""" -from __future__ import annotations - -import itertools -import operator -import warnings -from collections.abc import Iterator, Generator -from typing import Any - -import numpy as np - -from qiskit import ClassicalRegister, QuantumCircuit -from qiskit.algorithms.exceptions import AlgorithmError -from qiskit.primitives import BaseSampler -from qiskit.providers import Backend -from qiskit.quantum_info import partial_trace, Statevector -from qiskit.utils import QuantumInstance, algorithm_globals -from qiskit.utils.deprecation import deprecate_arg, deprecate_func - -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 `_. - """ - - @deprecate_arg( - "quantum_instance", - additional_msg=( - "Instead, use the ``sampler`` argument. " - "See https://qisk.it/algo_migration for a migration guide." - ), - since="0.24.0", - package_name="qiskit-terra", - ) - def __init__( - self, - iterations: list[int] | Iterator[int] | int | None = None, - growth_rate: float | None = None, - sample_from_iterations: bool = False, - quantum_instance: QuantumInstance | Backend | None = None, - 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. - quantum_instance: Deprecated: A Quantum Instance or Backend to run the circuits. - 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 - - if quantum_instance is not None and sampler is not None: - raise ValueError("Only one of quantum_instance or sampler can be passed, not both!") - - # check positionally passing the sampler in the place of quantum_instance - # which will be removed in future - if isinstance(quantum_instance, BaseSampler): - sampler = quantum_instance - quantum_instance = None - - self._quantum_instance: QuantumInstance | None = None - if quantum_instance is not None: - with warnings.catch_warnings(): - warnings.simplefilter("ignore", DeprecationWarning) - self.quantum_instance = quantum_instance - - self._sampler = sampler - - self._sample_from_iterations = sample_from_iterations - self._iterations_arg = iterations - - @property - @deprecate_func( - since="0.24.0", - package_name="qiskit-terra", - is_property=True, - additional_msg="See https://qisk.it/algo_migration for a migration guide.", - ) - def quantum_instance(self) -> QuantumInstance | None: - r"""Deprecated. Get the quantum instance. - - Returns: - The quantum instance used to run this algorithm. - """ - return self._quantum_instance - - @quantum_instance.setter - @deprecate_func( - since="0.24.0", - package_name="qiskit-terra", - is_property=True, - additional_msg="See https://qisk.it/algo_migration for a migration guide.", - ) - def quantum_instance(self, quantum_instance: QuantumInstance | Backend) -> None: - r"""Deprecated. Set quantum instance. - - Args: - quantum_instance: The quantum instance used to run this algorithm. - """ - if isinstance(quantum_instance, Backend): - quantum_instance = QuantumInstance(quantum_instance) - self._quantum_instance = quantum_instance - - @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 a quantum instance or sampler is not set. - AlgorithmError: If a 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 and self._quantum_instance is None: - raise ValueError("A quantum instance or sampler must be provided.") - - if self._quantum_instance is not None and self._sampler is not None: - raise ValueError("Only one of quantum_instance or sampler can be passed, not both!") - - 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 - shots = 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: - with warnings.catch_warnings(): - warnings.filterwarnings("ignore", category=DeprecationWarning) - power = algorithm_globals.random.integers(power) - # Run a grover experiment for a given power of the Grover operator. - if self._sampler is not None: - qc = self.construct_circuit(amplification_problem, power, measurement=True) - job = self._sampler.run([qc]) - - try: - results = job.result() - except Exception as exc: - raise AlgorithmError("Sampler job failed.") from exc - - num_bits = len(amplification_problem.objective_qubits) - circuit_results: dict[str, Any] | Statevector | np.ndarray = { - np.binary_repr(k, num_bits): v for k, v in results.quasi_dists[0].items() - } - top_measurement, max_probability = max(circuit_results.items(), key=lambda x: x[1]) - - else: # use of else brach instead of elif as this seperates out the deprecated logic - if self._quantum_instance.is_statevector: - qc = self.construct_circuit(amplification_problem, power, measurement=False) - circuit_results = self._quantum_instance.execute(qc).get_statevector() - num_bits = len(amplification_problem.objective_qubits) - - # trace out work qubits - if qc.width() != num_bits: - indices = [ - i - for i in range(qc.num_qubits) - if i not in amplification_problem.objective_qubits - ] - rho = partial_trace(circuit_results, indices) - circuit_results = np.diag(rho.data) - - max_amplitude = max(circuit_results.max(), circuit_results.min(), key=abs) - max_amplitude_idx = np.where(circuit_results == max_amplitude)[0][0] - top_measurement = np.binary_repr(max_amplitude_idx, num_bits) - max_probability = np.abs(max_amplitude) ** 2 - shots = 1 - else: - qc = self.construct_circuit(amplification_problem, power, measurement=True) - circuit_results = self._quantum_instance.execute(qc).get_counts(qc) - top_measurement = max(circuit_results.items(), key=operator.itemgetter(1))[0] - shots = sum(circuit_results.values()) - max_probability = ( - max(circuit_results.items(), key=operator.itemgetter(1))[1] / shots - ) - - 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 - 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: - raise ValueError("Please pass ``power`` if the iterations are not an integer.") - power = self._iterations[0] - - 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/algorithms/amplitude_estimators/__init__.py b/qiskit/algorithms/amplitude_estimators/__init__.py deleted file mode 100644 index 764f8863857d..000000000000 --- a/qiskit/algorithms/amplitude_estimators/__init__.py +++ /dev/null @@ -1,34 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2020. -# -# 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 Amplitude Estimators package.""" - -from .amplitude_estimator import AmplitudeEstimator, AmplitudeEstimatorResult -from .ae import AmplitudeEstimation, AmplitudeEstimationResult -from .fae import FasterAmplitudeEstimation, FasterAmplitudeEstimationResult -from .iae import IterativeAmplitudeEstimation, IterativeAmplitudeEstimationResult -from .mlae import MaximumLikelihoodAmplitudeEstimation, MaximumLikelihoodAmplitudeEstimationResult -from .estimation_problem import EstimationProblem - -__all__ = [ - "AmplitudeEstimator", - "AmplitudeEstimatorResult", - "AmplitudeEstimation", - "AmplitudeEstimationResult", - "FasterAmplitudeEstimation", - "FasterAmplitudeEstimationResult", - "IterativeAmplitudeEstimation", - "IterativeAmplitudeEstimationResult", - "MaximumLikelihoodAmplitudeEstimation", - "MaximumLikelihoodAmplitudeEstimationResult", - "EstimationProblem", -] diff --git a/qiskit/algorithms/amplitude_estimators/ae.py b/qiskit/algorithms/amplitude_estimators/ae.py deleted file mode 100644 index 226d37245c1d..000000000000 --- a/qiskit/algorithms/amplitude_estimators/ae.py +++ /dev/null @@ -1,691 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2018, 2022. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. - -"""The Quantum Phase Estimation-based Amplitude Estimation algorithm.""" - -from __future__ import annotations -from collections import OrderedDict -import warnings -import numpy as np -from scipy.stats import chi2, norm -from scipy.optimize import bisect - -from qiskit import QuantumCircuit, ClassicalRegister -from qiskit.providers import Backend -from qiskit.primitives import BaseSampler -from qiskit.utils import QuantumInstance -from qiskit.utils.deprecation import deprecate_arg, deprecate_func -from .amplitude_estimator import AmplitudeEstimator, AmplitudeEstimatorResult -from .ae_utils import pdf_a, derivative_log_pdf_a, bisect_max -from .estimation_problem import EstimationProblem -from ..exceptions import AlgorithmError - - -class AmplitudeEstimation(AmplitudeEstimator): - r"""The Quantum Phase Estimation-based Amplitude Estimation algorithm. - - This class implements the original Quantum Amplitude Estimation (QAE) algorithm, introduced by - [1]. This canonical version uses quantum phase estimation along with a set of :math:`m` - additional evaluation qubits to find an estimate :math:`\tilde{a}`, that is restricted to the - grid - - .. math:: - - \tilde{a} \in \{\sin^2(\pi y / 2^m) : y = 0, ..., 2^{m-1}\} - - More evaluation qubits produce a finer sampling grid, therefore the accuracy of the algorithm - increases with :math:`m`. - - Using a maximum likelihood post processing, this grid constraint can be circumvented. - This improved estimator is implemented as well, see [2] Appendix A for more detail. - - .. note:: - - This class does not support the :attr:`.EstimationProblem.is_good_state` property, - as for phase estimation-based QAE, the oracle that identifes the good states - must be encoded in the Grover operator. To set custom oracles, the - :attr:`.EstimationProblem.grover_operator` attribute can be set directly. - - References: - [1]: Brassard, G., Hoyer, P., Mosca, M., & Tapp, A. (2000). - Quantum Amplitude Amplification and Estimation. - `arXiv:quant-ph/0005055 `_. - [2]: Grinko, D., Gacon, J., Zoufal, C., & Woerner, S. (2019). - Iterative Quantum Amplitude Estimation. - `arXiv:1912.05559 `_. - """ - - @deprecate_arg( - "quantum_instance", - additional_msg=( - "Instead, use the ``sampler`` argument. See https://qisk.it/algo_migration for a " - "migration guide." - ), - since="0.24.0", - package_name="qiskit-terra", - ) - def __init__( - self, - num_eval_qubits: int, - phase_estimation_circuit: QuantumCircuit | None = None, - iqft: QuantumCircuit | None = None, - quantum_instance: QuantumInstance | Backend | None = None, - sampler: BaseSampler | None = None, - ) -> None: - r""" - Args: - num_eval_qubits: The number of evaluation qubits. - phase_estimation_circuit: The phase estimation circuit used to run the algorithm. - Defaults to the standard phase estimation circuit from the circuit library, - `qiskit.circuit.library.PhaseEstimation` when None. - iqft: The inverse quantum Fourier transform component, defaults to using a standard - implementation from `qiskit.circuit.library.QFT` when None. - quantum_instance: Deprecated: The backend (or `QuantumInstance`) to execute - the circuits on. - sampler: A sampler primitive to evaluate the circuits. - - Raises: - ValueError: If the number of evaluation qubits is smaller than 1. - """ - if num_eval_qubits < 1: - raise ValueError("The number of evaluation qubits must at least be 1.") - - super().__init__() - - # set quantum instance - with warnings.catch_warnings(): - warnings.simplefilter("ignore") - self.quantum_instance = quantum_instance - - # get parameters - self._m = num_eval_qubits - self._M = 2**num_eval_qubits # pylint: disable=invalid-name - - self._iqft = iqft - self._pec = phase_estimation_circuit - self._sampler = sampler - - @property - def sampler(self) -> BaseSampler | None: - """Get the sampler primitive. - - Returns: - The sampler primitive to evaluate the circuits. - """ - return self._sampler - - @sampler.setter - def sampler(self, sampler: BaseSampler) -> None: - """Set sampler primitive. - - Args: - sampler: A sampler primitive to evaluate the circuits. - """ - self._sampler = sampler - - @property - @deprecate_func( - additional_msg="See https://qisk.it/algo_migration for a migration guide.", - since="0.24.0", - package_name="qiskit-terra", - is_property=True, - ) - def quantum_instance(self) -> QuantumInstance | None: - """Deprecated: Get the quantum instance. - - Returns: - The quantum instance used to run this algorithm. - """ - return self._quantum_instance - - @quantum_instance.setter - @deprecate_func( - additional_msg="See https://qisk.it/algo_migration for a migration guide.", - since="0.24.0", - package_name="qiskit-terra", - is_property=True, - ) - def quantum_instance(self, quantum_instance: QuantumInstance | Backend) -> None: - """Deprecated: Set quantum instance. - - Args: - quantum_instance: The quantum instance used to run this algorithm. - """ - if isinstance(quantum_instance, Backend): - quantum_instance = QuantumInstance(quantum_instance) - self._quantum_instance = quantum_instance - - def construct_circuit( - self, estimation_problem: EstimationProblem, measurement: bool = False - ) -> QuantumCircuit: - """Construct the Amplitude Estimation quantum circuit. - - Args: - estimation_problem: The estimation problem for which to construct the QAE circuit. - measurement: Boolean flag to indicate if measurements should be included in the circuit. - - Returns: - The QuantumCircuit object for the constructed circuit. - """ - # use custom Phase Estimation circuit if provided - if self._pec is not None: - pec = self._pec - - # otherwise use the circuit library -- note that this does not include the A operator - else: - from qiskit.circuit.library import PhaseEstimation - - pec = PhaseEstimation(self._m, estimation_problem.grover_operator, iqft=self._iqft) - - # combine the Phase Estimation circuit with the A operator - circuit = QuantumCircuit(*pec.qregs) - circuit.compose( - estimation_problem.state_preparation, - list(range(self._m, circuit.num_qubits)), - inplace=True, - ) - circuit.compose(pec, inplace=True) - - # add measurements if necessary - if measurement: - cr = ClassicalRegister(self._m) - circuit.add_register(cr) - circuit.measure(list(range(self._m)), list(range(self._m))) - - return circuit - - def evaluate_measurements( - self, - circuit_results: dict[str, int] | np.ndarray, - threshold: float = 1e-6, - ) -> tuple[dict[float, float], dict[int, float]]: - """Evaluate the results from the circuit simulation. - - Given the probabilities from statevector simulation of the QAE circuit, compute the - probabilities that the measurements y/gridpoints a are the best estimate. - - Args: - circuit_results: The circuit result from the QAE circuit. Can be either a counts dict - or a statevector or a quasi-probabilities dict. - threshold: Measurements with probabilities below the threshold are discarded. - - Returns: - Dictionaries containing the a gridpoints with respective probabilities and - y measurements with respective probabilities, in this order. - """ - # compute grid sample and measurement dicts - if isinstance(circuit_results, dict): - if set(map(type, circuit_results.values())) == {int}: - samples, measurements = self._evaluate_count_results(circuit_results) - else: - samples, measurements = self._evaluate_quasi_probabilities_results(circuit_results) - else: - samples, measurements = self._evaluate_statevector_results(circuit_results) - - # cutoff probabilities below the threshold - samples = {a: p for a, p in samples.items() if p > threshold} - measurements = {y: p for y, p in measurements.items() if p > threshold} - - return samples, measurements - - def _evaluate_statevector_results(self, statevector): - # map measured results to estimates - measurements = OrderedDict() # type: OrderedDict - num_qubits = int(np.log2(len(statevector))) - for i, amplitude in enumerate(statevector): - b = bin(i)[2:].zfill(num_qubits)[::-1] - y = int(b[: self._m], 2) # chop off all except the evaluation qubits - measurements[y] = measurements.get(y, 0) + np.abs(amplitude) ** 2 - - samples = OrderedDict() # type: OrderedDict - for y, probability in measurements.items(): - if y >= int(self._M / 2): - y = self._M - y - # due to the finite accuracy of the sine, we round the result to 7 decimals - a = np.round(np.power(np.sin(y * np.pi / 2**self._m), 2), decimals=7) - samples[a] = samples.get(a, 0) + probability - - return samples, measurements - - def _evaluate_quasi_probabilities_results(self, circuit_results): - # construct probabilities - measurements = OrderedDict() - samples = OrderedDict() - for state, probability in circuit_results.items(): - # reverts the last _m items - y = int(state[: -self._m - 1 : -1], 2) - measurements[y] = probability - a = np.round(np.power(np.sin(y * np.pi / 2**self._m), 2), decimals=7) - samples[a] = samples.get(a, 0.0) + probability - - return samples, measurements - - def _evaluate_count_results(self, counts) -> tuple[dict[float, float], dict[int, float]]: - # construct probabilities - measurements: dict[int, float] = OrderedDict() - samples: dict[float, float] = OrderedDict() - shots = sum(counts.values()) - for state, count in counts.items(): - y = int(state.replace(" ", "")[: self._m][::-1], 2) - probability = count / shots - measurements[y] = probability - a = np.round(np.power(np.sin(y * np.pi / 2**self._m), 2), decimals=7) - samples[a] = samples.get(a, 0.0) + probability - - return samples, measurements - - @staticmethod - def compute_mle( - result: "AmplitudeEstimationResult", apply_post_processing: bool = False - ) -> float: - """Compute the Maximum Likelihood Estimator (MLE). - - Args: - result: An amplitude estimation result object. - apply_post_processing: If True, apply the post processing to the MLE before returning - it. - - Returns: - The MLE for the provided result object. - """ - m = result.num_evaluation_qubits - M = 2**m # pylint: disable=invalid-name - qae = result.estimation - - # likelihood function - a_i = np.asarray(list(result.samples.keys())) - p_i = np.asarray(list(result.samples.values())) - - def loglikelihood(a): - return np.sum(result.shots * p_i * np.log(pdf_a(a_i, a, m))) - - # y is pretty much an integer, but to map 1.9999 to 2 we must first - # use round and then int conversion - y = int(np.round(M * np.arcsin(np.sqrt(qae)) / np.pi)) - - # Compute the two intervals in which are candidates for containing - # the maximum of the log-likelihood function: the two bubbles next to - # the QAE estimate - if y == 0: - right_of_qae = np.sin(np.pi * (y + 1) / M) ** 2 - bubbles = [qae, right_of_qae] - - elif y == int(M / 2): # remember, M = 2^m is a power of 2 - left_of_qae = np.sin(np.pi * (y - 1) / M) ** 2 - bubbles = [left_of_qae, qae] - - else: - left_of_qae = np.sin(np.pi * (y - 1) / M) ** 2 - right_of_qae = np.sin(np.pi * (y + 1) / M) ** 2 - bubbles = [left_of_qae, qae, right_of_qae] - - # Find global maximum amongst the two local maxima - a_opt = qae - loglik_opt = loglikelihood(a_opt) - for a, b in zip(bubbles[:-1], bubbles[1:]): - locmax, val = bisect_max(loglikelihood, a, b, retval=True) - if val > loglik_opt: - a_opt = locmax - loglik_opt = val - - if apply_post_processing: - return result.post_processing(a_opt) - - return a_opt - - def estimate(self, estimation_problem: EstimationProblem) -> "AmplitudeEstimationResult": - """Run the amplitude estimation algorithm on provided estimation problem. - - Args: - estimation_problem: The estimation problem. - - Returns: - An amplitude estimation results object. - - Raises: - ValueError: If `state_preparation` or `objective_qubits` are not set in the - `estimation_problem`. - ValueError: A quantum instance or sampler must be provided. - AlgorithmError: Sampler job run error. - """ - # check if A factory or state_preparation has been set - if estimation_problem.state_preparation is None: - raise ValueError( - "The state_preparation property of the estimation problem must be set." - ) - if self._quantum_instance is None and self._sampler is None: - raise ValueError("A quantum instance or sampler must be provided.") - - if estimation_problem.objective_qubits is None: - raise ValueError("The objective_qubits property of the estimation problem must be set.") - - if estimation_problem.has_good_state: - warnings.warn( - "The AmplitudeEstimation class does not support an is_good_state function to " - "identify good states. For this algorithm, a custom oracle has to be encoded directly " - "in the grover_operator. If no custom oracle is set, this algorithm identifies good " - "states as those, where all objective qubits are in state 1." - ) - - result = AmplitudeEstimationResult() - result.num_evaluation_qubits = self._m - result.post_processing = estimation_problem.post_processing - - shots = 0 - if self._quantum_instance is not None and self._quantum_instance.is_statevector: - circuit = self.construct_circuit(estimation_problem, measurement=False) - # run circuit on statevector simulator - statevector = self._quantum_instance.execute(circuit).get_statevector() - result.circuit_results = statevector - # store number of shots: convention is 1 shot for statevector, - # needed so that MLE works! - shots = 1 - else: - circuit = self.construct_circuit(estimation_problem, measurement=True) - if self._quantum_instance is not None: - # run circuit on QASM simulator - result.circuit_results = self._quantum_instance.execute(circuit).get_counts() - shots = sum(result.circuit_results.values()) - else: - try: - job = self._sampler.run([circuit]) - ret = job.result() - except Exception as exc: - raise AlgorithmError("The job was not completed successfully. ") from exc - - shots = ret.metadata[0].get("shots") - if shots is None: - result.circuit_results = ret.quasi_dists[0].binary_probabilities() - shots = 1 - else: - result.circuit_results = { - k: round(v * shots) - for k, v in ret.quasi_dists[0].binary_probabilities().items() - } - - # store shots - result.shots = shots - samples, measurements = self.evaluate_measurements(result.circuit_results) - - result.samples = samples - result.samples_processed = { - estimation_problem.post_processing(a): p for a, p in samples.items() - } - result.measurements = measurements - - # determine the most likely estimate - result.max_probability = 0 - for amplitude, (mapped, prob) in zip(samples.keys(), result.samples_processed.items()): - if prob > result.max_probability: - result.max_probability = prob - result.estimation = amplitude - result.estimation_processed = mapped - - # store the number of oracle queries - result.num_oracle_queries = result.shots * (self._M - 1) - - # run the MLE post-processing - mle = self.compute_mle(result) - result.mle = mle - result.mle_processed = estimation_problem.post_processing(mle) - - result.confidence_interval = self.compute_confidence_interval(result) - result.confidence_interval_processed = tuple( - estimation_problem.post_processing(value) for value in result.confidence_interval - ) - - return result - - @staticmethod - def compute_confidence_interval( - result: "AmplitudeEstimationResult", alpha: float = 0.05, kind: str = "likelihood_ratio" - ) -> tuple[float, float]: - """Compute the (1 - alpha) confidence interval. - - Args: - result: An amplitude estimation result for which to compute the confidence interval. - alpha: Confidence level: compute the (1 - alpha) confidence interval. - kind: The method to compute the confidence interval, can be 'fisher', 'observed_fisher' - or 'likelihood_ratio' (default) - - Returns: - The (1 - alpha) confidence interval of the specified kind. - - Raises: - NotImplementedError: If the confidence interval method `kind` is not implemented. - """ - # if statevector simulator the estimate is exact - if isinstance(result.circuit_results, (list, np.ndarray)): - return (result.mle, result.mle) - - if kind in ["likelihood_ratio", "lr"]: - return _likelihood_ratio_confint(result, alpha) - - if kind in ["fisher", "fi"]: - return _fisher_confint(result, alpha, observed=False) - - if kind in ["observed_fisher", "observed_information", "oi"]: - return _fisher_confint(result, alpha, observed=True) - - raise NotImplementedError(f"CI `{kind}` is not implemented.") - - -class AmplitudeEstimationResult(AmplitudeEstimatorResult): - """The ``AmplitudeEstimation`` result object.""" - - def __init__(self) -> None: - super().__init__() - self._num_evaluation_qubits: int | None = None - self._mle: float | None = None - self._mle_processed: float | None = None - self._samples: dict[float, float] | None = None - self._samples_processed: dict[float, float] | None = None - self._y_measurements: dict[int, float] | None = None - self._max_probability: float | None = None - - @property - def num_evaluation_qubits(self) -> int: - """Returns the number of evaluation qubits.""" - return self._num_evaluation_qubits - - @num_evaluation_qubits.setter - def num_evaluation_qubits(self, num_evaluation_qubits: int) -> None: - """Set the number of evaluation qubits.""" - self._num_evaluation_qubits = num_evaluation_qubits - - @property - def mle_processed(self) -> float: - """Return the post-processed MLE for the amplitude.""" - return self._mle_processed - - @mle_processed.setter - def mle_processed(self, value: float) -> None: - """Set the post-processed MLE for the amplitude.""" - self._mle_processed = value - - @property - def samples_processed(self) -> dict[float, float]: - """Return the post-processed measurement samples with their measurement probability.""" - return self._samples_processed - - @samples_processed.setter - def samples_processed(self, value: dict[float, float]) -> None: - """Set the post-processed measurement samples.""" - self._samples_processed = value - - @property - def mle(self) -> float: - r"""Return the MLE for the amplitude, in $[0, 1]$.""" - return self._mle - - @mle.setter - def mle(self, value: float) -> None: - r"""Set the MLE for the amplitude, in $[0, 1]$.""" - self._mle = value - - @property - def samples(self) -> dict[float, float]: - """Return the measurement samples with their measurement probability.""" - return self._samples - - @samples.setter - def samples(self, value: dict[float, float]) -> None: - """Set the measurement samples with their measurement probability.""" - self._samples = value - - @property - def measurements(self) -> dict[int, float]: - """Return the measurements as integers with their measurement probability.""" - return self._y_measurements - - @measurements.setter - def measurements(self, value: dict[int, float]) -> None: - """Set the measurements as integers with their measurement probability.""" - self._y_measurements = 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 - - -def _compute_fisher_information(result: AmplitudeEstimationResult, observed: bool = False) -> float: - """Computes the Fisher information for the output of the previous run. - - Args: - result: An amplitude estimation result for which to compute the confidence interval. - observed: If True, the observed Fisher information is returned, otherwise - the expected Fisher information. - - Returns: - The Fisher information. - """ - fisher_information = None - mlv = result.mle # MLE in [0,1] - m = result.num_evaluation_qubits - M = 2**m # pylint: disable=invalid-name - - if observed: - a_i = np.asarray(list(result.samples.keys())) - p_i = np.asarray(list(result.samples.values())) - - # Calculate the observed Fisher information - fisher_information = sum(p * derivative_log_pdf_a(a, mlv, m) ** 2 for p, a in zip(p_i, a_i)) - else: - - def integrand(x): - return (derivative_log_pdf_a(x, mlv, m)) ** 2 * pdf_a(x, mlv, m) - - grid = np.sin(np.pi * np.arange(M / 2 + 1) / M) ** 2 - fisher_information = sum(integrand(x) for x in grid) - - return fisher_information - - -def _fisher_confint( - result: AmplitudeEstimationResult, alpha: float, observed: bool = False -) -> tuple[float, float]: - """Compute the Fisher information confidence interval for the MLE of the previous run. - - Args: - result: An amplitude estimation result for which to compute the confidence interval. - alpha: Specifies the (1 - alpha) confidence level (0 < alpha < 1). - observed: If True, the observed Fisher information is used to construct the - confidence interval, otherwise the expected Fisher information. - - Returns: - The Fisher information confidence interval. - """ - # approximate the standard deviation of the MLE and construct the confidence interval - std = np.sqrt(result.shots * _compute_fisher_information(result, observed)) - confint = result.mle + norm.ppf(1 - alpha / 2) / std * np.array([-1, 1]) - - # transform the confidence interval from [0, 1] to the target interval - return result.post_processing(confint[0]), result.post_processing(confint[1]) - - -def _likelihood_ratio_confint( - result: AmplitudeEstimationResult, alpha: float -) -> tuple[float, float]: - """Compute the likelihood ratio confidence interval for the MLE of the previous run. - - Args: - result: An amplitude estimation result for which to compute the confidence interval. - alpha: Specifies the (1 - alpha) confidence level (0 < alpha < 1). - - Returns: - The likelihood ratio confidence interval. - """ - # Compute the two intervals in which we the look for values above - # the likelihood ratio: the two bubbles next to the QAE estimate - m = result.num_evaluation_qubits - M = 2**m # pylint: disable=invalid-name - qae = result.estimation - - y = int(np.round(M * np.arcsin(np.sqrt(qae)) / np.pi)) - if y == 0: - right_of_qae = np.sin(np.pi * (y + 1) / M) ** 2 - bubbles = [qae, right_of_qae] - - elif y == int(M / 2): # remember, M = 2^m is a power of 2 - left_of_qae = np.sin(np.pi * (y - 1) / M) ** 2 - bubbles = [left_of_qae, qae] - - else: - left_of_qae = np.sin(np.pi * (y - 1) / M) ** 2 - right_of_qae = np.sin(np.pi * (y + 1) / M) ** 2 - bubbles = [left_of_qae, qae, right_of_qae] - - # likelihood function - a_i = np.asarray(list(result.samples.keys())) - p_i = np.asarray(list(result.samples.values())) - - def loglikelihood(a): - return np.sum(result.shots * p_i * np.log(pdf_a(a_i, a, m))) - - # The threshold above which the likelihoods are in the - # confidence interval - loglik_mle = loglikelihood(result.mle) - thres = loglik_mle - chi2.ppf(1 - alpha, df=1) / 2 - - def cut(x): - return loglikelihood(x) - thres - - # Store the boundaries of the confidence interval - # It's valid to start off with the zero-width confidence interval, since the maximum - # of the likelihood function is guaranteed to be over the threshold, and if alpha = 0 - # that's the valid interval - lower = upper = result.mle - - # Check the two intervals/bubbles: check if they surpass the - # threshold and if yes add the part that does to the CI - for a, b in zip(bubbles[:-1], bubbles[1:]): - # Compute local maximum and perform a bisect search between - # the local maximum and the bubble boundaries - locmax, val = bisect_max(loglikelihood, a, b, retval=True) - if val >= thres: - # Bisect pre-condition is that the function has different - # signs at the boundaries of the interval we search in - if cut(a) * cut(locmax) < 0: - left = bisect(cut, a, locmax) - lower = np.minimum(lower, left) - if cut(locmax) * cut(b) < 0: - right = bisect(cut, locmax, b) - upper = np.maximum(upper, right) - - # Put together CI - return result.post_processing(lower), result.post_processing(upper) diff --git a/qiskit/algorithms/amplitude_estimators/ae_utils.py b/qiskit/algorithms/amplitude_estimators/ae_utils.py deleted file mode 100644 index bd695be45a63..000000000000 --- a/qiskit/algorithms/amplitude_estimators/ae_utils.py +++ /dev/null @@ -1,258 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2018, 2020. -# -# 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. - -"""Utils for the Maximum-Likelihood estimation used in ``AmplitudeEstimation``.""" - -import logging -import numpy as np - -logger = logging.getLogger(__name__) - -# pylint: disable=invalid-name - - -def bisect_max(f, a, b, steps=50, minwidth=1e-12, retval=False): - """Find the maximum of the real-valued function f in the interval [a, b] using bisection. - - Args: - f (callable): the function to find the maximum of - a (float): the lower limit of the interval - b (float): the upper limit of the interval - steps (int): the maximum number of steps in the bisection - minwidth (float): if the current interval is smaller than minwidth stop - the search - retval (bool): return value - - Returns: - float: The maximum of f in [a,b] according to this algorithm. - """ - it = 0 - m = (a + b) / 2 - fm = 0 - while it < steps and b - a > minwidth: - l, r = (a + m) / 2, (m + b) / 2 - fl, fm, fr = f(l), f(m), f(r) - - # fl is the maximum - if fl > fm and fl > fr: - b = m - m = l - # fr is the maximum - elif fr > fm and fr > fl: - a = m - m = r - # fm is the maximum - else: - a = l - b = r - - it += 1 - - if it == steps: - logger.warning("-- Warning, bisect_max didn't converge after %s steps", steps) - - if retval: - return m, fm - - return m - - -def _circ_dist(x, p): - r"""Circumferential distance function. - - For two angles :math:`x` and :math:`p` on the unit circuit this function is defined as - - .. math:: - - d(x, p) = \min_{z \in [-1, 0, 1]} |z + p - x| - - Args: - x (float): first angle - p (float): second angle - - Returns: - float: d(x, p) - """ - t = p - x - # Since x and p \in [0,1] it suffices to check not all integers - # but only -1, 0 and 1 - z = np.array([-1, 0, 1]) - - if hasattr(t, "__len__"): - d = np.empty_like(t) - for idx, ti in enumerate(t): - d[idx] = np.min(np.abs(z + ti)) - return d - - return np.min(np.abs(z + t)) - - -def _derivative_circ_dist(x, p): - """Derivative of circumferential distance function. - - Args: - x (float): first angle - p (float): second angle - - Returns: - float: The derivative. - """ - # pylint: disable=chained-comparison,misplaced-comparison-constant - t = p - x - if t < -0.5 or (0 < t and t < 0.5): - return -1 - if t > 0.5 or (-0.5 < t and t < 0): - return 1 - return 0 - - -def _amplitude_to_angle(a): - r"""Transform from the amplitude :math:`a \in [0, 1]` to the generating angle. - - In QAE, the amplitude can be written from a generating angle :math:`\omega` as - - .. math: - - a = \sin^2(\pi \omega) - - This returns the :math:`\omega` for a given :math:`a`. - - Args: - a (float): A value in :math:`[0,1]`. - - Returns: - float: :math:`\sin^{-1}(\sqrt{a}) / \pi` - """ - return np.arcsin(np.sqrt(a)) / np.pi - - -def _derivative_amplitude_to_angle(a): - """Compute the derivative of ``amplitude_to_angle``.""" - return 1 / (2 * np.pi * np.sqrt((1 - a) * a)) - - -def _alpha(x, p): - """Helper function for `pdf_a`, alpha = pi * d(omega(x), omega(p)). - - Here, omega(x) is `_amplitude_to_angle(x)`. - """ - omega = _amplitude_to_angle - return np.pi * _circ_dist(omega(x), omega(p)) - - -def _derivative_alpha(x, p): - """Compute the derivative of alpha.""" - omega = _amplitude_to_angle - d_omega = _derivative_amplitude_to_angle - return np.pi * _derivative_circ_dist(omega(x), omega(p)) * d_omega(p) - - -def _beta(x, p): - """Helper function for `pdf_a`, beta = pi * d(1 - omega(x), omega(p)).""" - omega = _amplitude_to_angle - return np.pi * _circ_dist(1 - omega(x), omega(p)) - - -def _derivative_beta(x, p): - """Compute the derivative of beta.""" - omega = _amplitude_to_angle - d_omega = _derivative_amplitude_to_angle - return np.pi * _derivative_circ_dist(1 - omega(x), omega(p)) * d_omega(p) - - -def _pdf_a_single_angle(x, p, m, pi_delta): - """Helper function for `pdf_a`.""" - M = 2**m - - d = pi_delta(x, p) - res = np.sin(M * d) ** 2 / (M * np.sin(d)) ** 2 if d != 0 else 1 - - return res - - -def pdf_a(x, p, m): - """ - Return the PDF of a, i.e. the probability of getting the estimate x - (in [0, 1]) if p (in [0, 1]) is the true value, given that we use m qubits. - - Args: - x (float): the grid point - p (float): the true value - m (float): the number of evaluation qubits - - Returns: - float: PDF(x|p) - """ - # We'll use list comprehension, so the input should be a list - scalar = False - if not hasattr(x, "__len__"): - scalar = True - x = np.asarray([x]) - - # Compute the probabilities: Add up both angles that produce the given - # value, except for the angles 0 and 0.5, which map to the unique a-values, - # 0 and 1, respectively - pr = np.array( - [ - _pdf_a_single_angle(xi, p, m, _alpha) + _pdf_a_single_angle(xi, p, m, _beta) - if (xi not in [0, 1]) - else _pdf_a_single_angle(xi, p, m, _alpha) - for xi in x - ] - ).flatten() - - # If is was a scalar return scalar otherwise the array - return pr[0] if scalar else pr - - -def derivative_log_pdf_a(x, p, m): - """ - Return the derivative of the logarithm of the PDF of a. - - Args: - x (float): the grid point - p (float): the true value - m (float): the number of evaluation qubits - - Returns: - float: d/dp log(PDF(x|p)) - """ - M = 2**m - - if x not in [0, 1]: - num_p1 = 0 - for A, dA, B, dB in zip( - [_alpha, _beta], - [_derivative_alpha, _derivative_beta], - [_beta, _alpha], - [_derivative_beta, _derivative_alpha], - ): - num_p1 += 2 * M * np.sin(M * A(x, p)) * np.cos(M * A(x, p)) * dA(x, p) * np.sin( - B(x, p) - ) ** 2 + 2 * np.sin(M * A(x, p)) ** 2 * np.sin(B(x, p)) * np.cos(B(x, p)) * dB(x, p) - - den_p1 = ( - np.sin(M * _alpha(x, p)) ** 2 * np.sin(_beta(x, p)) ** 2 - + np.sin(M * _beta(x, p)) ** 2 * np.sin(_alpha(x, p)) ** 2 - ) - - num_p2 = 0 - for A, dA, B in zip( - [_alpha, _beta], [_derivative_alpha, _derivative_beta], [_beta, _alpha] - ): - num_p2 += 2 * np.cos(A(x, p)) * dA(x, p) * np.sin(B(x, p)) - - den_p2 = np.sin(_alpha(x, p)) * np.sin(_beta(x, p)) - - return num_p1 / den_p1 - num_p2 / den_p2 - - return 2 * _derivative_alpha(x, p) * (M / np.tan(M * _alpha(x, p)) - 1 / np.tan(_alpha(x, p))) diff --git a/qiskit/algorithms/amplitude_estimators/amplitude_estimator.py b/qiskit/algorithms/amplitude_estimators/amplitude_estimator.py deleted file mode 100644 index 613827c6ccd6..000000000000 --- a/qiskit/algorithms/amplitude_estimators/amplitude_estimator.py +++ /dev/null @@ -1,131 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2018, 2022. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. - -"""The Amplitude Estimation interface.""" - -from __future__ import annotations -from abc import abstractmethod, ABC -from collections.abc import Callable - -import numpy as np - -from .estimation_problem import EstimationProblem -from ..algorithm_result import AlgorithmResult - - -class AmplitudeEstimator(ABC): - """The Amplitude Estimation interface.""" - - @abstractmethod - def estimate(self, estimation_problem: EstimationProblem) -> "AmplitudeEstimatorResult": - """Run the amplitude estimation algorithm. - - Args: - estimation_problem: An ``EstimationProblem`` containing all problem-relevant information - such as the state preparation and the objective qubits. - """ - raise NotImplementedError - - -class AmplitudeEstimatorResult(AlgorithmResult): - """The results object for amplitude estimation algorithms.""" - - def __init__(self) -> None: - super().__init__() - self._circuit_results: np.ndarray | dict[str, int] | None = None - self._shots: int | None = None - self._estimation: float | None = None - self._estimation_processed: float | None = None - self._num_oracle_queries: int | None = None - self._post_processing: Callable[[float], float] | None = None - self._confidence_interval: tuple[float, float] | None = None - self._confidence_interval_processed: tuple[float, float] | None = None - - @property - def circuit_results(self) -> np.ndarray | dict[str, int] | None: - """Return the circuit results. Can be a statevector or counts dictionary.""" - return self._circuit_results - - @circuit_results.setter - def circuit_results(self, value: np.ndarray | dict[str, int]) -> None: - """Set the circuit results.""" - self._circuit_results = value - - @property - def shots(self) -> int: - """Return the number of shots used. Is 1 for statevector-based simulations.""" - return self._shots - - @shots.setter - def shots(self, value: int) -> None: - """Set the number of shots used.""" - self._shots = value - - @property - def estimation(self) -> float: - r"""Return the estimation for the amplitude in :math:`[0, 1]`.""" - return self._estimation - - @estimation.setter - def estimation(self, value: float) -> None: - r"""Set the estimation for the amplitude in :math:`[0, 1]`.""" - self._estimation = value - - @property - def estimation_processed(self) -> float: - """Return the estimation for the amplitude after the post-processing has been applied.""" - return self._estimation_processed - - @estimation_processed.setter - def estimation_processed(self, value: float) -> None: - """Set the estimation for the amplitude after the post-processing has been applied.""" - self._estimation_processed = value - - @property - def num_oracle_queries(self) -> int: - """Return the number of Grover oracle queries.""" - return self._num_oracle_queries - - @num_oracle_queries.setter - def num_oracle_queries(self, value: int) -> None: - """Set the number of Grover oracle queries.""" - self._num_oracle_queries = value - - @property - def post_processing(self) -> Callable[[float], float]: - """Return a handle to the post processing function.""" - return self._post_processing - - @post_processing.setter - def post_processing(self, post_processing: Callable[[float], float]) -> None: - """Set a handle to the post processing function.""" - self._post_processing = post_processing - - @property - def confidence_interval(self) -> tuple[float, float]: - """Return the confidence interval for the amplitude (95% interval by default).""" - return self._confidence_interval - - @confidence_interval.setter - def confidence_interval(self, confidence_interval: tuple[float, float]) -> None: - """Set the confidence interval for the amplitude (95% interval by default).""" - self._confidence_interval = confidence_interval - - @property - def confidence_interval_processed(self) -> tuple[float, float]: - """Return the post-processed confidence interval (95% interval by default).""" - return self._confidence_interval_processed - - @confidence_interval_processed.setter - def confidence_interval_processed(self, confidence_interval: tuple[float, float]) -> None: - """Set the post-processed confidence interval (95% interval by default).""" - self._confidence_interval_processed = confidence_interval diff --git a/qiskit/algorithms/amplitude_estimators/estimation_problem.py b/qiskit/algorithms/amplitude_estimators/estimation_problem.py deleted file mode 100644 index 72e9418f72d6..000000000000 --- a/qiskit/algorithms/amplitude_estimators/estimation_problem.py +++ /dev/null @@ -1,274 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2020. -# -# 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 Estimation problem class.""" - -from __future__ import annotations -import warnings -from collections.abc import Callable - -import numpy - -from qiskit.circuit import QuantumCircuit, QuantumRegister -from qiskit.circuit.library import GroverOperator - - -class EstimationProblem: - """The estimation problem is the input to amplitude estimation algorithm. - - This class contains all problem-specific information required to run an amplitude estimation - algorithm. That means, it minimally contains the state preparation and the specification - of the good state. It can further hold some post processing on the estimation of the amplitude - or a custom Grover operator. - """ - - def __init__( - self, - state_preparation: QuantumCircuit, - objective_qubits: int | list[int], - grover_operator: QuantumCircuit | None = None, - post_processing: Callable[[float], float] | None = None, - is_good_state: Callable[[str], bool] | None = None, - ) -> None: - r""" - Args: - state_preparation: A circuit preparing the input state, referred to as - :math:`\mathcal{A}`. - objective_qubits: A single qubit index or a list of qubit indices to specify which - qubits to measure. The ``is_good_state`` function is applied on the bitstring of - these objective qubits. - grover_operator: The Grover operator :math:`\mathcal{Q}` used as unitary in the - phase estimation circuit. - post_processing: A mapping applied to the result of the algorithm - :math:`0 \leq a \leq 1`, usually used to map the estimate to a target interval. - Defaults to the identity. - is_good_state: A function to check whether a string represents a good state. Defaults - to all objective qubits being in state :math:`|1\rangle`. - """ - self._state_preparation = state_preparation - self._objective_qubits = objective_qubits - self._grover_operator = grover_operator - self._post_processing = post_processing - self._is_good_state = is_good_state - - @property - def state_preparation(self) -> QuantumCircuit | None: - r"""Get the :math:`\mathcal{A}` operator encoding the amplitude :math:`a`. - - Returns: - The :math:`\mathcal{A}` operator as `QuantumCircuit`. - """ - return self._state_preparation - - @state_preparation.setter - def state_preparation(self, state_preparation: QuantumCircuit) -> None: - r"""Set the :math:`\mathcal{A}` operator, that encodes the amplitude to be estimated. - - Args: - state_preparation: The new :math:`\mathcal{A}` operator. - """ - self._state_preparation = state_preparation - - @property - def objective_qubits(self) -> list[int]: - """Get the criterion for a measurement outcome to be in a 'good' state. - - Returns: - The criterion as list of qubit indices. - """ - 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: - """Set the criterion for a measurement outcome to be in a 'good' state. - - Args: - objective_qubits: The criterion as callable of list of qubit indices. - """ - self._objective_qubits = objective_qubits - - @property - def post_processing(self) -> Callable[[float], float]: - """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[[float], float] | None) -> None: - """Set the post processing function. - - Args: - post_processing: A handle to the post processing function. If set to ``None``, the - identity will be used as post processing. - """ - self._post_processing = post_processing - - @property - def has_good_state(self) -> bool: - """Check whether an :attr:`is_good_state` function is set. - - Some amplitude estimators, such as :class:`.AmplitudeEstimation` do not support - a custom implementation of the :attr:`is_good_state` function, and can only handle - the default. - - Returns: - ``True``, if a custom :attr:`is_good_state` is set, otherwise returns ``False``. - """ - return self._is_good_state is not None - - @property - def is_good_state(self) -> Callable[[str], bool]: - """Checks whether a bitstring represents a good state. - - Returns: - Handle to the ``is_good_state`` callable. - """ - if self._is_good_state is None: - return lambda x: all(bit == "1" for bit in x) - - return self._is_good_state - - @is_good_state.setter - def is_good_state(self, is_good_state: Callable[[str], bool] | None) -> None: - """Set the ``is_good_state`` function. - - Args: - is_good_state: A function to determine whether a bitstring represents a good state. - If set to ``None``, the good state will be defined as all bits being one. - """ - 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 not None: - return self._grover_operator - - # build the reflection about the bad state: a MCZ with open controls (thus X gates - # around the controls) and X gates around the target to change from a phaseflip on - # |1> to a phaseflip on |0> - num_state_qubits = self.state_preparation.num_qubits - self.state_preparation.num_ancillas - - oracle = QuantumCircuit(num_state_qubits) - oracle.h(self.objective_qubits[-1]) - if len(self.objective_qubits) == 1: - oracle.x(self.objective_qubits[0]) - else: - oracle.mcx(self.objective_qubits[:-1], self.objective_qubits[-1]) - oracle.h(self.objective_qubits[-1]) - - # construct the grover operator - return GroverOperator(oracle, self.state_preparation) - - @grover_operator.setter - def grover_operator(self, grover_operator: QuantumCircuit | None) -> None: - r"""Set the :math:`\mathcal{Q}` operator. - - Args: - grover_operator: The new :math:`\mathcal{Q}` operator. If set to ``None``, - the default construction via ``qiskit.circuit.library.GroverOperator`` is used. - """ - self._grover_operator = grover_operator - - def rescale(self, scaling_factor: float) -> "EstimationProblem": - """Rescale the good state amplitude in the estimation problem. - - Args: - scaling_factor: The scaling factor in [0, 1]. - - Returns: - A rescaled estimation problem. - """ - if self._grover_operator is not None: - warnings.warn("Rescaling discards the Grover operator.") - - # rescale the amplitude by a factor of 1/4 by adding an auxiliary qubit - rescaled_stateprep = _rescale_amplitudes(self.state_preparation, scaling_factor) - num_qubits = self.state_preparation.num_qubits - objective_qubits = self.objective_qubits + [num_qubits] - - # add the scaling qubit to the good state qualifier - def is_good_state(bitstr): - return self.is_good_state(bitstr[1:]) and bitstr[0] == "1" - - # rescaled estimation problem - problem = EstimationProblem( - rescaled_stateprep, - objective_qubits=objective_qubits, - post_processing=self.post_processing, - is_good_state=is_good_state, - ) - - return problem - - -def _rescale_amplitudes(circuit: QuantumCircuit, scaling_factor: float) -> QuantumCircuit: - r"""Uses an auxiliary qubit to scale the amplitude of :math:`|1\rangle` by ``scaling_factor``. - - Explained in Section 2.1. of [1]. - - For example, for a scaling factor of 0.25 this turns this circuit - - .. parsed-literal:: - - ┌───┐ - state_0: ─────┤ H ├─────────■──── - ┌───┴───┴───┐ ┌───┴───┐ - obj_0: ─┤ RY(0.125) ├─┤ RY(1) ├ - └───────────┘ └───────┘ - - into - - .. parsed-literal:: - - ┌───┐ - state_0: ─────┤ H ├─────────■──── - ┌───┴───┴───┐ ┌───┴───┐ - obj_0: ─┤ RY(0.125) ├─┤ RY(1) ├ - ┌┴───────────┴┐└───────┘ - scaling_0: ┤ RY(0.50536) ├───────── - └─────────────┘ - - References: - - [1]: K. Nakaji. Faster Amplitude Estimation, 2020; - `arXiv:2002.02417 `_ - - Args: - circuit: The circuit whose amplitudes to rescale. - scaling_factor: The rescaling factor. - - Returns: - A copy of the circuit with an additional qubit and RY gate for the rescaling. - """ - qr = QuantumRegister(1, "scaling") - rescaled = QuantumCircuit(*circuit.qregs, qr) - rescaled.compose(circuit, circuit.qubits, inplace=True) - rescaled.ry(2 * numpy.arcsin(scaling_factor), qr) - return rescaled diff --git a/qiskit/algorithms/amplitude_estimators/fae.py b/qiskit/algorithms/amplitude_estimators/fae.py deleted file mode 100644 index 0fa5a7588685..000000000000 --- a/qiskit/algorithms/amplitude_estimators/fae.py +++ /dev/null @@ -1,394 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2017, 2022. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. - -"""Faster Amplitude Estimation.""" - -from __future__ import annotations -import warnings -import numpy as np - -from qiskit.circuit import QuantumCircuit, ClassicalRegister -from qiskit.providers import Backend -from qiskit.primitives import BaseSampler -from qiskit.utils import QuantumInstance -from qiskit.utils.deprecation import deprecate_arg, deprecate_func -from qiskit.algorithms.exceptions import AlgorithmError - -from .amplitude_estimator import AmplitudeEstimator, AmplitudeEstimatorResult -from .estimation_problem import EstimationProblem - - -class FasterAmplitudeEstimation(AmplitudeEstimator): - """The Faster Amplitude Estimation algorithm. - - The Faster Amplitude Estimation (FAE) [1] algorithm is a variant of Quantum Amplitude - Estimation (QAE), where the Quantum Phase Estimation (QPE) by an iterative Grover search, - similar to [2]. - - Due to the iterative version of the QPE, this algorithm does not require any additional - qubits, as the originally proposed QAE [3] and thus the resulting circuits are less complex. - - References: - - [1]: K. Nakaji. Faster Amplitude Estimation, 2020; - `arXiv:2002.02417 `_ - [2]: D. Grinko et al. Iterative Amplitude Estimation, 2019; - `arXiv:1912.05559 `_ - [3]: G. Brassard et al. Quantum Amplitude Amplification and Estimation, 2000; - `arXiv:quant-ph/0005055 `_ - - """ - - @deprecate_arg( - "quantum_instance", - additional_msg=( - "Instead, use the ``sampler`` argument. See https://qisk.it/algo_migration for a " - "migration guide." - ), - since="0.24.0", - package_name="qiskit-terra", - ) - def __init__( - self, - delta: float, - maxiter: int, - rescale: bool = True, - quantum_instance: QuantumInstance | Backend | None = None, - sampler: BaseSampler | None = None, - ) -> None: - r""" - Args: - delta: The probability that the true value is outside of the final confidence interval. - maxiter: The number of iterations, the maximal power of Q is `2 ** (maxiter - 1)`. - rescale: Whether to rescale the problem passed to `estimate`. - quantum_instance: Deprecated: The quantum instance or backend - to run the circuits. - sampler: A sampler primitive to evaluate the circuits. - - .. note:: - - This algorithm overwrites the number of shots set in the ``quantum_instance`` - argument, but will reset them to the initial number after running. - - """ - super().__init__() - # set quantum instance - with warnings.catch_warnings(): - warnings.simplefilter("ignore") - self.quantum_instance = quantum_instance - self._shots = (int(1944 * np.log(2 / delta)), int(972 * np.log(2 / delta))) - self._rescale = rescale - self._delta = delta - self._maxiter = maxiter - self._num_oracle_calls = 0 - self._sampler = sampler - - @property - def sampler(self) -> BaseSampler | None: - """Get the sampler primitive. - - Returns: - The sampler primitive to evaluate the circuits. - """ - return self._sampler - - @sampler.setter - def sampler(self, sampler: BaseSampler) -> None: - """Set sampler primitive. - - Args: - sampler: A sampler primitive to evaluate the circuits. - """ - self._sampler = sampler - - @property - @deprecate_func( - since="0.24.0", - package_name="qiskit-terra", - is_property=True, - additional_msg="See https://qisk.it/algo_migration for a migration guide.", - ) - def quantum_instance(self) -> QuantumInstance | None: - """Deprecated. Get the quantum instance. - - Returns: - The quantum instance used to run this algorithm. - """ - return self._quantum_instance - - @quantum_instance.setter - @deprecate_func( - since="0.24.0", - package_name="qiskit-terra", - is_property=True, - additional_msg="See https://qisk.it/algo_migration for a migration guide.", - ) - def quantum_instance(self, quantum_instance: QuantumInstance | Backend) -> None: - """Deprecated. Set quantum instance. - - Args: - quantum_instance: The quantum instance used to run this algorithm. - """ - if isinstance(quantum_instance, Backend): - quantum_instance = QuantumInstance(quantum_instance) - self._quantum_instance = quantum_instance - - def _cos_estimate(self, estimation_problem, k, shots): - if self._quantum_instance is None and self._sampler is None: - raise ValueError("A quantum instance or sampler must be provided.") - - if self._sampler is not None: - circuit = self.construct_circuit(estimation_problem, k, measurement=True) - try: - job = self._sampler.run([circuit], shots=shots) - result = job.result() - except Exception as exc: - raise AlgorithmError("The job was not completed successfully. ") from exc - - if shots is None: - shots = 1 - self._num_oracle_calls += (2 * k + 1) * shots - - # sum over all probabilities where the objective qubits are 1 - prob = 0 - for bit, probabilities in result.quasi_dists[0].binary_probabilities().items(): - # check if it is a good state - if estimation_problem.is_good_state(bit): - prob += probabilities - - cos_estimate = 1 - 2 * prob - elif self._quantum_instance.is_statevector: - circuit = self.construct_circuit(estimation_problem, k, measurement=False) - statevector = self._quantum_instance.execute(circuit).get_statevector() - - # sum over all amplitudes where the objective qubits are 1 - prob = 0 - for i, amplitude in enumerate(statevector): - # get bitstring of objective qubits - full_state = bin(i)[2:].zfill(circuit.num_qubits)[::-1] - state = "".join([full_state[i] for i in estimation_problem.objective_qubits]) - - # check if it is a good state - if estimation_problem.is_good_state(state[::-1]): - prob = prob + np.abs(amplitude) ** 2 - - cos_estimate = 1 - 2 * prob - else: - circuit = self.construct_circuit(estimation_problem, k, measurement=True) - - self._quantum_instance.run_config.shots = shots - counts = self._quantum_instance.execute(circuit).get_counts() - self._num_oracle_calls += (2 * k + 1) * shots - - good_counts = 0 - for state, count in counts.items(): - if estimation_problem.is_good_state(state): - good_counts += count - - cos_estimate = 1 - 2 * good_counts / shots - - return cos_estimate - - def _chernoff(self, cos, shots) -> list[float]: - width = np.sqrt(np.log(2 / self._delta) * 12 / shots) - confint = [np.maximum(-1, cos - width), np.minimum(1, cos + width)] - return confint - - def construct_circuit( - self, estimation_problem: EstimationProblem, k: int, measurement: bool = False - ) -> QuantumCircuit | tuple[QuantumCircuit, list[int]]: - r"""Construct the circuit :math:`Q^k X |0\rangle>`. - - The A operator is the unitary specifying the QAE problem and Q the associated Grover - operator. - - Args: - estimation_problem: The estimation problem for which to construct the circuit. - k: The power of the Q operator. - measurement: Boolean flag to indicate if measurements should be included in the - circuits. - - Returns: - The circuit :math:`Q^k X |0\rangle`. - """ - num_qubits = max( - estimation_problem.state_preparation.num_qubits, - estimation_problem.grover_operator.num_qubits, - ) - circuit = QuantumCircuit(num_qubits, name="circuit") - - # add classical register if needed - if measurement: - c = ClassicalRegister(len(estimation_problem.objective_qubits)) - circuit.add_register(c) - - # add A operator - circuit.compose(estimation_problem.state_preparation, inplace=True) - - # add Q^k - if k != 0: - circuit.compose(estimation_problem.grover_operator.power(k), inplace=True) - - # add optional measurement - if measurement: - # real hardware can currently not handle operations after measurements, which might - # happen if the circuit gets transpiled, hence we're adding a safeguard-barrier - circuit.barrier() - circuit.measure(estimation_problem.objective_qubits, c[:]) - - return circuit - - def estimate(self, estimation_problem: EstimationProblem) -> "FasterAmplitudeEstimationResult": - """Run the amplitude estimation algorithm on provided estimation problem. - - Args: - estimation_problem: The estimation problem. - - Returns: - An amplitude estimation results object. - - Raises: - ValueError: A quantum instance or Sampler must be provided. - AlgorithmError: Sampler run error. - """ - if self._quantum_instance is None and self._sampler is None: - raise ValueError("A quantum instance or sampler must be provided.") - - self._num_oracle_calls = 0 - user_defined_shots = ( - self._quantum_instance._run_config.shots if self._quantum_instance is not None else None - ) - - if self._rescale: - problem = estimation_problem.rescale(0.25) - else: - problem = estimation_problem - - if self._quantum_instance is not None and self._quantum_instance.is_statevector: - cos = self._cos_estimate(problem, k=0, shots=1) - theta = np.arccos(cos) / 2 - theta_ci = [theta, theta] - theta_cis = [theta_ci] - num_steps = num_first_stage_steps = 1 - else: - theta_ci = [0, np.arcsin(0.25)] - first_stage = True - j_0 = self._maxiter - - theta_cis = [theta_ci] - num_first_stage_steps = 0 - num_steps = 0 - - def cos_estimate(power, shots): - return self._cos_estimate(problem, power, shots) - - for j in range(1, self._maxiter + 1): - num_steps += 1 - if first_stage: - num_first_stage_steps += 1 - c = cos_estimate(2 ** (j - 1), self._shots[0]) - chernoff_ci = self._chernoff(c, self._shots[0]) - theta_ci = [np.arccos(x) / (2 ** (j + 1) + 2) for x in chernoff_ci[::-1]] - - if 2 ** (j + 1) * theta_ci[1] >= 3 * np.pi / 8 and j < self._maxiter: - j_0 = j - v = 2**j * np.sum(theta_ci) - first_stage = False - else: - cos = cos_estimate(2 ** (j - 1), self._shots[1]) - cos_2 = cos_estimate(2 ** (j - 1) + 2 ** (j_0 - 1), self._shots[1]) - sin = (cos * np.cos(v) - cos_2) / np.sin(v) - rho = np.arctan2(sin, cos) - n = int(((2 ** (j + 1) + 2) * theta_ci[1] - rho + np.pi / 3) / (2 * np.pi)) - - theta_ci = [ - (2 * np.pi * n + rho + sign * np.pi / 3) / (2 ** (j + 1) + 2) - for sign in [-1, 1] - ] - theta_cis.append(theta_ci) - - theta = np.mean(theta_ci) - rescaling = 4 if self._rescale else 1 - value = (rescaling * np.sin(theta)) ** 2 - value_ci = ((rescaling * np.sin(theta_ci[0])) ** 2, (rescaling * np.sin(theta_ci[1])) ** 2) - - result = FasterAmplitudeEstimationResult() - result.num_oracle_queries = self._num_oracle_calls - result.num_steps = num_steps - result.num_first_state_steps = num_first_stage_steps - if self._quantum_instance is not None and self._quantum_instance.is_statevector: - result.success_probability = 1.0 - else: - result.success_probability = 1 - (2 * self._maxiter - j_0) * self._delta - - result.estimation = value - result.estimation_processed = problem.post_processing(value) - result.confidence_interval = value_ci - result.confidence_interval_processed = tuple(problem.post_processing(x) for x in value_ci) - result.theta_intervals = theta_cis - - # reset shots to what the user had defined - if self._quantum_instance is not None: - self._quantum_instance._run_config.shots = user_defined_shots - - return result - - -class FasterAmplitudeEstimationResult(AmplitudeEstimatorResult): - """The result object for the Faster Amplitude Estimation algorithm.""" - - def __init__(self) -> None: - super().__init__() - self._success_probability: float | None = None - self._num_steps: int | None = None - self._num_first_state_steps: int | None = None - self._theta_intervals: list[list[float]] | None = None - - @property - def success_probability(self) -> float: - """Return the success probability of the algorithm.""" - return self._success_probability - - @success_probability.setter - def success_probability(self, probability: float) -> None: - """Set the success probability of the algorithm.""" - self._success_probability = probability - - @property - def num_steps(self) -> int: - """Return the total number of steps taken in the algorithm.""" - return self._num_steps - - @num_steps.setter - def num_steps(self, num_steps: int) -> None: - """Set the total number of steps taken in the algorithm.""" - self._num_steps = num_steps - - @property - def num_first_state_steps(self) -> int: - """Return the number of steps taken in the first step of algorithm.""" - return self._num_first_state_steps - - @num_first_state_steps.setter - def num_first_state_steps(self, num_steps: int) -> None: - """Set the number of steps taken in the first step of algorithm.""" - self._num_first_state_steps = num_steps - - @property - def theta_intervals(self) -> list[list[float]]: - """Return the confidence intervals for the angles in each iteration.""" - return self._theta_intervals - - @theta_intervals.setter - def theta_intervals(self, value: list[list[float]]) -> None: - """Set the confidence intervals for the angles in each iteration.""" - self._theta_intervals = value diff --git a/qiskit/algorithms/amplitude_estimators/iae.py b/qiskit/algorithms/amplitude_estimators/iae.py deleted file mode 100644 index 638f86f08232..000000000000 --- a/qiskit/algorithms/amplitude_estimators/iae.py +++ /dev/null @@ -1,687 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2018, 2022. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. - -"""The Iterative Quantum Amplitude Estimation Algorithm.""" - -from __future__ import annotations -from typing import cast -import warnings -import numpy as np -from scipy.stats import beta - -from qiskit import ClassicalRegister, QuantumCircuit -from qiskit.providers import Backend -from qiskit.primitives import BaseSampler -from qiskit.utils import QuantumInstance -from qiskit.utils.deprecation import deprecate_arg, deprecate_func - -from .amplitude_estimator import AmplitudeEstimator, AmplitudeEstimatorResult -from .estimation_problem import EstimationProblem -from ..exceptions import AlgorithmError - - -class IterativeAmplitudeEstimation(AmplitudeEstimator): - r"""The Iterative Amplitude Estimation algorithm. - - This class implements the Iterative Quantum Amplitude Estimation (IQAE) algorithm, proposed - in [1]. The output of the algorithm is an estimate that, - with at least probability :math:`1 - \alpha`, differs by epsilon to the target value, where - both alpha and epsilon can be specified. - - It differs from the original QAE algorithm proposed by Brassard [2] in that it does not rely on - Quantum Phase Estimation, but is only based on Grover's algorithm. IQAE iteratively - applies carefully selected Grover iterations to find an estimate for the target amplitude. - - References: - [1]: Grinko, D., Gacon, J., Zoufal, C., & Woerner, S. (2019). - Iterative Quantum Amplitude Estimation. - `arXiv:1912.05559 `_. - [2]: Brassard, G., Hoyer, P., Mosca, M., & Tapp, A. (2000). - Quantum Amplitude Amplification and Estimation. - `arXiv:quant-ph/0005055 `_. - """ - - @deprecate_arg( - "quantum_instance", - additional_msg=( - "Instead, use the ``sampler`` argument. See https://qisk.it/algo_migration for a " - "migration guide." - ), - since="0.24.0", - package_name="qiskit-terra", - ) - def __init__( - self, - epsilon_target: float, - alpha: float, - confint_method: str = "beta", - min_ratio: float = 2, - quantum_instance: QuantumInstance | Backend | None = None, - sampler: BaseSampler | None = None, - ) -> None: - r""" - The output of the algorithm is an estimate for the amplitude `a`, that with at least - probability 1 - alpha has an error of epsilon. The number of A operator calls scales - linearly in 1/epsilon (up to a logarithmic factor). - - Args: - epsilon_target: Target precision for estimation target `a`, has values between 0 and 0.5 - alpha: Confidence level, the target probability is 1 - alpha, has values between 0 and 1 - confint_method: Statistical method used to estimate the confidence intervals in - each iteration, can be 'chernoff' for the Chernoff intervals or 'beta' for the - Clopper-Pearson intervals (default) - min_ratio: Minimal q-ratio (:math:`K_{i+1} / K_i`) for FindNextK - quantum_instance: Deprecated: Quantum Instance or Backend - sampler: A sampler primitive to evaluate the circuits. - - Raises: - AlgorithmError: if the method to compute the confidence intervals is not supported - ValueError: If the target epsilon is not in (0, 0.5] - ValueError: If alpha is not in (0, 1) - ValueError: If confint_method is not supported - """ - # validate ranges of input arguments - if not 0 < epsilon_target <= 0.5: - raise ValueError(f"The target epsilon must be in (0, 0.5], but is {epsilon_target}.") - - if not 0 < alpha < 1: - raise ValueError(f"The confidence level alpha must be in (0, 1), but is {alpha}") - - if confint_method not in {"chernoff", "beta"}: - raise ValueError( - f"The confidence interval method must be chernoff or beta, but is {confint_method}." - ) - - super().__init__() - - # set quantum instance - with warnings.catch_warnings(): - warnings.simplefilter("ignore") - self.quantum_instance = quantum_instance - - # store parameters - self._epsilon = epsilon_target - self._alpha = alpha - self._min_ratio = min_ratio - self._confint_method = confint_method - self._sampler = sampler - - @property - def sampler(self) -> BaseSampler | None: - """Get the sampler primitive. - - Returns: - The sampler primitive to evaluate the circuits. - """ - return self._sampler - - @sampler.setter - def sampler(self, sampler: BaseSampler) -> None: - """Set sampler primitive. - - Args: - sampler: A sampler primitive to evaluate the circuits. - """ - self._sampler = sampler - - @property - @deprecate_func( - since="0.24.0", - package_name="qiskit-terra", - is_property=True, - additional_msg="See https://qisk.it/algo_migration for a migration guide.", - ) - def quantum_instance(self) -> QuantumInstance | None: - """Deprecated. Get the quantum instance. - - Returns: - The quantum instance used to run this algorithm. - """ - return self._quantum_instance - - @quantum_instance.setter - @deprecate_func( - since="0.24.0", - package_name="qiskit-terra", - is_property=True, - additional_msg="See https://qisk.it/algo_migration for a migration guide.", - ) - def quantum_instance(self, quantum_instance: QuantumInstance | Backend) -> None: - """Deprecated. Set quantum instance. - - Args: - quantum_instance: The quantum instance used to run this algorithm. - """ - if isinstance(quantum_instance, Backend): - quantum_instance = QuantumInstance(quantum_instance) - self._quantum_instance = quantum_instance - - @property - def epsilon_target(self) -> float: - """Returns the target precision ``epsilon_target`` of the algorithm. - - Returns: - The target precision (which is half the width of the confidence interval). - """ - return self._epsilon - - @epsilon_target.setter - def epsilon_target(self, epsilon: float) -> None: - """Set the target precision of the algorithm. - - Args: - epsilon: Target precision for estimation target `a`. - """ - self._epsilon = epsilon - - def _find_next_k( - self, - k: int, - upper_half_circle: bool, - theta_interval: tuple[float, float], - min_ratio: float = 2.0, - ) -> tuple[int, bool]: - """Find the largest integer k_next, such that the interval (4 * k_next + 2)*theta_interval - lies completely in [0, pi] or [pi, 2pi], for theta_interval = (theta_lower, theta_upper). - - Args: - k: The current power of the Q operator. - upper_half_circle: Boolean flag of whether theta_interval lies in the - upper half-circle [0, pi] or in the lower one [pi, 2pi]. - theta_interval: The current confidence interval for the angle theta, - i.e. (theta_lower, theta_upper). - min_ratio: Minimal ratio K/K_next allowed in the algorithm. - - Returns: - The next power k, and boolean flag for the extrapolated interval. - - Raises: - AlgorithmError: if min_ratio is smaller or equal to 1 - """ - if min_ratio <= 1: - raise AlgorithmError("min_ratio must be larger than 1 to ensure convergence") - - # initialize variables - theta_l, theta_u = theta_interval - old_scaling = 4 * k + 2 # current scaling factor, called K := (4k + 2) - - # the largest feasible scaling factor K cannot be larger than K_max, - # which is bounded by the length of the current confidence interval - max_scaling = int(1 / (2 * (theta_u - theta_l))) - scaling = max_scaling - (max_scaling - 2) % 4 # bring into the form 4 * k_max + 2 - - # find the largest feasible scaling factor K_next, and thus k_next - while scaling >= min_ratio * old_scaling: - theta_min = scaling * theta_l - int(scaling * theta_l) - theta_max = scaling * theta_u - int(scaling * theta_u) - - if theta_min <= theta_max <= 0.5 and theta_min <= 0.5: - # the extrapolated theta interval is in the upper half-circle - upper_half_circle = True - return int((scaling - 2) / 4), upper_half_circle - - elif theta_max >= 0.5 and theta_max >= theta_min >= 0.5: - # the extrapolated theta interval is in the upper half-circle - upper_half_circle = False - return int((scaling - 2) / 4), upper_half_circle - - scaling -= 4 - - # if we do not find a feasible k, return the old one - return int(k), upper_half_circle - - def construct_circuit( - self, estimation_problem: EstimationProblem, k: int = 0, measurement: bool = False - ) -> QuantumCircuit: - r"""Construct the circuit :math:`\mathcal{Q}^k \mathcal{A} |0\rangle`. - - The A operator is the unitary specifying the QAE problem and Q the associated Grover - operator. - - Args: - estimation_problem: The estimation problem for which to construct the QAE circuit. - k: The power of the Q operator. - measurement: Boolean flag to indicate if measurements should be included in the - circuits. - - Returns: - The circuit implementing :math:`\mathcal{Q}^k \mathcal{A} |0\rangle`. - """ - num_qubits = max( - estimation_problem.state_preparation.num_qubits, - estimation_problem.grover_operator.num_qubits, - ) - circuit = QuantumCircuit(num_qubits, name="circuit") - - # add classical register if needed - if measurement: - c = ClassicalRegister(len(estimation_problem.objective_qubits)) - circuit.add_register(c) - - # add A operator - circuit.compose(estimation_problem.state_preparation, inplace=True) - - # add Q^k - if k != 0: - circuit.compose(estimation_problem.grover_operator.power(k), inplace=True) - - # add optional measurement - if measurement: - # real hardware can currently not handle operations after measurements, which might - # happen if the circuit gets transpiled, hence we're adding a safeguard-barrier - circuit.barrier() - circuit.measure(estimation_problem.objective_qubits, c[:]) - - return circuit - - def _good_state_probability( - self, - problem: EstimationProblem, - counts_or_statevector: dict[str, int] | np.ndarray, - num_state_qubits: int, - ) -> tuple[int, float] | float: - """Get the probability to measure '1' in the last qubit. - - Args: - problem: The estimation problem, used to obtain the number of objective qubits and - the ``is_good_state`` function. - counts_or_statevector: Either a counts-dictionary (with one measured qubit only!) or - the statevector returned from the statevector_simulator. - num_state_qubits: The number of state qubits. - - Returns: - If a dict is given, return (#one-counts, #one-counts/#all-counts), - otherwise Pr(measure '1' in the last qubit). - """ - if isinstance(counts_or_statevector, dict): - one_counts = 0 - for state, counts in counts_or_statevector.items(): - if problem.is_good_state(state): - one_counts += counts - - return int(one_counts), one_counts / sum(counts_or_statevector.values()) - else: - statevector = counts_or_statevector - num_qubits = int(np.log2(len(statevector))) # the total number of qubits - - # sum over all amplitudes where the objective qubit is 1 - prob = 0 - for i, amplitude in enumerate(statevector): - # consider only state qubits and revert bit order - bitstr = bin(i)[2:].zfill(num_qubits)[-num_state_qubits:][::-1] - objectives = [bitstr[index] for index in problem.objective_qubits] - if problem.is_good_state(objectives): - prob = prob + np.abs(amplitude) ** 2 - - return prob - - def estimate( - self, estimation_problem: EstimationProblem - ) -> "IterativeAmplitudeEstimationResult": - """Run the amplitude estimation algorithm on provided estimation problem. - - Args: - estimation_problem: The estimation problem. - - Returns: - An amplitude estimation results object. - - Raises: - ValueError: A quantum instance or Sampler must be provided. - AlgorithmError: Sampler job run error. - """ - if self._quantum_instance is None and self._sampler is None: - raise ValueError("A quantum instance or sampler must be provided.") - - # initialize memory variables - powers = [0] # list of powers k: Q^k, (called 'k' in paper) - ratios = [] # list of multiplication factors (called 'q' in paper) - theta_intervals = [[0, 1 / 4]] # a priori knowledge of theta / 2 / pi - a_intervals = [[0.0, 1.0]] # a priori knowledge of the confidence interval of the estimate - num_oracle_queries = 0 - num_one_shots = [] - - # maximum number of rounds - max_rounds = ( - int(np.log(self._min_ratio * np.pi / 8 / self._epsilon) / np.log(self._min_ratio)) + 1 - ) - upper_half_circle = True # initially theta is in the upper half-circle - - if self._quantum_instance is not None and self._quantum_instance.is_statevector: - # for statevector we can directly return the probability to measure 1 - # note, that no iterations here are necessary - # simulate circuit - circuit = self.construct_circuit(estimation_problem, k=0, measurement=False) - ret = self._quantum_instance.execute(circuit) - - # get statevector - statevector = ret.get_statevector(circuit) - - # calculate the probability of measuring '1' - num_qubits = circuit.num_qubits - circuit.num_ancillas - prob = self._good_state_probability(estimation_problem, statevector, num_qubits) - prob = cast(float, prob) # tell MyPy it's a float and not Tuple[int, float ] - - a_confidence_interval = [prob, prob] # type: list[float] - a_intervals.append(a_confidence_interval) - - theta_i_interval = [ - np.arccos(1 - 2 * a_i) / 2 / np.pi for a_i in a_confidence_interval # type: ignore - ] - theta_intervals.append(theta_i_interval) - num_oracle_queries = 0 # no Q-oracle call, only a single one to A - - else: - num_iterations = 0 # keep track of the number of iterations - # number of shots per iteration - shots = 0 - # do while loop, keep in mind that we scaled theta mod 2pi such that it lies in [0,1] - while theta_intervals[-1][1] - theta_intervals[-1][0] > self._epsilon / np.pi: - num_iterations += 1 - - # get the next k - k, upper_half_circle = self._find_next_k( - powers[-1], - upper_half_circle, - theta_intervals[-1], # type: ignore - min_ratio=self._min_ratio, - ) - - # store the variables - powers.append(k) - ratios.append((2 * powers[-1] + 1) / (2 * powers[-2] + 1)) - - # run measurements for Q^k A|0> circuit - circuit = self.construct_circuit(estimation_problem, k, measurement=True) - counts = {} - if self._quantum_instance is not None: - ret = self._quantum_instance.execute(circuit) - # get the counts and store them - counts = ret.get_counts(circuit) - shots = self._quantum_instance._run_config.shots - else: - try: - job = self._sampler.run([circuit]) - ret = job.result() - except Exception as exc: - raise AlgorithmError("The job was not completed successfully. ") from exc - - shots = ret.metadata[0].get("shots") - if shots is None: - circuit = self.construct_circuit(estimation_problem, k=0, measurement=True) - try: - job = self._sampler.run([circuit]) - ret = job.result() - except Exception as exc: - raise AlgorithmError( - "The job was not completed successfully. " - ) from exc - - # calculate the probability of measuring '1' - prob = 0.0 - for bit, probabilities in ret.quasi_dists[0].binary_probabilities().items(): - # check if it is a good state - if estimation_problem.is_good_state(bit): - prob += probabilities - - a_confidence_interval = [prob, prob] - a_intervals.append(a_confidence_interval) - - theta_i_interval = [ - np.arccos(1 - 2 * a_i) / 2 / np.pi for a_i in a_confidence_interval - ] - theta_intervals.append(theta_i_interval) - num_oracle_queries = 0 # no Q-oracle call, only a single one to A - break - - counts = { - k: round(v * shots) - for k, v in ret.quasi_dists[0].binary_probabilities().items() - } - - # calculate the probability of measuring '1', 'prob' is a_i in the paper - num_qubits = circuit.num_qubits - circuit.num_ancillas - # type: ignore - one_counts, prob = self._good_state_probability( - estimation_problem, counts, num_qubits - ) - - num_one_shots.append(one_counts) - - # track number of Q-oracle calls - num_oracle_queries += shots * k - - # if on the previous iterations we have K_{i-1} == K_i, we sum these samples up - j = 1 # number of times we stayed fixed at the same K - round_shots = shots - round_one_counts = one_counts - if num_iterations > 1: - while ( - powers[num_iterations - j] == powers[num_iterations] - and num_iterations >= j + 1 - ): - j = j + 1 - round_shots += shots - round_one_counts += num_one_shots[-j] - - # compute a_min_i, a_max_i - if self._confint_method == "chernoff": - a_i_min, a_i_max = _chernoff_confint(prob, round_shots, max_rounds, self._alpha) - else: # 'beta' - a_i_min, a_i_max = _clopper_pearson_confint( - round_one_counts, round_shots, self._alpha / max_rounds - ) - - # compute theta_min_i, theta_max_i - if upper_half_circle: - theta_min_i = np.arccos(1 - 2 * a_i_min) / 2 / np.pi - theta_max_i = np.arccos(1 - 2 * a_i_max) / 2 / np.pi - else: - theta_min_i = 1 - np.arccos(1 - 2 * a_i_max) / 2 / np.pi - theta_max_i = 1 - np.arccos(1 - 2 * a_i_min) / 2 / np.pi - - # compute theta_u, theta_l of this iteration - scaling = 4 * k + 2 # current K_i factor - theta_u = (int(scaling * theta_intervals[-1][1]) + theta_max_i) / scaling - theta_l = (int(scaling * theta_intervals[-1][0]) + theta_min_i) / scaling - theta_intervals.append([theta_l, theta_u]) - - # compute a_u_i, a_l_i - a_u = np.sin(2 * np.pi * theta_u) ** 2 - a_l = np.sin(2 * np.pi * theta_l) ** 2 - a_u = cast(float, a_u) - a_l = cast(float, a_l) - a_intervals.append([a_l, a_u]) - - # get the latest confidence interval for the estimate of a - confidence_interval = tuple(a_intervals[-1]) - - # the final estimate is the mean of the confidence interval - estimation = np.mean(confidence_interval) - - result = IterativeAmplitudeEstimationResult() - result.alpha = self._alpha - result.post_processing = estimation_problem.post_processing - result.num_oracle_queries = num_oracle_queries - - result.estimation = estimation - result.epsilon_estimated = (confidence_interval[1] - confidence_interval[0]) / 2 - result.confidence_interval = confidence_interval - - result.estimation_processed = estimation_problem.post_processing(estimation) - confidence_interval = tuple( - estimation_problem.post_processing(x) for x in confidence_interval - ) - result.confidence_interval_processed = confidence_interval - result.epsilon_estimated_processed = (confidence_interval[1] - confidence_interval[0]) / 2 - result.estimate_intervals = a_intervals - result.theta_intervals = theta_intervals - result.powers = powers - result.ratios = ratios - - return result - - -class IterativeAmplitudeEstimationResult(AmplitudeEstimatorResult): - """The ``IterativeAmplitudeEstimation`` result object.""" - - def __init__(self) -> None: - super().__init__() - self._alpha: float | None = None - self._epsilon_target: float | None = None - self._epsilon_estimated: float | None = None - self._epsilon_estimated_processed: float | None = None - self._estimate_intervals: list[list[float]] | None = None - self._theta_intervals: list[list[float]] | None = None - self._powers: list[int] | None = None - self._ratios: list[float] | None = None - self._confidence_interval_processed: tuple[float, float] | None = None - - @property - def alpha(self) -> float: - r"""Return the confidence level :math:`\alpha`.""" - return self._alpha - - @alpha.setter - def alpha(self, value: float) -> None: - r"""Set the confidence level :math:`\alpha`.""" - self._alpha = value - - @property - def epsilon_target(self) -> float: - """Return the target half-width of the confidence interval.""" - return self._epsilon_target - - @epsilon_target.setter - def epsilon_target(self, value: float) -> None: - """Set the target half-width of the confidence interval.""" - self._epsilon_target = value - - @property - def epsilon_estimated(self) -> float: - """Return the estimated half-width of the confidence interval.""" - return self._epsilon_estimated - - @epsilon_estimated.setter - def epsilon_estimated(self, value: float) -> None: - """Set the estimated half-width of the confidence interval.""" - self._epsilon_estimated = value - - @property - def epsilon_estimated_processed(self) -> float: - """Return the post-processed estimated half-width of the confidence interval.""" - return self._epsilon_estimated_processed - - @epsilon_estimated_processed.setter - def epsilon_estimated_processed(self, value: float) -> None: - """Set the post-processed estimated half-width of the confidence interval.""" - self._epsilon_estimated_processed = value - - @property - def estimate_intervals(self) -> list[list[float]]: - """Return the confidence intervals for the estimate in each iteration.""" - return self._estimate_intervals - - @estimate_intervals.setter - def estimate_intervals(self, value: list[list[float]]) -> None: - """Set the confidence intervals for the estimate in each iteration.""" - self._estimate_intervals = value - - @property - def theta_intervals(self) -> list[list[float]]: - """Return the confidence intervals for the angles in each iteration.""" - return self._theta_intervals - - @theta_intervals.setter - def theta_intervals(self, value: list[list[float]]) -> None: - """Set the confidence intervals for the angles in each iteration.""" - self._theta_intervals = value - - @property - def powers(self) -> list[int]: - """Return the powers of the Grover operator in each iteration.""" - return self._powers - - @powers.setter - def powers(self, value: list[int]) -> None: - """Set the powers of the Grover operator in each iteration.""" - self._powers = value - - @property - def ratios(self) -> list[float]: - r"""Return the ratios :math:`K_{i+1}/K_{i}` for each iteration :math:`i`.""" - return self._ratios - - @ratios.setter - def ratios(self, value: list[float]) -> None: - r"""Set the ratios :math:`K_{i+1}/K_{i}` for each iteration :math:`i`.""" - self._ratios = value - - @property - def confidence_interval_processed(self) -> tuple[float, float]: - """Return the post-processed confidence interval.""" - return self._confidence_interval_processed - - @confidence_interval_processed.setter - def confidence_interval_processed(self, value: tuple[float, float]) -> None: - """Set the post-processed confidence interval.""" - self._confidence_interval_processed = value - - -def _chernoff_confint( - value: float, shots: int, max_rounds: int, alpha: float -) -> tuple[float, float]: - """Compute the Chernoff confidence interval for `shots` i.i.d. Bernoulli trials. - - The confidence interval is - - [value - eps, value + eps], where eps = sqrt(3 * log(2 * max_rounds/ alpha) / shots) - - but at most [0, 1]. - - Args: - value: The current estimate. - shots: The number of shots. - max_rounds: The maximum number of rounds, used to compute epsilon_a. - alpha: The confidence level, used to compute epsilon_a. - - Returns: - The Chernoff confidence interval. - """ - eps = np.sqrt(3 * np.log(2 * max_rounds / alpha) / shots) - lower = np.maximum(0, value - eps) - upper = np.minimum(1, value + eps) - return lower, upper - - -def _clopper_pearson_confint(counts: int, shots: int, alpha: float) -> tuple[float, float]: - """Compute the Clopper-Pearson confidence interval for `shots` i.i.d. Bernoulli trials. - - Args: - counts: The number of positive counts. - shots: The number of shots. - alpha: The confidence level for the confidence interval. - - Returns: - The Clopper-Pearson confidence interval. - """ - lower, upper = 0, 1 - - # if counts == 0, the beta quantile returns nan - if counts != 0: - lower = beta.ppf(alpha / 2, counts, shots - counts + 1) - - # if counts == shots, the beta quantile returns nan - if counts != shots: - upper = beta.ppf(1 - alpha / 2, counts + 1, shots - counts) - - return lower, upper diff --git a/qiskit/algorithms/amplitude_estimators/mlae.py b/qiskit/algorithms/amplitude_estimators/mlae.py deleted file mode 100644 index 841714f92210..000000000000 --- a/qiskit/algorithms/amplitude_estimators/mlae.py +++ /dev/null @@ -1,670 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2018, 2022. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. - -"""The Maximum Likelihood Amplitude Estimation algorithm.""" - -from __future__ import annotations -import warnings -from collections.abc import Sequence -from typing import Callable, List, Tuple - -import numpy as np -from scipy.optimize import brute -from scipy.stats import norm, chi2 - -from qiskit.providers import Backend -from qiskit import ClassicalRegister, QuantumRegister, QuantumCircuit -from qiskit.utils import QuantumInstance -from qiskit.primitives import BaseSampler -from qiskit.utils.deprecation import deprecate_arg, deprecate_func - -from .amplitude_estimator import AmplitudeEstimator, AmplitudeEstimatorResult -from .estimation_problem import EstimationProblem -from ..exceptions import AlgorithmError - -MINIMIZER = Callable[[Callable[[float], float], List[Tuple[float, float]]], float] - - -class MaximumLikelihoodAmplitudeEstimation(AmplitudeEstimator): - """The Maximum Likelihood Amplitude Estimation algorithm. - - This class implements the quantum amplitude estimation (QAE) algorithm without phase - estimation, as introduced in [1]. In comparison to the original QAE algorithm [2], - this implementation relies solely on different powers of the Grover operator and does not - require additional evaluation qubits. - Finally, the estimate is determined via a maximum likelihood estimation, which is why this - class in named ``MaximumLikelihoodAmplitudeEstimation``. - - References: - [1]: Suzuki, Y., Uno, S., Raymond, R., Tanaka, T., Onodera, T., & Yamamoto, N. (2019). - Amplitude Estimation without Phase Estimation. - `arXiv:1904.10246 `_. - [2]: Brassard, G., Hoyer, P., Mosca, M., & Tapp, A. (2000). - Quantum Amplitude Amplification and Estimation. - `arXiv:quant-ph/0005055 `_. - """ - - @deprecate_arg( - "quantum_instance", - additional_msg=( - "Instead, use the ``sampler`` argument. See https://qisk.it/algo_migration for a " - "migration guide." - ), - since="0.24.0", - package_name="qiskit-terra", - ) - def __init__( - self, - evaluation_schedule: list[int] | int, - minimizer: MINIMIZER | None = None, - quantum_instance: QuantumInstance | Backend | None = None, - sampler: BaseSampler | None = None, - ) -> None: - r""" - Args: - evaluation_schedule: If a list, the powers applied to the Grover operator. The list - element must be non-negative. If a non-negative integer, an exponential schedule is - used where the highest power is 2 to the integer minus 1: - `[id, Q^2^0, ..., Q^2^(evaluation_schedule-1)]`. - minimizer: A minimizer used to find the minimum of the likelihood function. - Defaults to a brute search where the number of evaluation points is determined - according to ``evaluation_schedule``. The minimizer takes a function as first - argument and a list of (float, float) tuples (as bounds) as second argument and - returns a single float which is the found minimum. - quantum_instance: Deprecated: Quantum Instance or Backend - sampler: A sampler primitive to evaluate the circuits. - - Raises: - ValueError: If the number of oracle circuits is smaller than 1. - """ - - super().__init__() - - # set quantum instance - with warnings.catch_warnings(): - warnings.simplefilter("ignore") - self.quantum_instance = quantum_instance - - # get parameters - if isinstance(evaluation_schedule, int): - if evaluation_schedule < 0: - raise ValueError("The evaluation schedule cannot be < 0.") - - self._evaluation_schedule = [0] + [2**j for j in range(evaluation_schedule)] - else: - if any(value < 0 for value in evaluation_schedule): - raise ValueError("The elements of the evaluation schedule cannot be < 0.") - - self._evaluation_schedule = evaluation_schedule - - if minimizer is None: - # default number of evaluations is max(10^4, pi/2 * 10^3 * 2^(m)) - nevals = max(10000, int(np.pi / 2 * 1000 * 2 * self._evaluation_schedule[-1])) - - def default_minimizer(objective_fn, bounds): - return brute(objective_fn, bounds, Ns=nevals)[0] - - self._minimizer = default_minimizer - else: - self._minimizer = minimizer - - self._sampler = sampler - - @property - def sampler(self) -> BaseSampler | None: - """Get the sampler primitive. - - Returns: - The sampler primitive to evaluate the circuits. - """ - return self._sampler - - @sampler.setter - def sampler(self, sampler: BaseSampler) -> None: - """Set sampler primitive. - - Args: - sampler: A sampler primitive to evaluate the circuits. - """ - self._sampler = sampler - - @property - @deprecate_func( - since="0.24.0", - package_name="qiskit-terra", - is_property=True, - additional_msg="See https://qisk.it/algo_migration for a migration guide.", - ) - def quantum_instance(self) -> QuantumInstance | None: - """Deprecated. Get the quantum instance. - - Returns: - The quantum instance used to run this algorithm. - """ - return self._quantum_instance - - @quantum_instance.setter - @deprecate_func( - since="0.24.0", - package_name="qiskit-terra", - is_property=True, - additional_msg="See https://qisk.it/algo_migration for a migration guide.", - ) - def quantum_instance(self, quantum_instance: QuantumInstance | Backend) -> None: - """Deprecated. Set quantum instance. - - Args: - quantum_instance: The quantum instance used to run this algorithm. - """ - if isinstance(quantum_instance, Backend): - quantum_instance = QuantumInstance(quantum_instance) - self._quantum_instance = quantum_instance - - def construct_circuits( - self, estimation_problem: EstimationProblem, measurement: bool = False - ) -> list[QuantumCircuit]: - """Construct the Amplitude Estimation w/o QPE quantum circuits. - - Args: - estimation_problem: The estimation problem for which to construct the QAE circuit. - measurement: Boolean flag to indicate if measurement should be included in the circuits. - - Returns: - A list with the QuantumCircuit objects for the algorithm. - """ - # keep track of the Q-oracle queries - circuits = [] - - num_qubits = max( - estimation_problem.state_preparation.num_qubits, - estimation_problem.grover_operator.num_qubits, - ) - q = QuantumRegister(num_qubits, "q") - qc_0 = QuantumCircuit(q, name="qc_a") # 0 applications of Q, only a single A operator - - # add classical register if needed - if measurement: - c = ClassicalRegister(len(estimation_problem.objective_qubits)) - qc_0.add_register(c) - - qc_0.compose(estimation_problem.state_preparation, inplace=True) - - for k in self._evaluation_schedule: - qc_k = qc_0.copy(name=f"qc_a_q_{k}") - - if k != 0: - qc_k.compose(estimation_problem.grover_operator.power(k), inplace=True) - - if measurement: - # real hardware can currently not handle operations after measurements, - # which might happen if the circuit gets transpiled, hence we're adding - # a safeguard-barrier - qc_k.barrier() - qc_k.measure(estimation_problem.objective_qubits, c[:]) - - circuits += [qc_k] - - return circuits - - @staticmethod - def compute_confidence_interval( - result: "MaximumLikelihoodAmplitudeEstimationResult", - alpha: float, - kind: str = "fisher", - apply_post_processing: bool = False, - ) -> tuple[float, float]: - """Compute the `alpha` confidence interval using the method `kind`. - - The confidence level is (1 - `alpha`) and supported kinds are 'fisher', - 'likelihood_ratio' and 'observed_fisher' with shorthand - notations 'fi', 'lr' and 'oi', respectively. - - Args: - result: A maximum likelihood amplitude estimation result. - alpha: The confidence level. - kind: The method to compute the confidence interval. Defaults to 'fisher', which - computes the theoretical Fisher information. - apply_post_processing: If True, apply post-processing to the confidence interval. - - Returns: - The specified confidence interval. - - Raises: - AlgorithmError: If `run()` hasn't been called yet. - NotImplementedError: If the method `kind` is not supported. - """ - interval: tuple[float, float] | None = None - - # if statevector simulator the estimate is exact - if all(isinstance(data, (list, np.ndarray)) for data in result.circuit_results): - interval = (result.estimation, result.estimation) - - elif kind in ["likelihood_ratio", "lr"]: - interval = _likelihood_ratio_confint(result, alpha) - - elif kind in ["fisher", "fi"]: - interval = _fisher_confint(result, alpha, observed=False) - - elif kind in ["observed_fisher", "observed_information", "oi"]: - interval = _fisher_confint(result, alpha, observed=True) - - if interval is None: - raise NotImplementedError(f"CI `{kind}` is not implemented.") - - if apply_post_processing: - return result.post_processing(interval[0]), result.post_processing(interval[1]) - - return interval - - def compute_mle( - self, - circuit_results: list[dict[str, int] | np.ndarray], - estimation_problem: EstimationProblem, - num_state_qubits: int | None = None, - return_counts: bool = False, - ) -> float | tuple[float, list[float]]: - """Compute the MLE via a grid-search. - - This is a stable approach if sufficient gridpoints are used. - - Args: - circuit_results: A list of circuit outcomes. Can be counts or statevectors. - estimation_problem: The estimation problem containing the evaluation schedule and the - number of likelihood function evaluations used to find the minimum. - num_state_qubits: The number of state qubits, required for statevector simulations. - return_counts: If True, returns the good counts. - - Returns: - The MLE for the provided result object. - """ - good_counts, all_counts = _get_counts(circuit_results, estimation_problem, num_state_qubits) - - # search range - eps = 1e-15 # to avoid invalid value in log - search_range = [0 + eps, np.pi / 2 - eps] - - def loglikelihood(theta): - # loglik contains the first `it` terms of the full loglikelihood - loglik = 0 - for i, k in enumerate(self._evaluation_schedule): - angle = (2 * k + 1) * theta - loglik += np.log(np.sin(angle) ** 2) * good_counts[i] - loglik += np.log(np.cos(angle) ** 2) * (all_counts[i] - good_counts[i]) - return -loglik - - est_theta = self._minimizer(loglikelihood, [search_range]) - - if return_counts: - return est_theta, good_counts - return est_theta - - def estimate( - self, estimation_problem: EstimationProblem - ) -> "MaximumLikelihoodAmplitudeEstimationResult": - """Run the amplitude estimation algorithm on provided estimation problem. - - Args: - estimation_problem: The estimation problem. - - Returns: - An amplitude estimation results object. - - Raises: - ValueError: A quantum instance or Sampler must be provided. - AlgorithmError: If `state_preparation` is not set in - `estimation_problem`. - AlgorithmError: Sampler job run error - """ - if self._quantum_instance is None and self._sampler is None: - raise ValueError("A quantum instance or sampler must be provided.") - if estimation_problem.state_preparation is None: - raise AlgorithmError( - "The state_preparation property of the estimation problem must be set." - ) - - result = MaximumLikelihoodAmplitudeEstimationResult() - result.evaluation_schedule = self._evaluation_schedule - result.minimizer = self._minimizer - result.post_processing = estimation_problem.post_processing - - shots = 0 - if self._quantum_instance is not None and self._quantum_instance.is_statevector: - # run circuit on statevector simulator - circuits = self.construct_circuits(estimation_problem, measurement=False) - ret = self._quantum_instance.execute(circuits) - - # get statevectors and construct MLE input - statevectors = [np.asarray(ret.get_statevector(circuit)) for circuit in circuits] - result.circuit_results = statevectors - - # to count the number of Q-oracle calls (don't count shots) - result.shots = 1 - else: - circuits = self.construct_circuits(estimation_problem, measurement=True) - if self._quantum_instance is not None: - # run circuit on QASM simulator - ret = self._quantum_instance.execute(circuits) - # get counts and construct MLE input - result.circuit_results = [ret.get_counts(circuit) for circuit in circuits] - shots = self._quantum_instance._run_config.shots - else: - try: - job = self._sampler.run(circuits) - ret = job.result() - except Exception as exc: - raise AlgorithmError("The job was not completed successfully. ") from exc - - result.circuit_results = [] - shots = ret.metadata[0].get("shots") - if shots is None: - for quasi_dist in ret.quasi_dists: - circuit_result = quasi_dist.binary_probabilities() - result.circuit_results.append(circuit_result) - shots = 1 - else: - # get counts and construct MLE input - for quasi_dist in ret.quasi_dists: - counts = { - k: round(v * shots) - for k, v in quasi_dist.binary_probabilities().items() - } - result.circuit_results.append(counts) - - result.shots = shots - - # run maximum likelihood estimation - num_state_qubits = circuits[0].num_qubits - circuits[0].num_ancillas - theta, good_counts = self.compute_mle( - result.circuit_results, estimation_problem, num_state_qubits, True - ) - - # store results - result.theta = theta - result.good_counts = good_counts - result.estimation = np.sin(result.theta) ** 2 - - # not sure why pylint complains, this is a callable and the tests pass - # pylint: disable=not-callable - result.estimation_processed = result.post_processing(result.estimation) - - result.fisher_information = _compute_fisher_information(result) - result.num_oracle_queries = result.shots * sum(k for k in result.evaluation_schedule) - - # compute and store confidence interval - confidence_interval = self.compute_confidence_interval(result, alpha=0.05, kind="fisher") - result.confidence_interval = confidence_interval - result.confidence_interval_processed = tuple( - estimation_problem.post_processing(value) for value in confidence_interval - ) - - return result - - -class MaximumLikelihoodAmplitudeEstimationResult(AmplitudeEstimatorResult): - """The ``MaximumLikelihoodAmplitudeEstimation`` result object.""" - - def __init__(self) -> None: - super().__init__() - self._theta: float | None = None - self._minimizer: Callable | None = None - self._good_counts: list[float] | None = None - self._evaluation_schedule: list[int] | None = None - self._fisher_information: float | None = None - - @property - def theta(self) -> float: - r"""Return the estimate for the angle :math:`\theta`.""" - return self._theta - - @theta.setter - def theta(self, value: float) -> None: - r"""Set the estimate for the angle :math:`\theta`.""" - self._theta = value - - @property - def minimizer(self) -> Callable: - """Return the minimizer used for the search of the likelihood function.""" - return self._minimizer - - @minimizer.setter - def minimizer(self, value: Callable) -> None: - """Set the number minimizer used for the search of the likelihood function.""" - self._minimizer = value - - @property - def good_counts(self) -> list[float]: - """Return the percentage of good counts per circuit power.""" - return self._good_counts - - @good_counts.setter - def good_counts(self, counts: list[float]) -> None: - """Set the percentage of good counts per circuit power.""" - self._good_counts = counts - - @property - def evaluation_schedule(self) -> list[int]: - """Return the evaluation schedule for the powers of the Grover operator.""" - return self._evaluation_schedule - - @evaluation_schedule.setter - def evaluation_schedule(self, evaluation_schedule: list[int]) -> None: - """Set the evaluation schedule for the powers of the Grover operator.""" - self._evaluation_schedule = evaluation_schedule - - @property - def fisher_information(self) -> float: - """Return the Fisher information for the estimated amplitude.""" - return self._fisher_information - - @fisher_information.setter - def fisher_information(self, value: float) -> None: - """Set the Fisher information for the estimated amplitude.""" - self._fisher_information = value - - -def _safe_min(array, default=0): - if len(array) == 0: - return default - return np.min(array) - - -def _safe_max(array, default=(np.pi / 2)): - if len(array) == 0: - return default - return np.max(array) - - -def _compute_fisher_information( - result: "MaximumLikelihoodAmplitudeEstimationResult", - num_sum_terms: int | None = None, - observed: bool = False, -) -> float: - """Compute the Fisher information. - - Args: - result: A maximum likelihood amplitude estimation result. - num_sum_terms: The number of sum terms to be included in the calculation of the - Fisher information. By default all values are included. - observed: If True, compute the observed Fisher information, otherwise the theoretical - one. - - Returns: - The computed Fisher information, or np.inf if statevector simulation was used. - - Raises: - KeyError: Call run() first! - """ - a = result.estimation - - # Corresponding angle to the value a (only use real part of 'a') - theta_a = np.arcsin(np.sqrt(np.real(a))) - - # Get the number of hits (shots_k) and one-hits (h_k) - one_hits = result.good_counts - all_hits = [result.shots] * len(one_hits) - - # Include all sum terms or just up to a certain term? - evaluation_schedule = result.evaluation_schedule - if num_sum_terms is not None: - evaluation_schedule = evaluation_schedule[:num_sum_terms] - # not necessary since zip goes as far as shortest list: - # all_hits = all_hits[:num_sum_terms] - # one_hits = one_hits[:num_sum_terms] - - # Compute the Fisher information - fisher_information = None - if observed: - # Note, that the observed Fisher information is very unreliable in this algorithm! - d_loglik = 0 - for shots_k, h_k, m_k in zip(all_hits, one_hits, evaluation_schedule): - tan = np.tan((2 * m_k + 1) * theta_a) - d_loglik += (2 * m_k + 1) * (h_k / tan + (shots_k - h_k) * tan) - - d_loglik /= np.sqrt(a * (1 - a)) - fisher_information = d_loglik**2 / len(all_hits) - - else: - fisher_information = sum( - shots_k * (2 * m_k + 1) ** 2 for shots_k, m_k in zip(all_hits, evaluation_schedule) - ) - fisher_information /= a * (1 - a) - - return fisher_information - - -def _fisher_confint( - result: MaximumLikelihoodAmplitudeEstimationResult, alpha: float = 0.05, observed: bool = False -) -> tuple[float, float]: - """Compute the `alpha` confidence interval based on the Fisher information. - - Args: - result: A maximum likelihood amplitude estimation results object. - alpha: The level of the confidence interval (must be <= 0.5), default to 0.05. - observed: If True, use observed Fisher information. - - Returns: - float: The alpha confidence interval based on the Fisher information - Raises: - AssertionError: Call run() first! - """ - # Get the (observed) Fisher information - fisher_information = None - try: - fisher_information = result.fisher_information - except KeyError as ex: - raise AssertionError("Call run() first!") from ex - - if observed: - fisher_information = _compute_fisher_information(result, observed=True) - - normal_quantile = norm.ppf(1 - alpha / 2) - confint = np.real(result.estimation) + normal_quantile / np.sqrt(fisher_information) * np.array( - [-1, 1] - ) - return result.post_processing(confint[0]), result.post_processing(confint[1]) - - -def _likelihood_ratio_confint( - result: MaximumLikelihoodAmplitudeEstimationResult, - alpha: float = 0.05, - nevals: int | None = None, -) -> tuple[float, float]: - """Compute the likelihood-ratio confidence interval. - - Args: - result: A maximum likelihood amplitude estimation results object. - alpha: The level of the confidence interval (< 0.5), defaults to 0.05. - nevals: The number of evaluations to find the intersection with the loglikelihood - function. Defaults to an adaptive value based on the maximal power of Q. - - Returns: - The alpha-likelihood-ratio confidence interval. - """ - if nevals is None: - nevals = max(10000, int(np.pi / 2 * 1000 * 2 * result.evaluation_schedule[-1])) - - def loglikelihood(theta, one_counts, all_counts): - loglik = 0 - for i, k in enumerate(result.evaluation_schedule): - loglik += np.log(np.sin((2 * k + 1) * theta) ** 2) * one_counts[i] - loglik += np.log(np.cos((2 * k + 1) * theta) ** 2) * (all_counts[i] - one_counts[i]) - return loglik - - one_counts = result.good_counts - all_counts = [result.shots] * len(one_counts) - - eps = 1e-15 # to avoid invalid value in log - thetas = np.linspace(0 + eps, np.pi / 2 - eps, nevals) - values = np.zeros(len(thetas)) - for i, theta in enumerate(thetas): - values[i] = loglikelihood(theta, one_counts, all_counts) - - loglik_mle = loglikelihood(result.theta, one_counts, all_counts) - chi2_quantile = chi2.ppf(1 - alpha, df=1) - thres = loglik_mle - chi2_quantile / 2 - - # the (outer) LR confidence interval - above_thres = thetas[values >= thres] - - # it might happen that the `above_thres` array is empty, - # to still provide a valid result use safe_min/max which - # then yield [0, pi/2] - confint = [_safe_min(above_thres, default=0), _safe_max(above_thres, default=np.pi / 2)] - mapped_confint = tuple(result.post_processing(np.sin(bound) ** 2) for bound in confint) - - return mapped_confint - - -def _get_counts( - circuit_results: Sequence[np.ndarray | list[float] | dict[str, int]], - estimation_problem: EstimationProblem, - num_state_qubits: int, -) -> tuple[list[float], list[int]]: - """Get the good and total counts. - - Returns: - A pair of two lists, ([1-counts per experiment], [shots per experiment]). - - Raises: - AlgorithmError: If self.run() has not been called yet. - """ - one_hits = [] # h_k: how often 1 has been measured, for a power Q^(m_k) - # shots_k: how often has been measured at a power Q^(m_k) - all_hits: np.ndarray | list[float] = [] - if all(isinstance(data, (list, np.ndarray)) for data in circuit_results): - probabilities = [] - num_qubits = int(np.log2(len(circuit_results[0]))) # the total number of qubits - for statevector in circuit_results: - p_k = 0.0 - for i, amplitude in enumerate(statevector): - probability = np.abs(amplitude) ** 2 - # consider only state qubits and revert bit order - bitstr = bin(i)[2:].zfill(num_qubits)[-num_state_qubits:][::-1] - objectives = [bitstr[index] for index in estimation_problem.objective_qubits] - if estimation_problem.is_good_state(objectives): - p_k += probability - probabilities += [p_k] - - one_hits = probabilities - all_hits = np.ones_like(one_hits) - else: - for counts in circuit_results: - all_hits.append(sum(counts.values())) - one_hits.append( - sum( - count - for bitstr, count in counts.items() - if estimation_problem.is_good_state(bitstr) - ) - ) - - return one_hits, all_hits diff --git a/qiskit/algorithms/aux_ops_evaluator.py b/qiskit/algorithms/aux_ops_evaluator.py deleted file mode 100644 index 308d7ca6cb65..000000000000 --- a/qiskit/algorithms/aux_ops_evaluator.py +++ /dev/null @@ -1,196 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2021, 2022. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. -"""Evaluator of auxiliary operators for algorithms.""" - -from __future__ import annotations - -import numpy as np - -from qiskit import QuantumCircuit -from qiskit.opflow import ( - CircuitSampler, - ListOp, - StateFn, - OperatorBase, - ExpectationBase, -) -from qiskit.providers import Backend -from qiskit.quantum_info import Statevector -from qiskit.utils import QuantumInstance -from qiskit.utils.deprecation import deprecate_func - -from .list_or_dict import ListOrDict - - -@deprecate_func( - additional_msg=( - "Instead, use the function " - "``qiskit.algorithms.observables_evaluator.estimate_observables``. See " - "https://qisk.it/algo_migration for a migration guide." - ), - since="0.24.0", - package_name="qiskit-terra", -) -def eval_observables( - quantum_instance: QuantumInstance | Backend, - quantum_state: Statevector | QuantumCircuit | OperatorBase, - observables: ListOrDict[OperatorBase], - expectation: ExpectationBase, - threshold: float = 1e-12, -) -> ListOrDict[tuple[complex, complex]]: - """ - Deprecated: Accepts a list or a dictionary of operators and calculates - their expectation values - means - and standard deviations. They are calculated with respect to a quantum state provided. A user - can optionally provide a threshold value which filters mean values falling below the threshold. - - This function has been superseded by the - :func:`qiskit.algorithms.observables_evaluator.eval_observables` function. - It will be deprecated in a future release and subsequently - removed after that. - - Args: - quantum_instance: A quantum instance used for calculations. - quantum_state: An unparametrized quantum circuit representing a quantum state that - expectation values are computed against. - observables: A list or a dictionary of operators whose expectation values are to be - calculated. - expectation: An instance of ExpectationBase which defines a method for calculating - expectation values. - threshold: A threshold value that defines which mean values should be neglected (helpful for - ignoring numerical instabilities close to 0). - - Returns: - A list or a dictionary of tuples (mean, standard deviation). - - Raises: - ValueError: If a ``quantum_state`` with free parameters is provided. - """ - - if ( - isinstance( - quantum_state, (QuantumCircuit, OperatorBase) - ) # Statevector cannot be parametrized - and len(quantum_state.parameters) > 0 - ): - raise ValueError( - "A parametrized representation of a quantum_state was provided. It is not " - "allowed - it cannot have free parameters." - ) - - # Create new CircuitSampler to avoid breaking existing one's caches. - sampler = CircuitSampler(quantum_instance) - - list_op = _prepare_list_op(quantum_state, observables) - observables_expect = expectation.convert(list_op) - observables_expect_sampled = sampler.convert(observables_expect) - - # compute means - values = np.real(observables_expect_sampled.eval()) - - # compute standard deviations - # We use sampler.quantum_instance to take care of case in which quantum_instance is Backend - std_devs = _compute_std_devs( - observables_expect_sampled, observables, expectation, sampler.quantum_instance - ) - - # Discard values below threshold - observables_means = values * (np.abs(values) > threshold) - # zip means and standard deviations into tuples - observables_results = list(zip(observables_means, std_devs)) - - # Return None eigenvalues for None operators if observables is a list. - # None operators are already dropped in compute_minimum_eigenvalue if observables is a dict. - - return _prepare_result(observables_results, observables) - - -def _prepare_list_op( - quantum_state: Statevector | QuantumCircuit | OperatorBase, - observables: ListOrDict[OperatorBase], -) -> ListOp: - """ - Accepts a list or a dictionary of operators and converts them to a ``ListOp``. - - Args: - quantum_state: An unparametrized quantum circuit representing a quantum state that - expectation values are computed against. - observables: A list or a dictionary of operators. - - Returns: - A ``ListOp`` that includes all provided observables. - """ - if isinstance(observables, dict): - observables = list(observables.values()) - - if not isinstance(quantum_state, StateFn): - quantum_state = StateFn(quantum_state) - - return ListOp([StateFn(obs, is_measurement=True).compose(quantum_state) for obs in observables]) - - -def _prepare_result( - observables_results: list[tuple[complex, complex]], - observables: ListOrDict[OperatorBase], -) -> ListOrDict[tuple[complex, complex]]: - """ - Prepares a list or a dictionary of eigenvalues from ``observables_results`` and - ``observables``. - - Args: - observables_results: A list of of tuples (mean, standard deviation). - observables: A list or a dictionary of operators whose expectation values are to be - calculated. - - Returns: - A list or a dictionary of tuples (mean, standard deviation). - """ - if isinstance(observables, list): - observables_eigenvalues: ListOrDict[tuple[complex, complex]] = [None] * len(observables) - key_value_iterator = enumerate(observables_results) - else: - observables_eigenvalues = {} - key_value_iterator = zip(observables.keys(), observables_results) - for key, value in key_value_iterator: - if observables[key] is not None: - observables_eigenvalues[key] = value - return observables_eigenvalues - - -def _compute_std_devs( - observables_expect_sampled: OperatorBase, - observables: ListOrDict[OperatorBase], - expectation: ExpectationBase, - quantum_instance: QuantumInstance | Backend, -) -> list[complex]: - """ - Calculates a list of standard deviations from expectation values of observables provided. - - Args: - observables_expect_sampled: Expected values of observables. - observables: A list or a dictionary of operators whose expectation values are to be - calculated. - expectation: An instance of ExpectationBase which defines a method for calculating - expectation values. - quantum_instance: A quantum instance used for calculations. - - Returns: - A list of standard deviations. - """ - variances = np.real(expectation.compute_variance(observables_expect_sampled)) - if not isinstance(variances, np.ndarray) and variances == 0.0: - # when `variances` is a single value equal to 0., our expectation value is exact and we - # manually ensure the variances to be a list of the correct length - variances = np.zeros(len(observables), dtype=float) - # TODO: this will crash if quantum_instance is a backend - std_devs = np.sqrt(variances / quantum_instance.run_config.shots) - return std_devs diff --git a/qiskit/algorithms/eigen_solvers/__init__.py b/qiskit/algorithms/eigen_solvers/__init__.py deleted file mode 100644 index 90cab015b8f5..000000000000 --- a/qiskit/algorithms/eigen_solvers/__init__.py +++ /dev/null @@ -1,19 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2020. -# -# 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. - -"""Eigen Solvers Package""" - -from .numpy_eigen_solver import NumPyEigensolver -from .eigen_solver import Eigensolver, EigensolverResult -from .vqd import VQD, VQDResult - -__all__ = ["NumPyEigensolver", "Eigensolver", "EigensolverResult", "VQD", "VQDResult"] diff --git a/qiskit/algorithms/eigen_solvers/eigen_solver.py b/qiskit/algorithms/eigen_solvers/eigen_solver.py deleted file mode 100644 index 5fd59b3e023c..000000000000 --- a/qiskit/algorithms/eigen_solvers/eigen_solver.py +++ /dev/null @@ -1,134 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2020, 2022. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. - -"""The Eigensolver interface""" -from __future__ import annotations -from abc import ABC, abstractmethod - -import numpy as np - -from qiskit.opflow import OperatorBase -from qiskit.utils.deprecation import deprecate_func -from ..algorithm_result import AlgorithmResult -from ..list_or_dict import ListOrDict - - -class Eigensolver(ABC): - """Deprecated: Eigensolver Interface. - - The Eigensolver interface has been superseded by the - :class:`qiskit.algorithms.eigensolvers.Eigensolver` interface. - This interface will be deprecated in a future release and subsequently - removed after that. - - Algorithms that can compute eigenvalues for an operator - may implement this interface to allow different algorithms to be - used interchangeably. - """ - - @deprecate_func( - additional_msg=( - "Instead, use the interface ``qiskit.algorithms.eigensolvers.Eigensolver``. See " - "https://qisk.it/algo_migration for a migration guide." - ), - since="0.24.0", - ) - def __init__(self) -> None: - pass - - @abstractmethod - def compute_eigenvalues( - self, operator: OperatorBase, aux_operators: ListOrDict[OperatorBase] | None = None - ) -> "EigensolverResult": - """ - Computes eigenvalues. Operator and aux_operators can be supplied here and - if not None will override any already set into algorithm so it can be reused with - different operators. While an operator is required by algorithms, aux_operators - are optional. To 'remove' a previous aux_operators array use an empty list here. - - Args: - operator: Qubit operator of the Observable - aux_operators: Optional list of auxiliary operators to be evaluated with the - eigenstate of the minimum eigenvalue main result and their expectation values - returned. For instance in chemistry these can be dipole operators, total particle - count operators so we can get values for these at the ground state. - - Returns: - EigensolverResult - """ - return EigensolverResult() - - @classmethod - def supports_aux_operators(cls) -> bool: - """Whether computing the expectation value of auxiliary operators is supported. - - Returns: - True if aux_operator expectations can be evaluated, False otherwise - """ - return False - - -class EigensolverResult(AlgorithmResult): - """Deprecated: Eigensolver Result. - - The EigensolverResult class has been superseded by the - :class:`qiskit.algorithms.eigensolvers.EigensolverResult` class. - This class will be deprecated in a future release and subsequently - removed after that. - - """ - - @deprecate_func( - additional_msg=( - "Instead, use the class ``qiskit.algorithms.eigensolvers.EigensolverResult``. " - "See https://qisk.it/algo_migration for a migration guide." - ), - since="0.24.0", - ) - def __init__(self) -> None: - super().__init__() - self._eigenvalues: np.ndarray | None = None - self._eigenstates: np.ndarray | None = None - self._aux_operator_eigenvalues: list[ListOrDict[tuple[complex, complex]]] | None = None - - @property - def eigenvalues(self) -> np.ndarray | None: - """returns eigen values""" - return self._eigenvalues - - @eigenvalues.setter - def eigenvalues(self, value: np.ndarray) -> None: - """set eigen values""" - self._eigenvalues = value - - @property - def eigenstates(self) -> np.ndarray | None: - """return eigen states""" - return self._eigenstates - - @eigenstates.setter - def eigenstates(self, value: np.ndarray) -> None: - """set eigen states""" - self._eigenstates = value - - @property - def aux_operator_eigenvalues(self) -> list[ListOrDict[tuple[complex, complex]]] | None: - """Return aux operator expectation values. - - These values are in fact tuples formatted as (mean, standard deviation). - """ - return self._aux_operator_eigenvalues - - @aux_operator_eigenvalues.setter - def aux_operator_eigenvalues(self, value: list[ListOrDict[tuple[complex, complex]]]) -> None: - """set aux operator eigen values""" - self._aux_operator_eigenvalues = value diff --git a/qiskit/algorithms/eigen_solvers/numpy_eigen_solver.py b/qiskit/algorithms/eigen_solvers/numpy_eigen_solver.py deleted file mode 100644 index 19433792f23f..000000000000 --- a/qiskit/algorithms/eigen_solvers/numpy_eigen_solver.py +++ /dev/null @@ -1,279 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2018, 2022. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. - -"""The Eigensolver algorithm.""" -from __future__ import annotations - -import logging -import warnings -from collections.abc import Callable - -import numpy as np -from scipy import sparse as scisparse - -from qiskit.opflow import I, ListOp, OperatorBase, StateFn -from qiskit.utils.validation import validate_min -from qiskit.utils.deprecation import deprecate_func -from ..exceptions import AlgorithmError -from .eigen_solver import Eigensolver, EigensolverResult -from ..list_or_dict import ListOrDict - -logger = logging.getLogger(__name__) - - -class NumPyEigensolver(Eigensolver): - r""" - Deprecated: NumPy Eigensolver algorithm. - - The NumPyEigensolver class has been superseded by the - :class:`qiskit.algorithms.eigensolvers.NumPyEigensolver` class. - This class will be deprecated in a future release and subsequently - removed after that. - - NumPy Eigensolver computes up to the first :math:`k` eigenvalues of a complex-valued square - matrix of dimension :math:`n \times n`, with :math:`k \leq n`. - - Note: - Operators are automatically converted to SciPy's ``spmatrix`` - as needed and this conversion can be costly in terms of memory and performance as the - operator size, mostly in terms of number of qubits it represents, gets larger. - """ - - @deprecate_func( - additional_msg=( - "Instead, use the class ``qiskit.algorithms.eigensolvers.NumPyEigensolver``. " - "See https://qisk.it/algo_migration for a migration guide." - ), - since="0.24.0", - package_name="qiskit-terra", - ) - def __init__( - self, - k: int = 1, - filter_criterion: Callable[ - [list | np.ndarray, float, ListOrDict[float] | None], bool - ] = None, - ) -> None: - """ - Args: - k: How many eigenvalues are to be computed, has a min. value of 1. - filter_criterion: callable that allows to filter eigenvalues/eigenstates, only feasible - eigenstates are returned in the results. The callable has the signature - `filter(eigenstate, eigenvalue, aux_values)` and must return a boolean to indicate - whether to keep this value in the final returned result or not. If the number of - elements that satisfies the criterion is smaller than `k` then the returned list has - fewer elements and can even be empty. - """ - validate_min("k", k, 1) - with warnings.catch_warnings(): - warnings.simplefilter("ignore") - super().__init__() - - self._in_k = k - self._k = k - - self._filter_criterion = filter_criterion - - self._ret = EigensolverResult() - - @property - def k(self) -> int: - """returns k (number of eigenvalues requested)""" - return self._in_k - - @k.setter - def k(self, k: int) -> None: - """set k (number of eigenvalues requested)""" - validate_min("k", k, 1) - self._in_k = k - self._k = k - - @property - def filter_criterion( - self, - ) -> Callable[[list | np.ndarray, float, ListOrDict[float] | None], bool] | None: - """returns the filter criterion if set""" - return self._filter_criterion - - @filter_criterion.setter - def filter_criterion( - self, - filter_criterion: Callable[[list | np.ndarray, float, ListOrDict[float] | None], bool] - | None, - ) -> None: - """set the filter criterion""" - self._filter_criterion = filter_criterion - - @classmethod - def supports_aux_operators(cls) -> bool: - return True - - def _check_set_k(self, operator: OperatorBase) -> None: - if operator is not None: - if self._in_k > 2**operator.num_qubits: - self._k = 2**operator.num_qubits - logger.debug( - "WARNING: Asked for %s eigenvalues but max possible is %s.", self._in_k, self._k - ) - else: - self._k = self._in_k - - def _solve(self, operator: OperatorBase) -> None: - sp_mat = operator.to_spmatrix() - # If matrix is diagonal, the elements on the diagonal are the eigenvalues. Solve by sorting. - if scisparse.csr_matrix(sp_mat.diagonal()).nnz == sp_mat.nnz: - diag = sp_mat.diagonal() - indices = np.argsort(diag)[: self._k] - eigval = diag[indices] - eigvec = np.zeros((sp_mat.shape[0], self._k)) - for i, idx in enumerate(indices): - eigvec[idx, i] = 1.0 - else: - if self._k >= 2**operator.num_qubits - 1: - logger.debug("SciPy doesn't support to get all eigenvalues, using NumPy instead.") - if operator.is_hermitian(): - eigval, eigvec = np.linalg.eigh(operator.to_matrix()) - else: - eigval, eigvec = np.linalg.eig(operator.to_matrix()) - else: - if operator.is_hermitian(): - eigval, eigvec = scisparse.linalg.eigsh(sp_mat, k=self._k, which="SA") - else: - eigval, eigvec = scisparse.linalg.eigs(sp_mat, k=self._k, which="SR") - indices = np.argsort(eigval)[: self._k] - eigval = eigval[indices] - eigvec = eigvec[:, indices] - self._ret.eigenvalues = eigval - self._ret.eigenstates = eigvec.T - - def _get_ground_state_energy(self, operator: OperatorBase) -> None: - if self._ret.eigenvalues is None or self._ret.eigenstates is None: - self._solve(operator) - - def _get_energies( - self, operator: OperatorBase, aux_operators: ListOrDict[OperatorBase] | None - ) -> None: - if self._ret.eigenvalues is None or self._ret.eigenstates is None: - self._solve(operator) - - if aux_operators is not None: - aux_op_vals = [] - for i in range(self._k): - aux_op_vals.append( - self._eval_aux_operators(aux_operators, self._ret.eigenstates[i]) - ) - self._ret.aux_operator_eigenvalues = aux_op_vals - - @staticmethod - def _eval_aux_operators( - aux_operators: ListOrDict[OperatorBase], wavefn, threshold: float = 1e-12 - ) -> ListOrDict[tuple[complex, complex]]: - - values: ListOrDict[tuple[complex, complex]] - - # As a list, aux_operators can contain None operators for which None values are returned. - # As a dict, the None operators in aux_operators have been dropped in compute_eigenvalues. - if isinstance(aux_operators, list): - values = [None] * len(aux_operators) - key_op_iterator = enumerate(aux_operators) - else: - values = {} - key_op_iterator = aux_operators.items() - for key, operator in key_op_iterator: - if operator is None: - continue - value = 0.0 - if operator.coeff != 0: - mat = operator.to_spmatrix() - # Terra doesn't support sparse yet, so do the matmul directly if so - # This is necessary for the particle_hole and other chemistry tests because the - # pauli conversions are 2^12th large and will OOM error if not sparse. - if isinstance(mat, scisparse.spmatrix): - value = mat.dot(wavefn).dot(np.conj(wavefn)) - else: - value = StateFn(operator, is_measurement=True).eval(wavefn) - value = value if np.abs(value) > threshold else 0.0 - # The value get's wrapped into a tuple: (mean, standard deviation). - # Since this is an exact computation, the standard deviation is known to be zero. - values[key] = (value, 0.0) - return values - - def compute_eigenvalues( - self, operator: OperatorBase, aux_operators: ListOrDict[OperatorBase] | None = None - ) -> EigensolverResult: - super().compute_eigenvalues(operator, aux_operators) - - if operator is None: - raise AlgorithmError("Operator was never provided") - - self._check_set_k(operator) - zero_op = I.tensorpower(operator.num_qubits) * 0.0 - if isinstance(aux_operators, list) and len(aux_operators) > 0: - # For some reason Chemistry passes aux_ops with 0 qubits and paulis sometimes. - aux_operators = [zero_op if op == 0 else op for op in aux_operators] - elif isinstance(aux_operators, dict) and len(aux_operators) > 0: - aux_operators = { - key: zero_op if op == 0 else op # Convert zero values to zero operators - for key, op in aux_operators.items() - if op is not None # Discard None values - } - else: - aux_operators = None - - k_orig = self._k - if self._filter_criterion: - # need to consider all elements if a filter is set - self._k = 2**operator.num_qubits - - self._ret = EigensolverResult() - self._solve(operator) - - # compute energies before filtering, as this also evaluates the aux operators - self._get_energies(operator, aux_operators) - - # if a filter is set, loop over the given values and only keep - if self._filter_criterion: - - eigvecs = [] - eigvals = [] - aux_ops = [] - cnt = 0 - for i in range(len(self._ret.eigenvalues)): - eigvec = self._ret.eigenstates[i] - eigval = self._ret.eigenvalues[i] - if self._ret.aux_operator_eigenvalues is not None: - aux_op = self._ret.aux_operator_eigenvalues[i] - else: - aux_op = None - if self._filter_criterion(eigvec, eigval, aux_op): - cnt += 1 - eigvecs += [eigvec] - eigvals += [eigval] - if self._ret.aux_operator_eigenvalues is not None: - aux_ops += [aux_op] - if cnt == k_orig: - break - - self._ret.eigenstates = np.array(eigvecs) - self._ret.eigenvalues = np.array(eigvals) - # conversion to np.array breaks in case of aux_ops - self._ret.aux_operator_eigenvalues = aux_ops - - self._k = k_orig - - # evaluate ground state after filtering (in case a filter is set) - self._get_ground_state_energy(operator) - if self._ret.eigenstates is not None: - self._ret.eigenstates = ListOp([StateFn(vec) for vec in self._ret.eigenstates]) - - logger.debug("EigensolverResult:\n%s", self._ret) - return self._ret diff --git a/qiskit/algorithms/eigen_solvers/vqd.py b/qiskit/algorithms/eigen_solvers/vqd.py deleted file mode 100644 index f9a1309e57d9..000000000000 --- a/qiskit/algorithms/eigen_solvers/vqd.py +++ /dev/null @@ -1,809 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2018, 2022. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. - -"""The Variational Quantum Deflation Algorithm for computing higher energy states. - -See https://arxiv.org/abs/1805.08138. -""" -from __future__ import annotations - -import logging -import warnings -from collections.abc import Callable -from time import time -import numpy as np - -from qiskit.circuit import QuantumCircuit, Parameter -from qiskit.circuit.library import RealAmplitudes -from qiskit.opflow.primitive_ops.pauli_op import PauliOp -from qiskit.providers import Backend -from qiskit.opflow import ( - OperatorBase, - ExpectationBase, - ExpectationFactory, - StateFn, - CircuitStateFn, - ListOp, - CircuitSampler, - PauliSumOp, -) -from qiskit.opflow.gradients import GradientBase -from qiskit.utils.validation import validate_min -from qiskit.utils.backend_utils import is_aer_provider -from qiskit.utils.deprecation import deprecate_func -from qiskit.utils import QuantumInstance -from ..list_or_dict import ListOrDict -from ..optimizers import Optimizer, SLSQP, Minimizer -from ..variational_algorithm import VariationalAlgorithm, VariationalResult -from .eigen_solver import Eigensolver, EigensolverResult -from ..minimum_eigen_solvers.vqe import _validate_bounds, _validate_initial_point -from ..exceptions import AlgorithmError -from ..aux_ops_evaluator import eval_observables - -logger = logging.getLogger(__name__) - - -class VQD(VariationalAlgorithm, Eigensolver): - r"""Deprecated: Variational Quantum Deflation algorithm. - - The VQD class has been superseded by the - :class:`qiskit.algorithms.eigensolvers.VQD` class. - This class will be deprecated in a future release and subsequently - removed after that. - - `VQD `__ is a quantum algorithm that uses a - variational technique to find - the k eigenvalues of the Hamiltonian :math:`H` of a given system. - - The algorithm computes excited state energies of generalised hamiltonians - by optimising over a modified cost function where each succesive eigen value - is calculated iteratively by introducing an overlap term with all - the previously computed eigenstaes that must be minimised, thus ensuring - higher energy eigen states are found. - - An instance of VQD requires defining three algorithmic sub-components: - an integer k denoting the number of eigenstates to calculate, a trial - state (a.k.a. ansatz)which is a :class:`QuantumCircuit`, - and one of the classical :mod:`~qiskit.algorithms.optimizers`. - The ansatz is varied, via its set of parameters, by the optimizer, - such that it works towards a state, as determined by the parameters - applied to the ansatz, that will result in the minimum expectation values - being measured of the input operator (Hamiltonian). The algorithm does - this by iteratively refining each excited state to be orthogonal to all - the previous excited states. - - An optional array of parameter values, via the *initial_point*, may be provided as the - starting point for the search of the minimum eigenvalue. This feature is particularly useful - such as when there are reasons to believe that the solution point is close to a particular - point. - - The length of the *initial_point* list value must match the number of the parameters - expected by the ansatz being used. If the *initial_point* is left at the default - of ``None``, then VQD will look to the ansatz for a preferred value, based on its - given initial state. If the ansatz returns ``None``, - then a random point will be generated within the parameter bounds set, as per above. - If the ansatz provides ``None`` as the lower bound, then VQD - will default it to :math:`-2\pi`; similarly, if the ansatz returns ``None`` - as the upper bound, the default value will be :math:`2\pi`. - - """ - - @deprecate_func( - additional_msg=( - "Instead, use the class ``qiskit.algorithms.eigensolvers.VQD``." - "See https://qisk.it/algo_migration for a migration guide." - ), - since="0.24.0", - package_name="qiskit-terra", - ) - def __init__( - self, - ansatz: QuantumCircuit | None = None, - k: int = 2, - betas: list[float] | None = None, - optimizer: Optimizer | Minimizer | None = None, - initial_point: np.ndarray | None = None, - gradient: GradientBase | Callable | None = None, - expectation: ExpectationBase | None = None, - include_custom: bool = False, - max_evals_grouped: int = 1, - callback: Callable[[int, np.ndarray, float, float, int], None] | None = None, - quantum_instance: QuantumInstance | Backend | None = None, - ) -> None: - """ - - Args: - ansatz: A parameterized circuit used as ansatz for the wave function. - k: the number of eigenvalues to return. Returns the lowest k eigenvalues. - betas: beta parameters in the VQD paper. - Should have length k - 1, with k the number of excited states. - These hyperparameters balance the contribution of each overlap term to the cost - function and have a default value computed as the mean square sum of the - coefficients of the observable. - optimizer: A classical optimizer. Can either be a Qiskit optimizer or a callable - that takes an array as input and returns a Qiskit or SciPy optimization result. - initial_point: An optional initial point (i.e. initial parameter values) - for the optimizer. If ``None`` then VQD will look to the ansatz for a preferred - point and if not will simply compute a random one. - gradient: An optional gradient function or operator for optimizer. - Only used to compute the ground state at the moment. - expectation: The Expectation converter for taking the average value of the - Observable over the ansatz state function. When ``None`` (the default) an - :class:`~qiskit.opflow.expectations.ExpectationFactory` is used to select - an appropriate expectation based on the operator and backend. When using Aer - qasm_simulator backend, with paulis, it is however much faster to leverage custom - Aer function for the computation but, although VQD performs much faster - with it, the outcome is ideal, with no shot noise, like using a state vector - simulator. If you are just looking for the quickest performance when choosing Aer - qasm_simulator and the lack of shot noise is not an issue then set `include_custom` - parameter here to ``True`` (defaults to ``False``). - include_custom: When `expectation` parameter here is None setting this to ``True`` will - allow the factory to include the custom Aer pauli expectation. - max_evals_grouped: Max number of evaluations performed simultaneously. Signals the - given optimizer that more than one set of parameters can be supplied so that - multiple points to compute the gradient can be passed and if computed in parallel - potentially the expectation values can be computed in parallel. Typically this is - possible when a finite difference gradient is used by the optimizer such that - improve overall execution time. Deprecated if a gradient operator or function is - given. - callback: a callback that can access the intermediate data during the optimization. - Four parameter values are passed to the callback as follows during each evaluation - by the optimizer for its current set of parameters as it works towards the minimum. - These are: the evaluation count, the optimizer parameters for the ansatz, the - evaluated mean, the evaluated standard deviation, and the current step. - quantum_instance: Quantum Instance or Backend - - """ - validate_min("max_evals_grouped", max_evals_grouped, 1) - - with warnings.catch_warnings(): - warnings.simplefilter("ignore") - super().__init__() - - self._max_evals_grouped = max_evals_grouped - self._circuit_sampler: CircuitSampler | None = None - self._expectation = None - self.expectation = expectation - self._include_custom = include_custom - - # set ansatz -- still supporting pre 0.18.0 sorting - - self._ansatz: QuantumCircuit | None = None - self.ansatz = ansatz - - self.k = k - self.betas = betas - - self._optimizer: Optimizer | None = None - self.optimizer = optimizer - - self._initial_point: np.ndarray | None = None - self.initial_point = initial_point - self._gradient: GradientBase | Callable | None = None - self.gradient = gradient - self._quantum_instance: QuantumInstance | None = None - - if quantum_instance is not None: - self.quantum_instance = quantum_instance - - self._eval_time = None - self._eval_count = 0 - self._callback: Callable[[int, np.ndarray, float, float, int], None] | None = None - self.callback = callback - - logger.info(self.print_settings()) - - @property - def ansatz(self) -> QuantumCircuit: - """Returns the ansatz.""" - return self._ansatz - - @ansatz.setter - def ansatz(self, ansatz: QuantumCircuit | None): - """Sets the ansatz. - - Args: - ansatz: The parameterized circuit used as an ansatz. - If None is passed, RealAmplitudes is used by default. - - """ - if ansatz is None: - ansatz = RealAmplitudes() - - self._ansatz = ansatz - - @property - def gradient(self) -> GradientBase | Callable | None: - """Returns the gradient.""" - return self._gradient - - @gradient.setter - def gradient(self, gradient: GradientBase | Callable | None): - """Sets the gradient.""" - self._gradient = gradient - - @property - def quantum_instance(self) -> QuantumInstance | None: - """Returns quantum instance.""" - return self._quantum_instance - - @quantum_instance.setter - def quantum_instance(self, quantum_instance: QuantumInstance | Backend) -> None: - """Sets a quantum_instance.""" - if not isinstance(quantum_instance, QuantumInstance): - quantum_instance = QuantumInstance(quantum_instance) - - self._quantum_instance = quantum_instance - self._circuit_sampler = CircuitSampler( - quantum_instance, param_qobj=is_aer_provider(quantum_instance.backend) - ) - - @property - def initial_point(self) -> np.ndarray | None: - """Returns initial point.""" - return self._initial_point - - @initial_point.setter - def initial_point(self, initial_point: np.ndarray): - """Sets initial point""" - self._initial_point = initial_point - - @property - def max_evals_grouped(self) -> int: - """Returns max_evals_grouped""" - return self._max_evals_grouped - - @max_evals_grouped.setter - def max_evals_grouped(self, max_evals_grouped: int): - """Sets max_evals_grouped""" - self._max_evals_grouped = max_evals_grouped - self.optimizer.set_max_evals_grouped(max_evals_grouped) - - @property - def include_custom(self) -> bool: - """Returns include_custom""" - return self._include_custom - - @include_custom.setter - def include_custom(self, include_custom: bool): - """Sets include_custom. If set to another value than the one that was previsously set, - the expectation attribute is reset to None. - """ - if include_custom != self._include_custom: - self._include_custom = include_custom - self.expectation = None - - @property - def callback(self) -> Callable[[int, np.ndarray, float, float, int], None] | None: - """Returns callback""" - return self._callback - - @callback.setter - def callback(self, callback: Callable[[int, np.ndarray, float, float, int], None] | None): - """Sets callback""" - self._callback = callback - - @property - def expectation(self) -> ExpectationBase | None: - """The expectation value algorithm used to construct the expectation measurement from - the observable.""" - return self._expectation - - @expectation.setter - def expectation(self, exp: ExpectationBase | None) -> None: - self._expectation = exp - - def _check_operator_ansatz(self, operator: OperatorBase): - """Check that the number of qubits of operator and ansatz match.""" - if operator is not None and self.ansatz is not None: - if operator.num_qubits != self.ansatz.num_qubits: - # try to set the number of qubits on the ansatz, if possible - try: - self.ansatz.num_qubits = operator.num_qubits - except AttributeError as ex: - raise AlgorithmError( - "The number of qubits of the ansatz does not match the " - "operator, and the ansatz does not allow setting the " - "number of qubits using `num_qubits`." - ) from ex - - @property - def optimizer(self) -> Optimizer: - """Returns optimizer""" - return self._optimizer - - @optimizer.setter - def optimizer(self, optimizer: Optimizer | None): - """Sets the optimizer attribute. - - Args: - optimizer: The optimizer to be used. If None is passed, SLSQP is used by default. - - """ - if optimizer is None: - optimizer = SLSQP() - - if isinstance(optimizer, Optimizer): - optimizer.set_max_evals_grouped(self.max_evals_grouped) - - self._optimizer = optimizer - - @property - def setting(self): - """Prepare the setting of VQD as a string.""" - ret = f"Algorithm: {self.__class__.__name__}\n" - params = "" - for key, value in self.__dict__.items(): - if key[0] == "_": - if "initial_point" in key and value is None: - params += "-- {}: {}\n".format(key[1:], "Random seed") - else: - params += f"-- {key[1:]}: {value}\n" - ret += f"{params}" - return ret - - def print_settings(self): - """Preparing the setting of VQD into a string. - - Returns: - str: the formatted setting of VQD. - """ - ret = "\n" - ret += "==================== Setting of {} ============================\n".format( - self.__class__.__name__ - ) - ret += f"{self.setting}" - ret += "===============================================================\n" - if self.ansatz is not None: - ret += "{}".format(self.ansatz.draw(output="text")) - else: - ret += "ansatz has not been set" - ret += "===============================================================\n" - ret += f"{self._optimizer.setting}" - ret += "===============================================================\n" - return ret - - def construct_expectation( - self, - parameter: list[float] | list[Parameter] | np.ndarray, - operator: OperatorBase, - return_expectation: bool = False, - ) -> OperatorBase | tuple[OperatorBase, ExpectationBase]: - r""" - Generate the ansatz circuit and expectation value measurement, and return their - runnable composition. - - Args: - parameter: Parameters for the ansatz circuit. - operator: Qubit operator of the Observable - return_expectation: If True, return the ``ExpectationBase`` expectation converter used - in the construction of the expectation value. Useful e.g. to compute the standard - deviation of the expectation value. - - Returns: - The Operator equalling the measurement of the ansatz :class:`StateFn` by the - Observable's expectation :class:`StateFn`, and, optionally, the expectation converter. - - Raises: - AlgorithmError: If no operator has been provided. - AlgorithmError: If no expectation is passed and None could be inferred via the - ExpectationFactory. - """ - if operator is None: - raise AlgorithmError("The operator was never provided.") - - self._check_operator_ansatz(operator) - - # if expectation was never created, try to create one - if self.expectation is None: - expectation = ExpectationFactory.build( - operator=operator, - backend=self.quantum_instance, - include_custom=self._include_custom, - ) - else: - expectation = self.expectation - - wave_function = self.ansatz.assign_parameters(parameter) - - observable_meas = expectation.convert(StateFn(operator, is_measurement=True)) - ansatz_circuit_op = CircuitStateFn(wave_function) - expect_op = observable_meas.compose(ansatz_circuit_op).reduce() - - if return_expectation: - return expect_op, expectation - - return expect_op - - def construct_circuit( - self, - parameter: list[float] | list[Parameter] | np.ndarray, - operator: OperatorBase, - ) -> list[QuantumCircuit]: - """Return the circuits used to compute the expectation value. - - Args: - parameter: Parameters for the ansatz circuit. - operator: Qubit operator of the Observable - - Returns: - A list of the circuits used to compute the expectation value. - """ - expect_op = self.construct_expectation(parameter, operator).to_circuit_op() - - circuits = [] - - # recursively extract circuits - def extract_circuits(op): - if isinstance(op, CircuitStateFn): - circuits.append(op.primitive) - elif isinstance(op, ListOp): - for op_i in op.oplist: - extract_circuits(op_i) - - extract_circuits(expect_op) - - return circuits - - @classmethod - def supports_aux_operators(cls) -> bool: - return True - - def _eval_aux_ops( - self, - parameters: np.ndarray, - aux_operators: ListOrDict[OperatorBase], - expectation: ExpectationBase, - threshold: float = 1e-12, - ) -> ListOrDict[tuple[complex, complex]]: - # Create new CircuitSampler to avoid breaking existing one's caches. - sampler = CircuitSampler(self.quantum_instance) - - if isinstance(aux_operators, dict): - list_op = ListOp(list(aux_operators.values())) - else: - list_op = ListOp(aux_operators) - - aux_op_meas = expectation.convert(StateFn(list_op, is_measurement=True)) - aux_op_expect = aux_op_meas.compose( - CircuitStateFn(self.ansatz.assign_parameters(parameters)) - ) - aux_op_expect_sampled = sampler.convert(aux_op_expect) - - # compute means - values = np.real(aux_op_expect_sampled.eval()) - - # compute standard deviations - variances = np.real(expectation.compute_variance(aux_op_expect_sampled)) - if not isinstance(variances, np.ndarray) and variances == 0.0: - # when `variances` is a single value equal to 0., our expectation value is exact and we - # manually ensure the variances to be a list of the correct length - variances = np.zeros(len(aux_operators), dtype=float) - std_devs = np.sqrt(variances / self.quantum_instance.run_config.shots) - - # Discard values below threshold - aux_op_means = values * (np.abs(values) > threshold) - # zip means and standard deviations into tuples - aux_op_results = zip(aux_op_means, std_devs) - - # Return None eigenvalues for None operators if aux_operators is a list. - # None operators are already dropped in compute_minimum_eigenvalue if aux_operators is a - # dict. - if isinstance(aux_operators, list): - aux_operator_eigenvalues: ListOrDict[tuple[complex, complex]] = [None] * len( - aux_operators - ) - key_value_iterator = enumerate(aux_op_results) - else: - aux_operator_eigenvalues = {} - key_value_iterator = zip(aux_operators.keys(), aux_op_results) - - for key, value in key_value_iterator: - if aux_operators[key] is not None: - aux_operator_eigenvalues[key] = value - - return aux_operator_eigenvalues - - def compute_eigenvalues( - self, operator: OperatorBase, aux_operators: ListOrDict[OperatorBase] | None = None - ) -> EigensolverResult: - super().compute_eigenvalues(operator, aux_operators) - - if self.quantum_instance is None: - raise AlgorithmError( - "A QuantumInstance or Backend must be supplied to run the quantum algorithm." - ) - self.quantum_instance.circuit_summary = True - - # this sets the size of the ansatz, so it must be called before the initial point - # validation - self._check_operator_ansatz(operator) - - # set an expectation for this algorithm run (will be reset to None at the end) - initial_point = _validate_initial_point(self.initial_point, self.ansatz) - - bounds = _validate_bounds(self.ansatz) - # We need to handle the array entries being zero or Optional i.e. having value None - if aux_operators: - zero_op = PauliSumOp.from_list([("I" * self.ansatz.num_qubits, 0)]) - - # Convert the None and zero values when aux_operators is a list. - # Drop None and convert zero values when aux_operators is a dict. - if isinstance(aux_operators, list): - key_op_iterator = enumerate(aux_operators) - converted: ListOrDict[OperatorBase] = [zero_op] * len(aux_operators) - else: - key_op_iterator = aux_operators.items() - converted = {} - for key, op in key_op_iterator: - if op is not None: - converted[key] = zero_op if op == 0 else op - - aux_operators = converted - - else: - aux_operators = None - - if self.betas is None: - upper_bound = ( - abs(operator.coeff) - if isinstance(operator, PauliOp) - else abs(operator.coeff) * sum(abs(operation.coeff) for operation in operator) - ) - self.betas = [upper_bound * 10] * (self.k) - logger.info("beta autoevaluated to %s", self.betas[0]) - - result = VQDResult() - result.optimal_point = [] - result.optimal_parameters = [] - result.optimal_value = [] - result.cost_function_evals = [] - result.optimizer_time = [] - result.eigenvalues = [] - result.eigenstates = [] - - if aux_operators is not None: - aux_values = [] - - for step in range(1, self.k + 1): - - self._eval_count = 0 - energy_evaluation, expectation = self.get_energy_evaluation( - step, operator, return_expectation=True, prev_states=result.optimal_parameters - ) - - # Convert the gradient operator into a callable function that is compatible with the - # optimization routine. Only used for the ground state currently as Gradient() doesnt - # support SumOps yet - if isinstance(self._gradient, GradientBase): - gradient = self._gradient.gradient_wrapper( - StateFn(operator, is_measurement=True) @ StateFn(self.ansatz), - bind_params=list(self.ansatz.parameters), - backend=self._quantum_instance, - ) - else: - gradient = self._gradient - - start_time = time() - - if callable(self.optimizer): - opt_result = self.optimizer( # pylint: disable=not-callable - fun=energy_evaluation, x0=initial_point, jac=gradient, bounds=bounds - ) - else: - opt_result = self.optimizer.minimize( - fun=energy_evaluation, x0=initial_point, jac=gradient, bounds=bounds - ) - - eval_time = time() - start_time - - result.optimal_point.append(opt_result.x) - result.optimal_parameters.append(dict(zip(self.ansatz.parameters, opt_result.x))) - result.optimal_value.append(opt_result.fun) - result.cost_function_evals.append(opt_result.nfev) - result.optimizer_time.append(eval_time) - - eigenvalue = ( - StateFn(operator, is_measurement=True) - .compose( - CircuitStateFn(self.ansatz.assign_parameters(result.optimal_parameters[-1])) - ) - .reduce() - .eval() - ) - - result.eigenvalues.append(eigenvalue) - result.eigenstates.append(self._get_eigenstate(result.optimal_parameters[-1])) - - if aux_operators is not None: - bound_ansatz = self.ansatz.assign_parameters(result.optimal_point[-1]) - aux_value = eval_observables( - self.quantum_instance, bound_ansatz, aux_operators, expectation=expectation - ) - aux_values.append(aux_value) - - if step == 1: - - logger.info( - "Ground state optimization complete in %s seconds.\n" - "Found opt_params %s in %s evals", - eval_time, - result.optimal_point, - self._eval_count, - ) - else: - logger.info( - ( - "%s excited state optimization complete in %s s.\n" - "Found opt_params %s in %s evals" - ), - str(step - 1), - eval_time, - result.optimal_point, - self._eval_count, - ) - - # To match the signature of NumpyEigenSolver Result - result.eigenstates = ListOp([StateFn(vec) for vec in result.eigenstates]) - result.eigenvalues = np.array(result.eigenvalues) - result.optimal_point = np.array(result.optimal_point) - result.optimal_value = np.array(result.optimal_value) - result.cost_function_evals = np.array(result.cost_function_evals) - result.optimizer_time = np.array(result.optimizer_time) - - if aux_operators is not None: - result.aux_operator_eigenvalues = aux_values - - return result - - def get_energy_evaluation( - self, - step: int, - operator: OperatorBase, - return_expectation: bool = False, - prev_states: list[np.ndarray] | None = None, - ) -> Callable[[np.ndarray], float | list[float]] | tuple[ - Callable[[np.ndarray], float | list[float]], ExpectationBase - ]: - """Returns a function handle to evaluates the energy at given parameters for the ansatz. - - This return value is the objective function to be passed to the optimizer for evaluation. - - Args: - step: level of energy being calculated. 0 for ground, 1 for first excited state... - operator: The operator whose energy to evaluate. - return_expectation: If True, return the ``ExpectationBase`` expectation converter used - in the construction of the expectation value. Useful e.g. to evaluate other - operators with the same expectation value converter. - prev_states: List of parameters from previous rounds of optimization. - - - Returns: - A callable that computes and returns the energy of the hamiltonian - of each parameter, and, optionally, the expectation - - Raises: - RuntimeError: If the circuit is not parameterized (i.e. has 0 free parameters). - AlgorithmError: If operator was not provided. - - """ - - num_parameters = self.ansatz.num_parameters - if num_parameters == 0: - raise RuntimeError("The ansatz must be parameterized, but has 0 free parameters.") - - if operator is None: - raise AlgorithmError("The operator was never provided.") - - if step > 1 and (len(prev_states) + 1) != step: - raise RuntimeError( - f"Passed previous states of the wrong size." - f"Passed array has length {str(len(prev_states))}" - ) - - self._check_operator_ansatz(operator) - overlap_op = [] - - ansatz_params = self.ansatz.parameters - expect_op, expectation = self.construct_expectation( - ansatz_params, operator, return_expectation=True - ) - - for state in range(step - 1): - - prev_circ = self.ansatz.assign_parameters(prev_states[state]) - overlap_op.append(~CircuitStateFn(prev_circ) @ CircuitStateFn(self.ansatz)) - - def energy_evaluation(parameters): - parameter_sets = np.reshape(parameters, (-1, num_parameters)) - # Dict associating each parameter with the lists of parameterization values for it - param_bindings = dict(zip(ansatz_params, parameter_sets.transpose().tolist())) - - sampled_expect_op = self._circuit_sampler.convert(expect_op, params=param_bindings) - means = np.real(sampled_expect_op.eval()) - - for state in range(step - 1): - sampled_final_op = self._circuit_sampler.convert( - overlap_op[state], params=param_bindings - ) - cost = sampled_final_op.eval() - means += np.real(self.betas[state] * np.conj(cost) * cost) - - if self._callback is not None: - variance = np.real(expectation.compute_variance(sampled_expect_op)) - estimator_error = np.sqrt(variance / self.quantum_instance.run_config.shots) - for i, param_set in enumerate(parameter_sets): - self._eval_count += 1 - self._callback(self._eval_count, param_set, means[i], estimator_error[i], step) - else: - self._eval_count += len(means) - - return means if len(means) > 1 else means[0] - - if return_expectation: - return energy_evaluation, expectation - - return energy_evaluation - - def _get_eigenstate(self, optimal_parameters) -> list[float] | dict[str, int]: - """Get the simulation outcome of the ansatz, provided with parameters.""" - optimal_circuit = self.ansatz.assign_parameters(optimal_parameters) - state_fn = self._circuit_sampler.convert(StateFn(optimal_circuit)).eval() - if self.quantum_instance.is_statevector: - state = state_fn.primitive.data # VectorStateFn -> Statevector -> np.array - else: - state = state_fn.to_dict_fn().primitive # SparseVectorStateFn -> DictStateFn -> dict - - return state - - -class VQDResult(VariationalResult, EigensolverResult): - """Deprecated: VQD Result. - - The VQDResult class has been superseded by the - :class:`qiskit.algorithms.eigensolvers.VQDResult` class. - This class will be deprecated in a future release and subsequently - removed after that. - - """ - - @deprecate_func( - additional_msg=( - "Instead, use the class ``qiskit.algorithms.eigensolvers.VQDResult``." - "See https://qisk.it/algo_migration for a migration guide." - ), - since="0.24.0", - package_name="qiskit-terra", - ) - def __init__(self) -> None: - super().__init__() - self._cost_function_evals: int | None = None - - @property - def cost_function_evals(self) -> int | None: - """Returns number of cost optimizer evaluations""" - return self._cost_function_evals - - @cost_function_evals.setter - def cost_function_evals(self, value: int) -> None: - """Sets number of cost function evaluations""" - self._cost_function_evals = value - - @property - def eigenstates(self) -> np.ndarray | None: - """return eigen state""" - return self._eigenstates - - @eigenstates.setter - def eigenstates(self, value: np.ndarray) -> None: - """set eigen state""" - self._eigenstates = value diff --git a/qiskit/algorithms/eigensolvers/__init__.py b/qiskit/algorithms/eigensolvers/__init__.py deleted file mode 100644 index 2934d6c2a340..000000000000 --- a/qiskit/algorithms/eigensolvers/__init__.py +++ /dev/null @@ -1,53 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2022. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. - -""" -===================================================================== -Eigensolvers Package (:mod:`qiskit.algorithms.eigensolvers`) -===================================================================== - -.. currentmodule:: qiskit.algorithms.eigensolvers - -Eigensolvers -================ - -.. autosummary:: - :toctree: ../stubs/ - - Eigensolver - NumPyEigensolver - VQD - -Results -======= - - .. autosummary:: - :toctree: ../stubs/ - - EigensolverResult - NumPyEigensolverResult - VQDResult - -""" - -from .numpy_eigensolver import NumPyEigensolver, NumPyEigensolverResult -from .eigensolver import Eigensolver, EigensolverResult -from .vqd import VQD, VQDResult - -__all__ = [ - "NumPyEigensolver", - "NumPyEigensolverResult", - "Eigensolver", - "EigensolverResult", - "VQD", - "VQDResult", -] diff --git a/qiskit/algorithms/eigensolvers/eigensolver.py b/qiskit/algorithms/eigensolvers/eigensolver.py deleted file mode 100644 index 30fb7c488844..000000000000 --- a/qiskit/algorithms/eigensolvers/eigensolver.py +++ /dev/null @@ -1,106 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2022. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. - -"""The eigensolver interface and result.""" - -from __future__ import annotations - -from abc import ABC, abstractmethod -from typing import Any -import numpy as np - -from qiskit.opflow import PauliSumOp -from qiskit.quantum_info.operators.base_operator import BaseOperator - -from ..algorithm_result import AlgorithmResult -from ..list_or_dict import ListOrDict - - -class Eigensolver(ABC): - """The eigensolver interface. - - Algorithms that can compute eigenvalues for an operator - may implement this interface to allow different algorithms to be - used interchangeably. - """ - - @abstractmethod - def compute_eigenvalues( - self, - operator: BaseOperator | PauliSumOp, - aux_operators: ListOrDict[BaseOperator | PauliSumOp] | None = None, - ) -> "EigensolverResult": - """ - Computes the minimum eigenvalue. The ``operator`` and ``aux_operators`` are supplied here. - While an ``operator`` is required by algorithms, ``aux_operators`` are optional. - - Args: - operator: Qubit operator of the observable. - aux_operators: Optional list of auxiliary operators to be evaluated with the - eigenstate of the minimum eigenvalue main result and their expectation values - returned. For instance, in chemistry, these can be dipole operators and total particle - count operators, so we can get values for these at the ground state. - - Returns: - An eigensolver result. - """ - return EigensolverResult() - - @classmethod - def supports_aux_operators(cls) -> bool: - """Whether computing the expectation value of auxiliary operators is supported. - - If the eigensolver computes the eigenvalues of the main operator, then it can compute - the expectation value of the ``aux_operators`` for that state. Otherwise they will be ignored. - - Returns: - ``True`` if ``aux_operator`` expectations can be evaluated, ``False`` otherwise. - """ - return False - - -class EigensolverResult(AlgorithmResult): - """Eigensolver result.""" - - def __init__(self) -> None: - super().__init__() - self._eigenvalues: np.ndarray | None = None - self._aux_operators_evaluated: list[ - ListOrDict[tuple[complex, dict[str, Any]]] - ] | None = None - - @property - def eigenvalues(self) -> np.ndarray | None: - """Return the eigenvalues.""" - return self._eigenvalues - - @eigenvalues.setter - def eigenvalues(self, value: np.ndarray) -> None: - """Set the eigenvalues.""" - self._eigenvalues = value - - @property - def aux_operators_evaluated( - self, - ) -> list[ListOrDict[tuple[complex, dict[str, Any]]]] | None: - """Return the aux operator expectation values. - - These values are in fact tuples formatted as (mean, metadata). - """ - return self._aux_operators_evaluated - - @aux_operators_evaluated.setter - def aux_operators_evaluated( - self, value: list[ListOrDict[tuple[complex, dict[str, Any]]]] - ) -> None: - """Set the aux operator eigenvalues.""" - self._aux_operators_evaluated = value diff --git a/qiskit/algorithms/eigensolvers/numpy_eigensolver.py b/qiskit/algorithms/eigensolvers/numpy_eigensolver.py deleted file mode 100644 index c0af8382ccd4..000000000000 --- a/qiskit/algorithms/eigensolvers/numpy_eigensolver.py +++ /dev/null @@ -1,327 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2022. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. - -"""The NumPy eigensolver algorithm.""" - -from __future__ import annotations - -import warnings -from typing import Callable, Union, List, Optional -import logging -import numpy as np -from scipy import sparse as scisparse - -from qiskit.opflow import PauliSumOp -from qiskit.quantum_info import SparsePauliOp, Statevector -from qiskit.quantum_info.operators.base_operator import BaseOperator -from qiskit.utils.validation import validate_min - -from .eigensolver import Eigensolver, EigensolverResult -from ..exceptions import AlgorithmError -from ..list_or_dict import ListOrDict - -logger = logging.getLogger(__name__) - -FilterType = Callable[[Union[List, np.ndarray], float, Optional[ListOrDict[float]]], bool] - - -class NumPyEigensolver(Eigensolver): - r""" - The NumPy eigensolver algorithm. - - The NumPy Eigensolver computes up to the first :math:`k` eigenvalues of a complex-valued square - matrix of dimension :math:`n \times n`, with :math:`k \leq n`. - - Note: - Operators are automatically converted to SciPy's ``spmatrix`` - as needed and this conversion can be costly in terms of memory and performance as the - operator size, mostly in terms of number of qubits it represents, gets larger. - """ - - def __init__( - self, - k: int = 1, - filter_criterion: FilterType | None = None, - ) -> None: - """ - Args: - k: Number of eigenvalues are to be computed, with a minimum value of 1. - filter_criterion: Callable that allows to filter eigenvalues/eigenstates. Only feasible - eigenstates are returned in the results. The callable has the signature - ``filter(eigenstate, eigenvalue, aux_values)`` and must return a boolean to indicate - whether to keep this value in the final returned result or not. If the number of - elements that satisfies the criterion is smaller than ``k``, then the returned list will - have fewer elements and can even be empty. - """ - with warnings.catch_warnings(): - warnings.filterwarnings("ignore", category=DeprecationWarning) - validate_min("k", k, 1) - - super().__init__() - - self._in_k = k - self._k = k - - self._filter_criterion = filter_criterion - - @property - def k(self) -> int: - """Return k (number of eigenvalues requested).""" - return self._in_k - - @k.setter - def k(self, k: int) -> None: - """Set k (number of eigenvalues requested).""" - validate_min("k", k, 1) - self._in_k = k - self._k = k - - @property - def filter_criterion( - self, - ) -> FilterType | None: - """Return the filter criterion if set.""" - return self._filter_criterion - - @filter_criterion.setter - def filter_criterion(self, filter_criterion: FilterType | None) -> None: - """Set the filter criterion.""" - self._filter_criterion = filter_criterion - - @classmethod - def supports_aux_operators(cls) -> bool: - return True - - def _check_set_k(self, operator: BaseOperator | PauliSumOp) -> None: - if operator is not None: - if self._in_k > 2**operator.num_qubits: - self._k = 2**operator.num_qubits - logger.debug( - "WARNING: Asked for %s eigenvalues but max possible is %s.", self._in_k, self._k - ) - else: - self._k = self._in_k - - def _solve(self, operator: BaseOperator | PauliSumOp) -> tuple[np.ndarray, np.ndarray]: - if isinstance(operator, PauliSumOp): - op_matrix = operator.to_spmatrix() - else: - try: - op_matrix = operator.to_matrix(sparse=True) - except TypeError: - logger.debug( - "WARNING: operator of type `%s` does not support sparse matrices. " - "Trying dense computation", - type(operator), - ) - try: - op_matrix = operator.to_matrix() - except AttributeError as ex: - raise AlgorithmError(f"Unsupported operator type `{type(operator)}`.") from ex - - if isinstance(op_matrix, scisparse.csr_matrix): - # If matrix is diagonal, the elements on the diagonal are the eigenvalues. Solve by sorting. - if scisparse.csr_matrix(op_matrix.diagonal()).nnz == op_matrix.nnz: - diag = op_matrix.diagonal() - indices = np.argsort(diag)[: self._k] - eigval = diag[indices] - eigvec = np.zeros((op_matrix.shape[0], self._k)) - for i, idx in enumerate(indices): - eigvec[idx, i] = 1.0 - else: - if self._k >= 2**operator.num_qubits - 1: - logger.debug( - "SciPy doesn't support to get all eigenvalues, using NumPy instead." - ) - eigval, eigvec = self._solve_dense(operator.to_matrix()) - else: - eigval, eigvec = self._solve_sparse(op_matrix, self._k) - else: - # Sparse SciPy matrix not supported, use dense NumPy computation. - eigval, eigvec = self._solve_dense(operator.to_matrix()) - - indices = np.argsort(eigval)[: self._k] - eigval = eigval[indices] - eigvec = eigvec[:, indices] - return eigval, eigvec.T - - @staticmethod - def _solve_sparse(op_matrix: scisparse.csr_matrix, k: int) -> tuple[np.ndarray, np.ndarray]: - if (op_matrix != op_matrix.H).nnz == 0: - # Operator is Hermitian - return scisparse.linalg.eigsh(op_matrix, k=k, which="SA") - else: - return scisparse.linalg.eigs(op_matrix, k=k, which="SR") - - @staticmethod - def _solve_dense(op_matrix: np.ndarray) -> tuple[np.ndarray, np.ndarray]: - if op_matrix.all() == op_matrix.conj().T.all(): - # Operator is Hermitian - return np.linalg.eigh(op_matrix) - else: - return np.linalg.eig(op_matrix) - - @staticmethod - def _eval_aux_operators( - aux_operators: ListOrDict[BaseOperator | PauliSumOp], - wavefn: np.ndarray, - threshold: float = 1e-12, - ) -> ListOrDict[tuple[complex, complex]]: - - values: ListOrDict[tuple[complex, complex]] - - # As a list, aux_operators can contain None operators for which None values are returned. - # As a dict, the None operators in aux_operators have been dropped in compute_eigenvalues. - if isinstance(aux_operators, list): - values = [None] * len(aux_operators) - key_op_iterator = enumerate(aux_operators) - else: - values = {} - key_op_iterator = aux_operators.items() - - for key, operator in key_op_iterator: - if operator is None: - continue - - if operator.num_qubits is None or operator.num_qubits < 1: - logger.info( - "The number of qubits of the %s operator must be greater than zero.", key - ) - continue - - op_matrix = None - if isinstance(operator, PauliSumOp): - if operator.coeff != 0: - op_matrix = operator.to_spmatrix() - else: - try: - op_matrix = operator.to_matrix(sparse=True) - except TypeError: - logger.debug( - "WARNING: operator of type `%s` does not support sparse matrices. " - "Trying dense computation", - type(operator), - ) - try: - op_matrix = operator.to_matrix() - except AttributeError as ex: - raise AlgorithmError(f"Unsupported operator type {type(operator)}.") from ex - - if isinstance(op_matrix, scisparse.csr_matrix): - value = op_matrix.dot(wavefn).dot(np.conj(wavefn)) - elif isinstance(op_matrix, np.ndarray): - value = Statevector(wavefn).expectation_value(operator) - else: - value = 0.0 - - value = value if np.abs(value) > threshold else 0.0 - # The value gets wrapped into a tuple: (mean, metadata). - # The metadata includes variance (and, for other eigensolvers, shots). - # Since this is an exact computation, there are no shots - # and the variance is known to be zero. - values[key] = (value, {"variance": 0.0}) - return values - - def compute_eigenvalues( - self, - operator: BaseOperator | PauliSumOp, - aux_operators: ListOrDict[BaseOperator | PauliSumOp] | None = None, - ) -> NumPyEigensolverResult: - - super().compute_eigenvalues(operator, aux_operators) - - if operator.num_qubits is None or operator.num_qubits < 1: - raise AlgorithmError("The number of qubits of the operator must be greater than zero.") - - self._check_set_k(operator) - - zero_op = SparsePauliOp(["I" * operator.num_qubits], coeffs=[0.0]) - if isinstance(aux_operators, list) and len(aux_operators) > 0: - # For some reason Chemistry passes aux_ops with 0 qubits and paulis sometimes. - aux_operators = [zero_op if op == 0 else op for op in aux_operators] - elif isinstance(aux_operators, dict) and len(aux_operators) > 0: - aux_operators = { - key: zero_op if op == 0 else op # Convert zero values to zero operators - for key, op in aux_operators.items() - if op is not None # Discard None values - } - else: - aux_operators = None - - k_orig = self._k - if self._filter_criterion: - # need to consider all elements if a filter is set - self._k = 2**operator.num_qubits - - eigvals, eigvecs = self._solve(operator) - - # compute energies before filtering, as this also evaluates the aux operators - if aux_operators is not None: - aux_op_vals = [ - self._eval_aux_operators(aux_operators, eigvecs[i]) for i in range(self._k) - ] - else: - aux_op_vals = None - - # if a filter is set, loop over the given values and only keep - if self._filter_criterion: - filt_eigvals = [] - filt_eigvecs = [] - filt_aux_op_vals = [] - count = 0 - for i, (eigval, eigvec) in enumerate(zip(eigvals, eigvecs)): - if aux_op_vals is not None: - aux_op_val = aux_op_vals[i] - else: - aux_op_val = None - - if self._filter_criterion(eigvec, eigval, aux_op_val): - count += 1 - filt_eigvecs.append(eigvec) - filt_eigvals.append(eigval) - if aux_op_vals is not None: - filt_aux_op_vals.append(aux_op_val) - - if count == k_orig: - break - - eigvals = np.array(filt_eigvals) - eigvecs = np.array(filt_eigvecs) - aux_op_vals = filt_aux_op_vals - - self._k = k_orig - - result = NumPyEigensolverResult() - result.eigenvalues = eigvals - result.eigenstates = [Statevector(vec) for vec in eigvecs] - result.aux_operators_evaluated = aux_op_vals - - logger.debug("NumpyEigensolverResult:\n%s", result) - return result - - -class NumPyEigensolverResult(EigensolverResult): - """NumPy eigensolver result.""" - - def __init__(self) -> None: - super().__init__() - self._eigenstates: list[Statevector] | None = None - - @property - def eigenstates(self) -> list[Statevector] | None: - """Return eigenstates.""" - return self._eigenstates - - @eigenstates.setter - def eigenstates(self, value: list[Statevector]) -> None: - """Set eigenstates.""" - self._eigenstates = value diff --git a/qiskit/algorithms/eigensolvers/vqd.py b/qiskit/algorithms/eigensolvers/vqd.py deleted file mode 100644 index 59f07d8a918b..000000000000 --- a/qiskit/algorithms/eigensolvers/vqd.py +++ /dev/null @@ -1,542 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2022, 2023. -# -# 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 Deflation Algorithm for computing higher energy states. - -See https://arxiv.org/abs/1805.08138. -""" - -from __future__ import annotations - -from collections.abc import Callable, Sequence -from typing import Any -import logging -from time import time - -import numpy as np - -from qiskit.algorithms.state_fidelities import BaseStateFidelity -from qiskit.circuit import QuantumCircuit -from qiskit.opflow import PauliSumOp -from qiskit.primitives import BaseEstimator -from qiskit.quantum_info.operators.base_operator import BaseOperator -from qiskit.quantum_info import SparsePauliOp - -from ..list_or_dict import ListOrDict -from ..optimizers import Optimizer, Minimizer, OptimizerResult -from ..variational_algorithm import VariationalAlgorithm -from .eigensolver import Eigensolver, EigensolverResult -from ..utils import validate_bounds, validate_initial_point -from ..exceptions import AlgorithmError -from ..observables_evaluator import estimate_observables - -# private function as we expect this to be updated in the next release -from ..utils.set_batching import _set_default_batchsize - -logger = logging.getLogger(__name__) - - -class VQD(VariationalAlgorithm, Eigensolver): - r"""The Variational Quantum Deflation algorithm. Implementation using primitives. - - `VQD `__ is a quantum algorithm that uses a - variational technique to find - the k eigenvalues of the Hamiltonian :math:`H` of a given system. - - The algorithm computes excited state energies of generalised hamiltonians - by optimising over a modified cost function where each succesive eigenvalue - is calculated iteratively by introducing an overlap term with all - the previously computed eigenstates that must be minimised, thus ensuring - higher energy eigenstates are found. - - An instance of VQD requires defining three algorithmic sub-components: - an integer k denoting the number of eigenstates to calculate, a trial - state (a.k.a. ansatz) which is a :class:`QuantumCircuit`, - and one instance (or list of) classical :mod:`~qiskit.algorithms.optimizers`. - The optimizer varies the circuit parameters - The trial state :math:`|\psi(\vec\theta)\rangle` is varied by the optimizer, - which modifies the set of ansatz parameters :math:`\vec\theta` - such that the expectation value of the operator on the corresponding - state approaches a minimum. The algorithm does this by iteratively refining - each excited state to be orthogonal to all the previous excited states. - - An optional array of parameter values, via the *initial_point*, may be provided - as the starting point for the search of the minimum eigenvalue. This feature is - particularly useful when there are reasons to believe that the solution point - is close to a particular point. - - The length of the *initial_point* list value must match the number of the parameters - expected by the ansatz. If the *initial_point* is left at the default - of ``None``, then VQD will look to the ansatz for a preferred value, based on its - given initial state. If the ansatz returns ``None``, - then a random point will be generated within the parameter bounds set, as per above. - It is also possible to give a list of initial points, one for every kth eigenvalue. - If the ansatz provides ``None`` as the lower bound, then VQD - will default it to :math:`-2\pi`; similarly, if the ansatz returns ``None`` - as the upper bound, the default value will be :math:`2\pi`. - - The following attributes can be set via the initializer but can also be read and - updated once the VQD object has been constructed. - - Attributes: - estimator (BaseEstimator): The primitive instance used to perform the expectation - estimation as indicated in the VQD paper. - fidelity (BaseStateFidelity): The fidelity class instance used to compute the - overlap estimation as indicated in the VQD paper. - ansatz (QuantumCircuit): A parameterized circuit used as ansatz for the wave function. - optimizer(Optimizer | Sequence[Optimizer]): A classical optimizer or a list of optimizers, - one for every k-th eigenvalue. Can either be a Qiskit optimizer or a callable - that takes an array as input and returns a Qiskit or SciPy optimization result. - k (int): the number of eigenvalues to return. Returns the lowest k eigenvalues. - betas (list[float]): Beta parameters in the VQD paper. - Should have length k - 1, with k the number of excited states. - These hyper-parameters balance the contribution of each overlap term to the cost - function and have a default value computed as the mean square sum of the - coefficients of the observable. - initial point (Sequence[float] | Sequence[Sequence[float]] | None): An optional initial - point (i.e. initial parameter values) or a list of initial points - (one for every k-th eigenvalue) for the optimizer. - If ``None`` then VQD will look to the ansatz for a - preferred point and if not will simply compute a random one. - callback (Callable[[int, np.ndarray, float, dict[str, Any]], None] | None): - A callback that can access the intermediate data - during the optimization. Four parameter values are passed to the callback as - follows during each evaluation by the optimizer: the evaluation count, - the optimizer parameters for the ansatz, the estimated value, the estimation - metadata, and the current step. - """ - - def __init__( - self, - estimator: BaseEstimator, - fidelity: BaseStateFidelity, - ansatz: QuantumCircuit, - optimizer: Optimizer | Minimizer | Sequence[Optimizer | Minimizer], - *, - k: int = 2, - betas: Sequence[float] | None = None, - initial_point: Sequence[float] | Sequence[Sequence[float]] | None = None, - callback: Callable[[int, np.ndarray, float, dict[str, Any]], None] | None = None, - ) -> None: - """ - - Args: - estimator: The estimator primitive. - fidelity: The fidelity class using primitives. - ansatz: A parameterized circuit used as ansatz for the wave function. - optimizer: A classical optimizer or a list of optimizers, one for every k-th eigenvalue. - Can either be a Qiskit optimizer or a callable - that takes an array as input and returns a Qiskit or SciPy optimization result. - k: The number of eigenvalues to return. Returns the lowest k eigenvalues. - betas: Beta parameters in the VQD paper. - Should have length k - 1, with k the number of excited states. - These hyperparameters balance the contribution of each overlap term to the cost - function and have a default value computed as the mean square sum of the - coefficients of the observable. - initial_point: An optional initial point (i.e. initial parameter values) - or a list of initial points (one for every k-th eigenvalue) - for the optimizer. - If ``None`` then VQD will look to the ansatz for a preferred - point and if not will simply compute a random one. - callback: A callback that can access the intermediate data - during the optimization. Four parameter values are passed to the callback as - follows during each evaluation by the optimizer: the evaluation count, - the optimizer parameters for the ansatz, the estimated value, - the estimation metadata, and the current step. - """ - super().__init__() - - self.estimator = estimator - self.fidelity = fidelity - self.ansatz = ansatz - self.optimizer = optimizer - self.k = k - self.betas = betas - # this has to go via getters and setters due to the VariationalAlgorithm interface - self.initial_point = initial_point - self.callback = callback - - self._eval_count = 0 - - @property - def initial_point(self) -> Sequence[float] | Sequence[Sequence[float]] | None: - """Returns initial point.""" - return self._initial_point - - @initial_point.setter - def initial_point(self, initial_point: Sequence[float] | Sequence[Sequence[float]] | None): - """Sets initial point""" - self._initial_point = initial_point - - def _check_operator_ansatz(self, operator: BaseOperator | PauliSumOp): - """Check that the number of qubits of operator and ansatz match.""" - if operator is not None and self.ansatz is not None: - if operator.num_qubits != self.ansatz.num_qubits: - # try to set the number of qubits on the ansatz, if possible - try: - self.ansatz.num_qubits = operator.num_qubits - except AttributeError as exc: - 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 exc - - @classmethod - def supports_aux_operators(cls) -> bool: - return True - - def compute_eigenvalues( - self, - operator: BaseOperator | PauliSumOp, - aux_operators: ListOrDict[BaseOperator | PauliSumOp] | None = None, - ) -> VQDResult: - super().compute_eigenvalues(operator, aux_operators) - - # this sets the size of the ansatz, so it must be called before the initial point - # validation - self._check_operator_ansatz(operator) - - bounds = validate_bounds(self.ansatz) - - # We need to handle the array entries being zero or Optional i.e. having value None - if aux_operators: - zero_op = SparsePauliOp.from_list([("I" * self.ansatz.num_qubits, 0)]) - - # Convert the None and zero values when aux_operators is a list. - # Drop None and convert zero values when aux_operators is a dict. - if isinstance(aux_operators, list): - key_op_iterator = enumerate(aux_operators) - converted: ListOrDict[BaseOperator | PauliSumOp] = [zero_op] * len(aux_operators) - else: - key_op_iterator = aux_operators.items() - converted = {} - for key, op in key_op_iterator: - if op is not None: - converted[key] = zero_op if op == 0 else op - - aux_operators = converted - - else: - aux_operators = None - - if self.betas is None: - if isinstance(operator, PauliSumOp): - operator = operator.coeff * operator.primitive - - try: - upper_bound = sum(np.abs(operator.coeffs)) - - except Exception as exc: - raise NotImplementedError( - r"Beta autoevaluation is not supported for operators" - f"of type {type(operator)}." - ) from exc - - betas = [upper_bound * 10] * (self.k) - logger.info("beta autoevaluated to %s", betas[0]) - else: - betas = self.betas - - result = self._build_vqd_result() - - if aux_operators is not None: - aux_values = [] - - # We keep a list of the bound circuits with optimal parameters, to avoid re-binding - # the same parameters to the ansatz if we do multiple steps - prev_states = [] - - num_initial_points = 0 - if self.initial_point is not None: - initial_points = np.reshape(self.initial_point, (-1, self.ansatz.num_parameters)) - num_initial_points = len(initial_points) - - # 0 just means the initial point is ``None`` and ``validate_initial_point`` - # will select a random point - if num_initial_points <= 1: - initial_point = validate_initial_point(self.initial_point, self.ansatz) - - for step in range(1, self.k + 1): - if num_initial_points > 1: - initial_point = validate_initial_point(initial_points[step - 1], self.ansatz) - - if step > 1: - prev_states.append(self.ansatz.assign_parameters(result.optimal_points[-1])) - - self._eval_count = 0 - energy_evaluation = self._get_evaluate_energy( - step, operator, betas, prev_states=prev_states - ) - - start_time = time() - - # TODO: add gradient support after FidelityGradients are implemented - if isinstance(self.optimizer, Sequence): - optimizer = self.optimizer[step - 1] - else: - optimizer = self.optimizer # fall back to single optimizer if not list - - if callable(optimizer): - opt_result = optimizer( # pylint: disable=not-callable - fun=energy_evaluation, x0=initial_point, 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(optimizer) - - opt_result = optimizer.minimize( - fun=energy_evaluation, x0=initial_point, bounds=bounds - ) - - # reset to original value - if was_updated: - optimizer.set_max_evals_grouped(None) - - eval_time = time() - start_time - - self._update_vqd_result(result, opt_result, eval_time, self.ansatz.copy()) - - if aux_operators is not None: - aux_value = estimate_observables( - self.estimator, self.ansatz, aux_operators, result.optimal_points[-1] - ) - aux_values.append(aux_value) - - if step == 1: - logger.info( - "Ground state optimization complete in %s seconds.\n" - "Found opt_params %s in %s evals", - eval_time, - result.optimal_points, - self._eval_count, - ) - else: - logger.info( - ( - "%s excited state optimization complete in %s s.\n" - "Found opt_params %s in %s evals" - ), - str(step - 1), - eval_time, - result.optimal_points, - self._eval_count, - ) - - # To match the signature of EigensolverResult - result.eigenvalues = np.array(result.eigenvalues) - - if aux_operators is not None: - result.aux_operators_evaluated = aux_values - - return result - - def _get_evaluate_energy( - self, - step: int, - operator: BaseOperator | PauliSumOp, - betas: Sequence[float], - prev_states: list[QuantumCircuit] | None = None, - ) -> Callable[[np.ndarray], float | np.ndarray]: - """Returns a function handle to evaluate the ansatz's energy for any given parameters. - This is the objective function to be passed to the optimizer that is used for evaluation. - - Args: - step: level of energy being calculated. 0 for ground, 1 for first excited state... - operator: The operator whose energy to evaluate. - betas: Beta parameters in the VQD paper. - prev_states: List of optimal circuits from previous rounds of optimization. - - Returns: - A callable that computes and returns the energy of the hamiltonian - of each parameter. - - Raises: - AlgorithmError: If the circuit is not parameterized (i.e. has 0 free parameters). - AlgorithmError: If operator was not provided. - RuntimeError: If the previous states array is of the wrong size. - """ - - num_parameters = self.ansatz.num_parameters - if num_parameters == 0: - raise AlgorithmError("The ansatz must be parameterized, but has no free parameters.") - - if step > 1 and (len(prev_states) + 1) != step: - raise RuntimeError( - f"Passed previous states of the wrong size." - f"Passed array has length {str(len(prev_states))}" - ) - - self._check_operator_ansatz(operator) - - def evaluate_energy(parameters: np.ndarray) -> float | np.ndarray: - # handle broadcasting: ensure parameters is of shape [array, array, ...] - if len(parameters.shape) == 1: - parameters = np.reshape(parameters, (-1, num_parameters)) - batch_size = len(parameters) - - estimator_job = self.estimator.run( - batch_size * [self.ansatz], batch_size * [operator], parameters - ) - - total_cost = np.zeros(batch_size) - - if step > 1: - # compute overlap cost - batched_prev_states = [state for state in prev_states for _ in range(batch_size)] - fidelity_job = self.fidelity.run( - batch_size * [self.ansatz] * (step - 1), - batched_prev_states, - np.tile(parameters, (step - 1, 1)), - ) - costs = fidelity_job.result().fidelities - - costs = np.reshape(costs, (step - 1, -1)) - for state, cost in enumerate(costs): - total_cost += np.real(betas[state] * cost) - - try: - estimator_result = estimator_job.result() - - except Exception as exc: - raise AlgorithmError("The primitive job to evaluate the energy failed!") from exc - - values = estimator_result.values + total_cost - - if self.callback is not None: - metadata = estimator_result.metadata - for params, value, meta in zip(parameters, values, metadata): - self._eval_count += 1 - self.callback(self._eval_count, params, value, meta, step) - else: - self._eval_count += len(values) - - return values if len(values) > 1 else values[0] - - return evaluate_energy - - @staticmethod - def _build_vqd_result() -> VQDResult: - result = VQDResult() - result.optimal_points = np.array([]) - result.optimal_parameters = [] - result.optimal_values = np.array([]) - result.cost_function_evals = np.array([], dtype=int) - result.optimizer_times = np.array([]) - result.eigenvalues = [] - result.optimizer_results = [] - result.optimal_circuits = [] - return result - - @staticmethod - def _update_vqd_result( - result: VQDResult, opt_result: OptimizerResult, eval_time, ansatz - ) -> VQDResult: - result.optimal_points = ( - np.concatenate([result.optimal_points, [opt_result.x]]) - if len(result.optimal_points) > 0 - else np.array([opt_result.x]) - ) - result.optimal_parameters.append(dict(zip(ansatz.parameters, opt_result.x))) - result.optimal_values = np.concatenate([result.optimal_values, [opt_result.fun]]) - result.cost_function_evals = np.concatenate([result.cost_function_evals, [opt_result.nfev]]) - result.optimizer_times = np.concatenate([result.optimizer_times, [eval_time]]) - result.eigenvalues.append(opt_result.fun + 0j) - result.optimizer_results.append(opt_result) - result.optimal_circuits.append(ansatz) - return result - - -class VQDResult(EigensolverResult): - """VQD Result.""" - - def __init__(self) -> None: - super().__init__() - - self._cost_function_evals: np.ndarray | None = None - self._optimizer_times: np.ndarray | None = None - self._optimal_values: np.ndarray | None = None - self._optimal_points: np.ndarray | None = None - self._optimal_parameters: list[dict] | None = None - self._optimizer_results: list[OptimizerResult] | None = None - self._optimal_circuits: list[QuantumCircuit] | None = None - - @property - def cost_function_evals(self) -> np.ndarray | None: - """Returns number of cost optimizer evaluations""" - return self._cost_function_evals - - @cost_function_evals.setter - def cost_function_evals(self, value: np.ndarray) -> None: - """Sets number of cost function evaluations""" - self._cost_function_evals = value - - @property - def optimizer_times(self) -> np.ndarray | None: - """Returns time taken for optimization for each step""" - return self._optimizer_times - - @optimizer_times.setter - def optimizer_times(self, value: np.ndarray) -> None: - """Sets time taken for optimization for each step""" - self._optimizer_times = value - - @property - def optimal_values(self) -> np.ndarray | None: - """Returns optimal value for each step""" - return self._optimal_values - - @optimal_values.setter - def optimal_values(self, value: np.ndarray) -> None: - """Sets optimal values""" - self._optimal_values = value - - @property - def optimal_points(self) -> np.ndarray | None: - """Returns optimal point for each step""" - return self._optimal_points - - @optimal_points.setter - def optimal_points(self, value: np.ndarray) -> None: - """Sets optimal points""" - self._optimal_points = value - - @property - def optimal_parameters(self) -> list[dict] | None: - """Returns the optimal parameters for each step""" - return self._optimal_parameters - - @optimal_parameters.setter - def optimal_parameters(self, value: list[dict]) -> None: - """Sets optimal parameters""" - self._optimal_parameters = value - - @property - def optimizer_results(self) -> list[OptimizerResult] | None: - """Returns the optimizer results for each step""" - return self._optimizer_results - - @optimizer_results.setter - def optimizer_results(self, value: list[OptimizerResult]) -> None: - """Sets optimizer results""" - self._optimizer_results = value - - @property - def optimal_circuits(self) -> list[QuantumCircuit] | None: - """The optimal circuits. Along with the optimal parameters, - these can be used to retrieve the different eigenstates.""" - return self._optimal_circuits - - @optimal_circuits.setter - def optimal_circuits(self, optimal_circuits: list[QuantumCircuit]) -> None: - self._optimal_circuits = optimal_circuits diff --git a/qiskit/algorithms/evolvers/__init__.py b/qiskit/algorithms/evolvers/__init__.py deleted file mode 100644 index 990c787b7c15..000000000000 --- a/qiskit/algorithms/evolvers/__init__.py +++ /dev/null @@ -1,21 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2021, 2022. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. - -"""Quantum Time Evolution package.""" - -from .evolution_result import EvolutionResult -from .evolution_problem import EvolutionProblem - -__all__ = [ - "EvolutionResult", - "EvolutionProblem", -] diff --git a/qiskit/algorithms/evolvers/evolution_problem.py b/qiskit/algorithms/evolvers/evolution_problem.py deleted file mode 100644 index 52fee9ff0be4..000000000000 --- a/qiskit/algorithms/evolvers/evolution_problem.py +++ /dev/null @@ -1,123 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2022. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. - -"""Evolution problem class.""" - -from __future__ import annotations - -from qiskit import QuantumCircuit -from qiskit.circuit import Parameter -from qiskit.opflow import OperatorBase, StateFn -from qiskit.utils.deprecation import deprecate_func -from ..list_or_dict import ListOrDict - - -class EvolutionProblem: - """Deprecated: Evolution problem class. - - The EvolutionProblem class has been superseded by the - :class:`qiskit.algorithms.time_evolvers.TimeEvolutionProblem` class. - This class will be deprecated in a future release and subsequently - removed after that. - - This class is the input to time evolution algorithms and must contain information on the total - evolution time, a quantum state to be evolved and under which Hamiltonian the state is evolved. - """ - - @deprecate_func( - additional_msg=( - "Instead, use the class ``qiskit.algorithms.time_evolvers.TimeEvolutionProblem``. " - "See https://qisk.it/algo_migration for a migration guide." - ), - since="0.24.0", - package_name="qiskit-terra", - ) - def __init__( - self, - hamiltonian: OperatorBase, - time: float, - initial_state: StateFn | QuantumCircuit | None = None, - aux_operators: ListOrDict[OperatorBase] | None = None, - truncation_threshold: float = 1e-12, - t_param: Parameter | None = None, - param_value_dict: dict[Parameter, complex] | None = None, - ): - """ - Args: - hamiltonian: The Hamiltonian under which to evolve the system. - time: Total time of evolution. - initial_state: The quantum state to be evolved for methods like Trotterization. - For variational time evolutions, where the evolution happens in an ansatz, - this argument is not required. - aux_operators: Optional list of auxiliary operators to be evaluated with the - evolved ``initial_state`` and their expectation values returned. - truncation_threshold: Defines a threshold under which values can be assumed to be 0. - Used when ``aux_operators`` is provided. - t_param: Time parameter in case of a time-dependent Hamiltonian. This - free parameter must be within the ``hamiltonian``. - param_value_dict: Maps free parameters in the problem to values. Depending on the - algorithm, it might refer to e.g. a Hamiltonian or an initial state. - - Raises: - ValueError: If non-positive time of evolution is provided. - """ - - self.t_param = t_param - self.param_value_dict = param_value_dict - self.hamiltonian = hamiltonian - self.time = time - self.initial_state = initial_state - self.aux_operators = aux_operators - self.truncation_threshold = truncation_threshold - - @property - def time(self) -> float: - """Returns time.""" - return self._time - - @time.setter - def time(self, time: float) -> None: - """ - Sets time and validates it. - - Raises: - ValueError: If time is not positive. - """ - if time <= 0: - raise ValueError(f"Evolution time must be > 0 but was {time}.") - self._time = time - - def validate_params(self) -> None: - """ - Checks if all parameters present in the Hamiltonian are also present in the dictionary - that maps them to values. - - Raises: - ValueError: If Hamiltonian parameters cannot be bound with data provided. - """ - if isinstance(self.hamiltonian, OperatorBase): - t_param_set = set() - if self.t_param is not None: - t_param_set.add(self.t_param) - hamiltonian_dict_param_set: set[Parameter] = set() - if self.param_value_dict is not None: - hamiltonian_dict_param_set = hamiltonian_dict_param_set.union( - set(self.param_value_dict.keys()) - ) - params_set = t_param_set.union(hamiltonian_dict_param_set) - hamiltonian_param_set = set(self.hamiltonian.parameters) - - if hamiltonian_param_set != params_set: - raise ValueError( - f"Provided parameters {params_set} do not match Hamiltonian parameters " - f"{hamiltonian_param_set}." - ) diff --git a/qiskit/algorithms/evolvers/evolution_result.py b/qiskit/algorithms/evolvers/evolution_result.py deleted file mode 100644 index ccf60f43128d..000000000000 --- a/qiskit/algorithms/evolvers/evolution_result.py +++ /dev/null @@ -1,56 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2021, 2022. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. - -"""Class for holding evolution result.""" - -from __future__ import annotations - -from qiskit import QuantumCircuit -from qiskit.algorithms.list_or_dict import ListOrDict -from qiskit.opflow import StateFn, OperatorBase -from qiskit.utils.deprecation import deprecate_func -from ..algorithm_result import AlgorithmResult - - -class EvolutionResult(AlgorithmResult): - """Deprecated: Class for holding evolution result. - - The EvolutionResult class has been superseded by the - :class:`qiskit.algorithms.time_evolvers.TimeEvolutionResult` class. - This class will be deprecated in a future release and subsequently - removed after that. - - """ - - @deprecate_func( - additional_msg=( - "Instead, use the class ``qiskit.algorithms.time_evolvers.TimeEvolutionResult``. " - "See https://qisk.it/algo_migration for a migration guide." - ), - since="0.24.0", - package_name="qiskit-terra", - ) - def __init__( - self, - evolved_state: StateFn | QuantumCircuit | OperatorBase, - aux_ops_evaluated: ListOrDict[tuple[complex, complex]] | None = None, - ): - """ - Args: - evolved_state: An evolved quantum state. - aux_ops_evaluated: Optional list of observables for which expected values on an evolved - state are calculated. These values are in fact tuples formatted as (mean, standard - deviation). - """ - - self.evolved_state = evolved_state - self.aux_ops_evaluated = aux_ops_evaluated diff --git a/qiskit/algorithms/evolvers/imaginary_evolver.py b/qiskit/algorithms/evolvers/imaginary_evolver.py deleted file mode 100644 index 74b301d3e539..000000000000 --- a/qiskit/algorithms/evolvers/imaginary_evolver.py +++ /dev/null @@ -1,55 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2021, 2022. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. - -"""Interface for Quantum Imaginary Time Evolution.""" - -from abc import ABC, abstractmethod - -from qiskit.utils.deprecation import deprecate_func -from .evolution_problem import EvolutionProblem -from .evolution_result import EvolutionResult - - -class ImaginaryEvolver(ABC): - """Deprecated: Interface for Quantum Imaginary Time Evolution. - - The ImaginaryEvolver interface has been superseded by the - :class:`qiskit.algorithms.time_evolvers.ImaginaryTimeEvolver` interface. - This interface will be deprecated in a future release and subsequently - removed after that. - - """ - - @deprecate_func( - additional_msg=( - "Instead, use the interface ``qiskit.algorithms.time_evolvers.ImaginaryTimeEvolver``. " - "See https://qisk.it/algo_migration for a migration guide." - ), - since="0.24.0", - ) - def __init__(self) -> None: - pass - - @abstractmethod - def evolve(self, evolution_problem: EvolutionProblem) -> EvolutionResult: - r"""Perform imaginary time evolution :math:`\exp(-\tau H)|\Psi\rangle`. - - Evolves an initial state :math:`|\Psi\rangle` for an imaginary time :math:`\tau` - under a Hamiltonian :math:`H`, as provided in the ``evolution_problem``. - - Args: - evolution_problem: The definition of the evolution problem. - - Returns: - Evolution result which includes an evolved quantum state. - """ - raise NotImplementedError() diff --git a/qiskit/algorithms/evolvers/real_evolver.py b/qiskit/algorithms/evolvers/real_evolver.py deleted file mode 100644 index 0e7753317908..000000000000 --- a/qiskit/algorithms/evolvers/real_evolver.py +++ /dev/null @@ -1,56 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2021, 2022. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. - -"""Interface for Quantum Real Time Evolution.""" - -from abc import ABC, abstractmethod -from qiskit.utils.deprecation import deprecate_func - -from .evolution_problem import EvolutionProblem -from .evolution_result import EvolutionResult - - -class RealEvolver(ABC): - """Deprecated: Interface for Quantum Real Time Evolution. - - The RealEvolver interface has been superseded by the - :class:`qiskit.algorithms.time_evolvers.RealTimeEvolver` interface. - This interface will be deprecated in a future release and subsequently - removed after that. - - """ - - @deprecate_func( - additional_msg=( - "Instead, use the interface ``qiskit.algorithms.time_evolvers.RealTimeEvolver``. " - "See https://qisk.it/algo_migration for a migration guide." - ), - since="0.24.0", - package_name="qiskit-terra", - ) - def __init__(self) -> None: - pass - - @abstractmethod - def evolve(self, evolution_problem: EvolutionProblem) -> EvolutionResult: - r"""Perform real time evolution :math:`\exp(-i t H)|\Psi\rangle`. - - Evolves an initial state :math:`|\Psi\rangle` for a time :math:`t` - under a Hamiltonian :math:`H`, as provided in the ``evolution_problem``. - - Args: - evolution_problem: The definition of the evolution problem. - - Returns: - Evolution result which includes an evolved quantum state. - """ - raise NotImplementedError() diff --git a/qiskit/algorithms/evolvers/trotterization/__init__.py b/qiskit/algorithms/evolvers/trotterization/__init__.py deleted file mode 100644 index fe1b8d8aedf2..000000000000 --- a/qiskit/algorithms/evolvers/trotterization/__init__.py +++ /dev/null @@ -1,21 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2021, 2022. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. -"""This package contains Trotterization-based Quantum Real Time Evolution algorithm. -It is compliant with the new Quantum Time Evolution Framework and makes use of -:class:`qiskit.synthesis.evolution.ProductFormula` and -:class:`~qiskit.circuit.library.PauliEvolutionGate` implementations. """ - -from qiskit.algorithms.evolvers.trotterization.trotter_qrte import ( - TrotterQRTE, -) - -__all__ = ["TrotterQRTE"] diff --git a/qiskit/algorithms/evolvers/trotterization/trotter_qrte.py b/qiskit/algorithms/evolvers/trotterization/trotter_qrte.py deleted file mode 100644 index fe740cf28bbf..000000000000 --- a/qiskit/algorithms/evolvers/trotterization/trotter_qrte.py +++ /dev/null @@ -1,263 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2021, 2022. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. - -"""An algorithm to implement a Trotterization real time-evolution.""" - -from __future__ import annotations - -import warnings - -from qiskit import QuantumCircuit -from qiskit.algorithms.aux_ops_evaluator import eval_observables -from qiskit.algorithms.evolvers import EvolutionProblem, EvolutionResult -from qiskit.algorithms.evolvers.real_evolver import RealEvolver -from qiskit.opflow import ( - SummedOp, - PauliOp, - CircuitOp, - ExpectationBase, - CircuitSampler, - PauliSumOp, - StateFn, - OperatorBase, -) -from qiskit.circuit.library import PauliEvolutionGate -from qiskit.providers import Backend -from qiskit.synthesis import ProductFormula, LieTrotter -from qiskit.utils import QuantumInstance -from qiskit.utils.deprecation import deprecate_func - - -class TrotterQRTE(RealEvolver): - """Deprecated: Quantum Real Time Evolution using Trotterization. - - The TrotterQRTE class has been superseded by the - :class:`qiskit.algorithms.time_evolvers.trotterization.TrotterQRTE` class. - This class will be deprecated in a future release and subsequently - removed after that. - - Type of Trotterization is defined by a ProductFormula provided. - - Examples:: - - from qiskit.opflow import X, Z, Zero - from qiskit.algorithms import EvolutionProblem, TrotterQRTE - from qiskit import BasicAer - from qiskit.utils import QuantumInstance - - operator = X + Z - initial_state = Zero - time = 1 - evolution_problem = EvolutionProblem(operator, 1, initial_state) - # LieTrotter with 1 rep - backend = BasicAer.get_backend("statevector_simulator") - quantum_instance = QuantumInstance(backend=backend) - trotter_qrte = TrotterQRTE(quantum_instance=quantum_instance) - evolved_state = trotter_qrte.evolve(evolution_problem).evolved_state - """ - - @deprecate_func( - additional_msg=( - "Instead, use the class ``qiskit.algorithms.time_evolvers.trotterization.TrotterQRTE``." - " See https://qisk.it/algo_migration for a migration guide." - ), - since="0.24.0", - package_name="qiskit-terra", - ) - def __init__( - self, - product_formula: ProductFormula | None = None, - expectation: ExpectationBase | None = None, - quantum_instance: QuantumInstance | Backend | None = None, - ) -> None: - """ - Args: - product_formula: A Lie-Trotter-Suzuki product formula. The default is the Lie-Trotter - first order product formula with a single repetition. - expectation: An instance of ExpectationBase which defines a method for calculating - expectation values of EvolutionProblem.aux_operators. - quantum_instance: A quantum instance used for calculating expectation values of - EvolutionProblem.aux_operators. - """ - with warnings.catch_warnings(): - warnings.simplefilter("ignore") - super().__init__() - if product_formula is None: - product_formula = LieTrotter() - self._product_formula = product_formula - self._quantum_instance = None - self._circuit_sampler: CircuitSampler | None = None - if quantum_instance is not None: - self.quantum_instance = quantum_instance - self._expectation = expectation - - @property - def product_formula(self) -> ProductFormula: - """Returns a product formula used in the algorithm.""" - return self._product_formula - - @product_formula.setter - def product_formula(self, product_formula: ProductFormula) -> None: - """ - Sets a product formula. - Args: - product_formula: A formula that defines the Trotterization algorithm. - """ - self._product_formula = product_formula - - @property - def quantum_instance(self) -> QuantumInstance | None: - """Returns a quantum instance used in the algorithm.""" - return self._quantum_instance - - @quantum_instance.setter - def quantum_instance(self, quantum_instance: QuantumInstance | Backend | None) -> None: - """ - Sets a quantum instance and a circuit sampler. - Args: - quantum_instance: The quantum instance used to run this algorithm. - """ - if isinstance(quantum_instance, Backend): - quantum_instance = QuantumInstance(quantum_instance) - - self._circuit_sampler = None - if quantum_instance is not None: - self._circuit_sampler = CircuitSampler(quantum_instance) - - self._quantum_instance = quantum_instance - - @property - def expectation(self) -> ExpectationBase | None: - """Returns an expectation used in the algorithm.""" - return self._expectation - - @expectation.setter - def expectation(self, expectation: ExpectationBase | None) -> None: - """ - Sets an expectation. - Args: - expectation: An instance of ExpectationBase which defines a method for calculating - expectation values of EvolutionProblem.aux_operators. - """ - self._expectation = expectation - - @classmethod - def supports_aux_operators(cls) -> bool: - """ - Whether computing the expectation value of auxiliary operators is supported. - - Returns: - True if ``aux_operators`` expectations in the EvolutionProblem can be evaluated, False - otherwise. - """ - return True - - def evolve(self, evolution_problem: EvolutionProblem) -> EvolutionResult: - """ - Evolves a quantum state for a given time using the Trotterization method - based on a product formula provided. The result is provided in the form of a quantum - circuit. If auxiliary operators are included in the ``evolution_problem``, they are - evaluated on an evolved state using a backend provided. - - .. note:: - Time-dependent Hamiltonians are not yet supported. - - Args: - evolution_problem: Instance defining evolution problem. For the included Hamiltonian, - ``PauliOp``, ``SummedOp`` or ``PauliSumOp`` are supported by TrotterQRTE. - - Returns: - Evolution result that includes an evolved state as a quantum circuit and, optionally, - auxiliary operators evaluated for a resulting state on a backend. - - Raises: - ValueError: If ``t_param`` is not set to None in the EvolutionProblem (feature not - currently supported). - ValueError: If the ``initial_state`` is not provided in the EvolutionProblem. - """ - evolution_problem.validate_params() - if evolution_problem.t_param is not None: - raise ValueError( - "TrotterQRTE does not accept a time dependent hamiltonian," - "``t_param`` from the EvolutionProblem should be set to None." - ) - - if evolution_problem.aux_operators is not None and ( - self._quantum_instance is None or self._expectation is None - ): - raise ValueError( - "aux_operators were provided for evaluations but no ``expectation`` or " - "``quantum_instance`` was provided." - ) - hamiltonian = evolution_problem.hamiltonian - if not isinstance(hamiltonian, (PauliOp, PauliSumOp, SummedOp)): - raise ValueError( - "TrotterQRTE only accepts PauliOp | " - f"PauliSumOp | SummedOp, {type(hamiltonian)} provided." - ) - if isinstance(hamiltonian, OperatorBase): - hamiltonian = hamiltonian.assign_parameters(evolution_problem.param_value_dict) - if isinstance(hamiltonian, SummedOp): - hamiltonian = self._summed_op_to_pauli_sum_op(hamiltonian) - # the evolution gate - evolution_gate = CircuitOp( - PauliEvolutionGate(hamiltonian, evolution_problem.time, synthesis=self._product_formula) - ) - - if evolution_problem.initial_state is not None: - initial_state = evolution_problem.initial_state - if isinstance(initial_state, QuantumCircuit): - initial_state = StateFn(initial_state) - evolved_state = evolution_gate @ initial_state - - else: - raise ValueError("``initial_state`` must be provided in the EvolutionProblem.") - - evaluated_aux_ops = None - if evolution_problem.aux_operators is not None: - evaluated_aux_ops = eval_observables( - self._quantum_instance, - evolved_state.primitive, - evolution_problem.aux_operators, - self._expectation, - evolution_problem.truncation_threshold, - ) - - return EvolutionResult(evolved_state, evaluated_aux_ops) - - @staticmethod - def _summed_op_to_pauli_sum_op( - hamiltonian: SummedOp, - ) -> PauliSumOp | PauliOp: - """ - Tries binding parameters in a Hamiltonian. - - Args: - hamiltonian: The Hamiltonian that defines an evolution. - - Returns: - Hamiltonian. - - Raises: - ValueError: If the ``SummedOp`` Hamiltonian contains operators of an invalid type. - """ - # PauliSumOp does not allow parametrized coefficients but after binding the parameters - # we need to convert it into a PauliSumOp for the PauliEvolutionGate. - op_list = [] - for op in hamiltonian.oplist: - if not isinstance(op, PauliOp): - raise ValueError( - "Content of the Hamiltonian not of type PauliOp. The " - f"following type detected: {type(op)}." - ) - op_list.append(op) - return sum(op_list) diff --git a/qiskit/algorithms/exceptions.py b/qiskit/algorithms/exceptions.py deleted file mode 100644 index 1f830a3cba95..000000000000 --- a/qiskit/algorithms/exceptions.py +++ /dev/null @@ -1,21 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2017, 2018. -# -# 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. - -"""Exception for errors raised by Algorithms module.""" - -from qiskit.exceptions import QiskitError - - -class AlgorithmError(QiskitError): - """For Algorithm specific errors.""" - - pass diff --git a/qiskit/algorithms/gradients/__init__.py b/qiskit/algorithms/gradients/__init__.py deleted file mode 100644 index ff3a2fceca5e..000000000000 --- a/qiskit/algorithms/gradients/__init__.py +++ /dev/null @@ -1,130 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2022, 2023 -# -# 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. - -""" -============================================== -Gradients (:mod:`qiskit.algorithms.gradients`) -============================================== - -.. currentmodule:: qiskit.algorithms.gradients - -Base Classes -============ - -.. autosummary:: - :toctree: ../stubs/ - - BaseEstimatorGradient - BaseQGT - BaseSamplerGradient - EstimatorGradientResult - SamplerGradientResult - QGTResult - -Finite Differences -================== - -.. autosummary:: - :toctree: ../stubs/ - - FiniteDiffEstimatorGradient - FiniteDiffSamplerGradient - -Linear Combination of Unitaries -=============================== - -.. autosummary:: - :toctree: ../stubs/ - - LinCombEstimatorGradient - LinCombSamplerGradient - LinCombQGT - -Parameter Shift Rules -===================== - -.. autosummary:: - :toctree: ../stubs/ - - ParamShiftEstimatorGradient - ParamShiftSamplerGradient - -Quantum Fisher Information -========================== - -.. autosummary:: - :toctree: ../stubs/ - - QFIResult - QFI - -Classical Methods -================= - -.. autosummary:: - :toctree: ../stubs/ - - ReverseEstimatorGradient - ReverseQGT - -Simultaneous Perturbation Stochastic Approximation -================================================== - -.. autosummary:: - :toctree: ../stubs/ - - SPSAEstimatorGradient - SPSASamplerGradient -""" - -from .base.base_estimator_gradient import BaseEstimatorGradient -from .base.base_qgt import BaseQGT -from .base.base_sampler_gradient import BaseSamplerGradient -from .base.estimator_gradient_result import EstimatorGradientResult -from .finite_diff.finite_diff_estimator_gradient import FiniteDiffEstimatorGradient -from .finite_diff.finite_diff_sampler_gradient import FiniteDiffSamplerGradient -from .lin_comb.lin_comb_estimator_gradient import DerivativeType, LinCombEstimatorGradient -from .lin_comb.lin_comb_qgt import LinCombQGT -from .lin_comb.lin_comb_sampler_gradient import LinCombSamplerGradient -from .param_shift.param_shift_estimator_gradient import ParamShiftEstimatorGradient -from .param_shift.param_shift_sampler_gradient import ParamShiftSamplerGradient -from .qfi import QFI -from .qfi_result import QFIResult -from .base.qgt_result import QGTResult -from .base.sampler_gradient_result import SamplerGradientResult -from .spsa.spsa_estimator_gradient import SPSAEstimatorGradient -from .spsa.spsa_sampler_gradient import SPSASamplerGradient -from .reverse.reverse_gradient import ReverseEstimatorGradient -from .reverse.reverse_qgt import ReverseQGT - -__all__ = [ - "BaseEstimatorGradient", - "BaseQGT", - "BaseSamplerGradient", - "DerivativeType", - "EstimatorGradientResult", - "FiniteDiffEstimatorGradient", - "FiniteDiffSamplerGradient", - "LinCombEstimatorGradient", - "LinCombQGT", - "LinCombSamplerGradient", - "ParamShiftEstimatorGradient", - "ParamShiftSamplerGradient", - "QFI", - "QFIResult", - "QGTResult", - "SamplerGradientResult", - "SPSAEstimatorGradient", - "SPSASamplerGradient", - "ReverseEstimatorGradient", - "ReverseQGT", -] diff --git a/qiskit/algorithms/gradients/base/__init__.py b/qiskit/algorithms/gradients/base/__init__.py deleted file mode 100644 index 8f5fd2a37f84..000000000000 --- a/qiskit/algorithms/gradients/base/__init__.py +++ /dev/null @@ -1,11 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2022, 2023 -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. diff --git a/qiskit/algorithms/gradients/base/base_estimator_gradient.py b/qiskit/algorithms/gradients/base/base_estimator_gradient.py deleted file mode 100644 index 0cbf478fa2ec..000000000000 --- a/qiskit/algorithms/gradients/base/base_estimator_gradient.py +++ /dev/null @@ -1,360 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2022, 2023 -# -# 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. - -""" -Abstract base class of gradient for ``Estimator``. -""" - -from __future__ import annotations - -from abc import ABC, abstractmethod -from collections.abc import Sequence -from copy import copy - -import numpy as np - -from qiskit.circuit import Parameter, ParameterExpression, QuantumCircuit -from qiskit.opflow import PauliSumOp -from qiskit.primitives import BaseEstimator -from qiskit.primitives.utils import _circuit_key -from qiskit.providers import Options -from qiskit.quantum_info.operators.base_operator import BaseOperator -from qiskit.transpiler.passes import TranslateParameterizedGates - -from .estimator_gradient_result import EstimatorGradientResult -from ..utils import ( - DerivativeType, - GradientCircuit, - _assign_unique_parameters, - _make_gradient_parameters, - _make_gradient_parameter_values, -) - -from ...algorithm_job import AlgorithmJob - - -class BaseEstimatorGradient(ABC): - """Base class for an ``EstimatorGradient`` to compute the gradients of the expectation value.""" - - def __init__( - self, - estimator: BaseEstimator, - options: Options | None = None, - derivative_type: DerivativeType = DerivativeType.REAL, - ): - r""" - Args: - estimator: The estimator used to compute the gradients. - options: Primitive backend runtime options used for circuit execution. - The order of priority is: options in ``run`` method > gradient's - default options > primitive's default setting. - Higher priority setting overrides lower priority setting - derivative_type: The type of derivative. Can be either ``DerivativeType.REAL`` - ``DerivativeType.IMAG``, or ``DerivativeType.COMPLEX``. - - - ``DerivativeType.REAL`` computes :math:`2 \mathrm{Re}[⟨ψ(ω)|O(θ)|dω ψ(ω)〉]`. - - ``DerivativeType.IMAG`` computes :math:`2 \mathrm{Im}[⟨ψ(ω)|O(θ)|dω ψ(ω)〉]`. - - ``DerivativeType.COMPLEX`` computes :math:`2 ⟨ψ(ω)|O(θ)|dω ψ(ω)〉`. - - Defaults to ``DerivativeType.REAL``, as this yields e.g. the commonly-used energy - gradient and this type is the only supported type for function-level schemes like - finite difference. - """ - self._estimator: BaseEstimator = estimator - self._default_options = Options() - if options is not None: - self._default_options.update_options(**options) - self._derivative_type = derivative_type - - self._gradient_circuit_cache: dict[ - tuple, - GradientCircuit, - ] = {} - - @property - def derivative_type(self) -> DerivativeType: - """Return the derivative type (real, imaginary or complex). - - Returns: - The derivative type. - """ - return self._derivative_type - - def run( - self, - circuits: Sequence[QuantumCircuit], - observables: Sequence[BaseOperator | PauliSumOp], - parameter_values: Sequence[Sequence[float]], - parameters: Sequence[Sequence[Parameter] | None] | None = None, - **options, - ) -> AlgorithmJob: - """Run the job of the estimator gradient on the given circuits. - - Args: - circuits: The list of quantum circuits to compute the gradients. - observables: The list of observables. - parameter_values: The list of parameter values to be bound to the circuit. - parameters: The sequence of parameters to calculate only the gradients of - the specified parameters. Each sequence of parameters corresponds to a circuit in - ``circuits``. Defaults to None, which means that the gradients of all parameters in - each circuit are calculated. None in the sequence means that the gradients of all - parameters in the corresponding circuit are calculated. - options: Primitive backend runtime options used for circuit execution. - The order of priority is: options in ``run`` method > gradient's - default options > primitive's default setting. - Higher priority setting overrides lower priority setting - - Returns: - The job object of the gradients of the expectation values. The i-th result corresponds to - ``circuits[i]`` evaluated with parameters bound as ``parameter_values[i]``. The j-th - element of the i-th result corresponds to the gradient of the i-th circuit with respect - to the j-th parameter. - - Raises: - ValueError: Invalid arguments are given. - """ - if isinstance(circuits, QuantumCircuit): - # Allow a single circuit to be passed in. - circuits = (circuits,) - if isinstance(observables, (BaseOperator, PauliSumOp)): - # Allow a single observable to be passed in. - observables = (observables,) - - if parameters is None: - # If parameters is None, we calculate the gradients of all parameters in each circuit. - parameters = [circuit.parameters for circuit in circuits] - else: - # If parameters is not None, we calculate the gradients of the specified parameters. - # None in parameters means that the gradients of all parameters in the corresponding - # circuit are calculated. - parameters = [ - params if params is not None else circuits[i].parameters - for i, params in enumerate(parameters) - ] - # Validate the arguments. - self._validate_arguments(circuits, observables, parameter_values, parameters) - # The priority of run option is as follows: - # options in ``run`` method > gradient's default options > primitive's default setting. - opts = copy(self._default_options) - opts.update_options(**options) - # Run the job. - job = AlgorithmJob( - self._run, circuits, observables, parameter_values, parameters, **opts.__dict__ - ) - job.submit() - return job - - @abstractmethod - def _run( - self, - circuits: Sequence[QuantumCircuit], - observables: Sequence[BaseOperator | PauliSumOp], - parameter_values: Sequence[Sequence[float]], - parameters: Sequence[Sequence[Parameter]], - **options, - ) -> EstimatorGradientResult: - """Compute the estimator gradients on the given circuits.""" - raise NotImplementedError() - - def _preprocess( - self, - circuits: Sequence[QuantumCircuit], - parameter_values: Sequence[Sequence[float]], - parameters: Sequence[Sequence[Parameter]], - supported_gates: Sequence[str], - ) -> tuple[Sequence[QuantumCircuit], Sequence[Sequence[float]], Sequence[Sequence[Parameter]]]: - """Preprocess the gradient. This makes a gradient circuit for each circuit. The gradient - circuit is a transpiled circuit by using the supported gates, and has unique parameters. - ``parameter_values`` and ``parameters`` are also updated to match the gradient circuit. - - Args: - circuits: The list of quantum circuits to compute the gradients. - parameter_values: The list of parameter values to be bound to the circuit. - parameters: The sequence of parameters to calculate only the gradients of the specified - parameters. - supported_gates: The supported gates used to transpile the circuit. - - Returns: - The list of gradient circuits, the list of parameter values, and the list of parameters. - parameter_values and parameters are updated to match the gradient circuit. - """ - translator = TranslateParameterizedGates(supported_gates) - g_circuits, g_parameter_values, g_parameters = [], [], [] - for circuit, parameter_value_, parameters_ in zip(circuits, parameter_values, parameters): - circuit_key = _circuit_key(circuit) - if circuit_key not in self._gradient_circuit_cache: - unrolled = translator(circuit) - self._gradient_circuit_cache[circuit_key] = _assign_unique_parameters(unrolled) - gradient_circuit = self._gradient_circuit_cache[circuit_key] - g_circuits.append(gradient_circuit.gradient_circuit) - g_parameter_values.append( - _make_gradient_parameter_values(circuit, gradient_circuit, parameter_value_) - ) - g_parameters.append(_make_gradient_parameters(gradient_circuit, parameters_)) - return g_circuits, g_parameter_values, g_parameters - - def _postprocess( - self, - results: EstimatorGradientResult, - circuits: Sequence[QuantumCircuit], - parameter_values: Sequence[Sequence[float]], - parameters: Sequence[Sequence[Parameter]], - ) -> EstimatorGradientResult: - """Postprocess the gradients. This method computes the gradient of the original circuits - by applying the chain rule to the gradient of the circuits with unique parameters. - - Args: - results: The computed gradients for the circuits with unique parameters. - circuits: The list of original circuits submitted for gradient computation. - parameter_values: The list of parameter values to be bound to the circuits. - parameters: The sequence of parameters to calculate only the gradients of the specified - parameters. - - Returns: - The gradients of the original circuits. - """ - gradients, metadata = [], [] - for idx, (circuit, parameter_values_, parameters_) in enumerate( - zip(circuits, parameter_values, parameters) - ): - gradient = np.zeros(len(parameters_)) - if ( - "derivative_type" in results.metadata[idx] - and results.metadata[idx]["derivative_type"] == DerivativeType.COMPLEX - ): - # If the derivative type is complex, cast the gradient to complex. - gradient = gradient.astype("complex") - gradient_circuit = self._gradient_circuit_cache[_circuit_key(circuit)] - g_parameters = _make_gradient_parameters(gradient_circuit, parameters_) - # Make a map from the gradient parameter to the respective index in the gradient. - g_parameter_indices = {param: i for i, param in enumerate(g_parameters)} - # Compute the original gradient from the gradient of the gradient circuit - # by using the chain rule. - for i, parameter in enumerate(parameters_): - for g_parameter, coeff in gradient_circuit.parameter_map[parameter]: - # Compute the coefficient - if isinstance(coeff, ParameterExpression): - local_map = { - p: parameter_values_[circuit.parameters.data.index(p)] - for p in coeff.parameters - } - bound_coeff = coeff.bind(local_map) - else: - bound_coeff = coeff - # The original gradient is a sum of the gradients of the parameters in the - # gradient circuit multiplied by the coefficients. - gradient[i] += ( - float(bound_coeff) - * results.gradients[idx][g_parameter_indices[g_parameter]] - ) - gradients.append(gradient) - metadata.append({"parameters": parameters_}) - return EstimatorGradientResult( - gradients=gradients, metadata=metadata, options=results.options - ) - - @staticmethod - def _validate_arguments( - circuits: Sequence[QuantumCircuit], - observables: Sequence[BaseOperator | PauliSumOp], - parameter_values: Sequence[Sequence[float]], - parameters: Sequence[Sequence[Parameter]], - ) -> None: - """Validate the arguments of the ``run`` method. - - Args: - circuits: The list of quantum circuits to compute the gradients. - observables: The list of observables. - parameter_values: The list of parameter values to be bound to the circuit. - parameters: The sequence of parameters to calculate only the gradients of the specified - parameters. - - Raises: - ValueError: Invalid arguments are given. - """ - if len(circuits) != len(parameter_values): - raise ValueError( - f"The number of circuits ({len(circuits)}) does not match " - f"the number of parameter value sets ({len(parameter_values)})." - ) - - for i, (circuit, parameter_value) in enumerate(zip(circuits, parameter_values)): - if not circuit.num_parameters: - raise ValueError(f"The {i}-th circuit is not parameterised.") - if len(parameter_value) != circuit.num_parameters: - raise ValueError( - f"The number of values ({len(parameter_value)}) does not match " - f"the number of parameters ({circuit.num_parameters}) for the {i}-th circuit." - ) - - if len(circuits) != len(observables): - raise ValueError( - f"The number of circuits ({len(circuits)}) does not match " - f"the number of observables ({len(observables)})." - ) - - for i, (circuit, observable) in enumerate(zip(circuits, observables)): - if circuit.num_qubits != observable.num_qubits: - raise ValueError( - f"The number of qubits of the {i}-th circuit ({circuit.num_qubits}) does " - f"not match the number of qubits of the {i}-th observable " - f"({observable.num_qubits})." - ) - - if len(circuits) != len(parameters): - raise ValueError( - f"The number of circuits ({len(circuits)}) does not match " - f"the number of the list of specified parameters ({len(parameters)})." - ) - - for i, (circuit, parameters_) in enumerate(zip(circuits, parameters)): - if not set(parameters_).issubset(circuit.parameters): - raise ValueError( - f"The {i}-th parameters contains parameters not present in the " - f"{i}-th circuit." - ) - - @property - def options(self) -> Options: - """Return the union of estimator options setting and gradient default options, - where, if the same field is set in both, the gradient's default options override - the primitive's default setting. - - Returns: - The gradient default + estimator options. - """ - return self._get_local_options(self._default_options.__dict__) - - def update_default_options(self, **options): - """Update the gradient's default options setting. - - Args: - **options: The fields to update the default options. - """ - - self._default_options.update_options(**options) - - def _get_local_options(self, options: Options) -> Options: - """Return the union of the primitive's default setting, - the gradient default options, and the options in the ``run`` method. - The order of priority is: options in ``run`` method > gradient's - default options > primitive's default setting. - - Args: - options: The fields to update the options - - Returns: - The gradient default + estimator + run options. - """ - opts = copy(self._estimator.options) - opts.update_options(**options) - return opts diff --git a/qiskit/algorithms/gradients/base/base_qgt.py b/qiskit/algorithms/gradients/base/base_qgt.py deleted file mode 100644 index f2999a8f2bf0..000000000000 --- a/qiskit/algorithms/gradients/base/base_qgt.py +++ /dev/null @@ -1,383 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2022, 2023. -# -# 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. - -""" -Abstract base class of the Quantum Geometric Tensor (QGT). -""" - -from __future__ import annotations - -from abc import ABC, abstractmethod -from collections.abc import Sequence -from copy import copy - -import numpy as np - -from qiskit.circuit import Parameter, ParameterExpression, QuantumCircuit -from qiskit.primitives import BaseEstimator -from qiskit.primitives.utils import _circuit_key -from qiskit.providers import Options -from qiskit.transpiler.passes import TranslateParameterizedGates - -from .qgt_result import QGTResult -from ..utils import ( - DerivativeType, - GradientCircuit, - _assign_unique_parameters, - _make_gradient_parameters, - _make_gradient_parameter_values, -) - -from ...algorithm_job import AlgorithmJob - - -class BaseQGT(ABC): - r"""Base class to computes the Quantum Geometric Tensor (QGT) given a pure, - parameterized quantum state. QGT is defined as: - - .. math:: - - \mathrm{QGT}_{ij}= \langle \partial_i \psi | \partial_j \psi \rangle - - \langle\partial_i \psi | \psi \rangle \langle\psi | \partial_j \psi \rangle. - """ - - def __init__( - self, - estimator: BaseEstimator, - phase_fix: bool = True, - derivative_type: DerivativeType = DerivativeType.COMPLEX, - options: Options | None = None, - ): - r""" - Args: - estimator: The estimator used to compute the QGT. - phase_fix: Whether to calculate the second term (phase fix) of the QGT, which is - :math:`\langle\partial_i \psi | \psi \rangle \langle\psi | \partial_j \psi \rangle`. - Defaults to ``True``. - derivative_type: The type of derivative. Can be either ``DerivativeType.REAL`` - ``DerivativeType.IMAG``, or ``DerivativeType.COMPLEX``. Defaults to - ``DerivativeType.REAL``. - - - ``DerivativeType.REAL`` computes - - .. math:: - - \mathrm{Re(QGT)}_{ij}= \mathrm{Re}[\langle \partial_i \psi | \partial_j \psi \rangle - - \langle\partial_i \psi | \psi \rangle \langle\psi | \partial_j \psi \rangle]. - - - ``DerivativeType.IMAG`` computes - - .. math:: - - \mathrm{Im(QGT)}_{ij}= \mathrm{Im}[\langle \partial_i \psi | \partial_j \psi \rangle - - \langle\partial_i \psi | \psi \rangle \langle\psi | \partial_j \psi \rangle]. - - - ``DerivativeType.COMPLEX`` computes - - .. math:: - - \mathrm{QGT}_{ij}= [\langle \partial_i \psi | \partial_j \psi \rangle - - \langle\partial_i \psi | \psi \rangle \langle\psi | \partial_j \psi \rangle]. - - options: Backend runtime options used for circuit execution. The order of priority is: - options in ``run`` method > QGT's default options > primitive's default - setting. Higher priority setting overrides lower priority setting. - """ - self._estimator: BaseEstimator = estimator - self._phase_fix: bool = phase_fix - self._derivative_type: DerivativeType = derivative_type - self._default_options = Options() - if options is not None: - self._default_options.update_options(**options) - self._qgt_circuit_cache: dict[tuple, GradientCircuit] = {} - self._gradient_circuit_cache: dict[tuple, GradientCircuit] = {} - - @property - def derivative_type(self) -> DerivativeType: - """The derivative type.""" - return self._derivative_type - - @derivative_type.setter - def derivative_type(self, derivative_type: DerivativeType) -> None: - """Set the derivative type.""" - self._derivative_type = derivative_type - - def run( - self, - circuits: Sequence[QuantumCircuit], - parameter_values: Sequence[Sequence[float]], - parameters: Sequence[Sequence[Parameter] | None] | None = None, - **options, - ) -> AlgorithmJob: - """Run the job of the QGTs on the given circuits. - - Args: - circuits: The list of quantum circuits to compute the QGTs. - parameter_values: The list of parameter values to be bound to the circuit. - parameters: The sequence of parameters to calculate only the QGTs of - the specified parameters. Each sequence of parameters corresponds to a circuit in - ``circuits``. Defaults to None, which means that the QGTs of all parameters in - each circuit are calculated. - options: Primitive backend runtime options used for circuit execution. - The order of priority is: options in ``run`` method > QGT's - default options > primitive's default setting. - Higher priority setting overrides lower priority setting. - - Returns: - The job object of the QGTs of the expectation values. The i-th result corresponds to - ``circuits[i]`` evaluated with parameters bound as ``parameter_values[i]``. - - Raises: - ValueError: Invalid arguments are given. - """ - if isinstance(circuits, QuantumCircuit): - # Allow a single circuit to be passed in. - circuits = (circuits,) - - if parameters is None: - # If parameters is None, we calculate the gradients of all parameters in each circuit. - parameters = [circuit.parameters for circuit in circuits] - else: - # If parameters is not None, we calculate the gradients of the specified parameters. - # None in parameters means that the gradients of all parameters in the corresponding - # circuit are calculated. - parameters = [ - params if params is not None else circuits[i].parameters - for i, params in enumerate(parameters) - ] - # Validate the arguments. - self._validate_arguments(circuits, parameter_values, parameters) - # The priority of run option is as follows: - # options in ``run`` method > QGT's default options > primitive's default setting. - opts = copy(self._default_options) - opts.update_options(**options) - job = AlgorithmJob(self._run, circuits, parameter_values, parameters, **opts.__dict__) - job.submit() - return job - - @abstractmethod - def _run( - self, - circuits: Sequence[QuantumCircuit], - parameter_values: Sequence[Sequence[float]], - parameters: Sequence[Sequence[Parameter]], - **options, - ) -> QGTResult: - """Compute the QGTs on the given circuits.""" - raise NotImplementedError() - - def _preprocess( - self, - circuits: Sequence[QuantumCircuit], - parameter_values: Sequence[Sequence[float]], - parameters: Sequence[Sequence[Parameter]], - supported_gates: Sequence[str], - ) -> tuple[Sequence[QuantumCircuit], Sequence[Sequence[float]], Sequence[Sequence[Parameter]]]: - """Preprocess the gradient. This makes a gradient circuit for each circuit. The gradient - circuit is a transpiled circuit by using the supported gates, and has unique parameters. - ``parameter_values`` and ``parameters`` are also updated to match the gradient circuit. - - Args: - circuits: The list of quantum circuits to compute the gradients. - parameter_values: The list of parameter values to be bound to the circuit. - parameters: The sequence of parameters to calculate only the gradients of the specified - parameters. - supported_gates: The supported gates used to transpile the circuit. - - Returns: - The list of gradient circuits, the list of parameter values, and the list of parameters. - parameter_values and parameters are updated to match the gradient circuit. - """ - translator = TranslateParameterizedGates(supported_gates) - g_circuits, g_parameter_values, g_parameters = [], [], [] - for circuit, parameter_value_, parameters_ in zip(circuits, parameter_values, parameters): - circuit_key = _circuit_key(circuit) - if circuit_key not in self._gradient_circuit_cache: - unrolled = translator(circuit) - self._gradient_circuit_cache[circuit_key] = _assign_unique_parameters(unrolled) - gradient_circuit = self._gradient_circuit_cache[circuit_key] - g_circuits.append(gradient_circuit.gradient_circuit) - g_parameter_values.append( - _make_gradient_parameter_values(circuit, gradient_circuit, parameter_value_) - ) - g_parameters_ = [ - g_param - for g_param in gradient_circuit.gradient_circuit.parameters - if g_param in _make_gradient_parameters(gradient_circuit, parameters_) - ] - g_parameters.append(g_parameters_) - return g_circuits, g_parameter_values, g_parameters - - def _postprocess( - self, - results: QGTResult, - circuits: Sequence[QuantumCircuit], - parameter_values: Sequence[Sequence[float]], - parameters: Sequence[Sequence[Parameter]], - ) -> QGTResult: - """Postprocess the QGTs. This method computes the QGTs of the original circuits - by applying the chain rule to the QGTs of the circuits with unique parameters. - - Args: - results: The computed QGT for the circuits with unique parameters. - circuits: The list of original circuits submitted for gradient computation. - parameter_values: The list of parameter values to be bound to the circuits. - parameters: The sequence of parameters to calculate only the gradients of the specified - parameters. - - Returns: - The QGTs of the original circuits. - """ - qgts, metadata = [], [] - for idx, (circuit, parameter_values_, parameters_) in enumerate( - zip(circuits, parameter_values, parameters) - ): - dtype = complex if self.derivative_type == DerivativeType.COMPLEX else float - qgt: np.ndarray = np.zeros((len(parameters_), len(parameters_)), dtype=dtype) - - gradient_circuit = self._gradient_circuit_cache[_circuit_key(circuit)] - g_parameters = _make_gradient_parameters(gradient_circuit, parameters_) - # Make a map from the gradient parameter to the respective index in the gradient. - # parameters_ = [param for param in circuit.parameters if param in parameters_] - g_parameter_indices = [ - param - for param in gradient_circuit.gradient_circuit.parameters - if param in g_parameters - ] - g_parameter_indices = {param: i for i, param in enumerate(g_parameter_indices)} - rows, cols = np.triu_indices(len(parameters_)) - for row, col in zip(rows, cols): - for g_parameter1, coeff1 in gradient_circuit.parameter_map[parameters_[row]]: - for g_parameter2, coeff2 in gradient_circuit.parameter_map[parameters_[col]]: - if isinstance(coeff1, ParameterExpression): - local_map = { - p: parameter_values_[circuit.parameters.data.index(p)] - for p in coeff1.parameters - } - bound_coeff1 = coeff1.bind(local_map) - else: - bound_coeff1 = coeff1 - if isinstance(coeff2, ParameterExpression): - local_map = { - p: parameter_values_[circuit.parameters.data.index(p)] - for p in coeff2.parameters - } - bound_coeff2 = coeff2.bind(local_map) - else: - bound_coeff2 = coeff2 - qgt[row, col] += ( - float(bound_coeff1) - * float(bound_coeff2) - * results.qgts[idx][ - g_parameter_indices[g_parameter1], g_parameter_indices[g_parameter2] - ] - ) - - if self.derivative_type == DerivativeType.IMAG: - qgt += -1 * np.triu(qgt, k=1).T - else: - qgt += np.triu(qgt, k=1).conjugate().T - qgts.append(qgt) - metadata.append([{"parameters": parameters_}]) - return QGTResult( - qgts=qgts, - derivative_type=self.derivative_type, - metadata=metadata, - options=results.options, - ) - - @staticmethod - def _validate_arguments( - circuits: Sequence[QuantumCircuit], - parameter_values: Sequence[Sequence[float]], - parameters: Sequence[Sequence[Parameter]], - ) -> None: - """Validate the arguments of the ``run`` method. - - Args: - circuits: The list of quantum circuits to compute the QGTs. - parameter_values: The list of parameter values to be bound to the circuits. - parameters: The sequence of parameters with respect to which the QGTs should be - computed. - - Raises: - ValueError: Invalid arguments are given. - """ - if len(circuits) != len(parameter_values): - raise ValueError( - f"The number of circuits ({len(circuits)}) does not match " - f"the number of parameter values ({len(parameter_values)})." - ) - - if len(circuits) != len(parameters): - raise ValueError( - f"The number of circuits ({len(circuits)}) does not match " - f"the number of the specified parameter sets ({len(parameters)})." - ) - - for i, (circuit, parameter_value) in enumerate(zip(circuits, parameter_values)): - if not circuit.num_parameters: - raise ValueError(f"The {i}-th circuit is not parameterised.") - if len(parameter_value) != circuit.num_parameters: - raise ValueError( - f"The number of values ({len(parameter_value)}) does not match " - f"the number of parameters ({circuit.num_parameters}) for the {i}-th circuit." - ) - - if len(circuits) != len(parameters): - raise ValueError( - f"The number of circuits ({len(circuits)}) does not match " - f"the number of the list of specified parameters ({len(parameters)})." - ) - - for i, (circuit, parameters_) in enumerate(zip(circuits, parameters)): - if not set(parameters_).issubset(circuit.parameters): - raise ValueError( - f"The {i}-th parameters contains parameters not present in the " - f"{i}-th circuit." - ) - - @property - def options(self) -> Options: - """Return the union of estimator options setting and QGT default options, - where, if the same field is set in both, the QGT's default options override - the primitive's default setting. - - Returns: - The QGT default + estimator options. - """ - return self._get_local_options(self._default_options.__dict__) - - def update_default_options(self, **options): - """Update the gradient's default options setting. - - Args: - **options: The fields to update the default options. - """ - - self._default_options.update_options(**options) - - def _get_local_options(self, options: Options) -> Options: - """Return the union of the primitive's default setting, - the QGT default options, and the options in the ``run`` method. - The order of priority is: options in ``run`` method > QGT's default options > primitive's - default setting. - - Args: - options: The fields to update the options - - Returns: - The QGT default + estimator + run options. - """ - opts = copy(self._estimator.options) - opts.update_options(**options) - return opts diff --git a/qiskit/algorithms/gradients/base/base_sampler_gradient.py b/qiskit/algorithms/gradients/base/base_sampler_gradient.py deleted file mode 100644 index b4947365fd5d..000000000000 --- a/qiskit/algorithms/gradients/base/base_sampler_gradient.py +++ /dev/null @@ -1,296 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2022, 2023 -# -# 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. - -""" -Abstract base class of gradient for ``Sampler``. -""" - -from __future__ import annotations - -from abc import ABC, abstractmethod -from collections import defaultdict -from collections.abc import Sequence -from copy import copy - -from qiskit.circuit import Parameter, ParameterExpression, QuantumCircuit -from qiskit.primitives import BaseSampler -from qiskit.primitives.utils import _circuit_key -from qiskit.providers import Options -from qiskit.transpiler.passes import TranslateParameterizedGates - -from .sampler_gradient_result import SamplerGradientResult -from ..utils import ( - GradientCircuit, - _assign_unique_parameters, - _make_gradient_parameters, - _make_gradient_parameter_values, -) - -from ...algorithm_job import AlgorithmJob - - -class BaseSamplerGradient(ABC): - """Base class for a ``SamplerGradient`` to compute the gradients of the sampling probability.""" - - def __init__(self, sampler: BaseSampler, options: Options | None = None): - """ - Args: - sampler: The sampler used to compute the gradients. - options: Primitive backend runtime options used for circuit execution. - The order of priority is: options in ``run`` method > gradient's - default options > primitive's default setting. - Higher priority setting overrides lower priority setting - """ - self._sampler: BaseSampler = sampler - self._default_options = Options() - if options is not None: - self._default_options.update_options(**options) - self._gradient_circuit_cache: dict[tuple, GradientCircuit] = {} - - def run( - self, - circuits: Sequence[QuantumCircuit], - parameter_values: Sequence[Sequence[float]], - parameters: Sequence[Sequence[Parameter] | None] | None = None, - **options, - ) -> AlgorithmJob: - """Run the job of the sampler gradient on the given circuits. - - Args: - circuits: The list of quantum circuits to compute the gradients. - parameter_values: The list of parameter values to be bound to the circuit. - parameters: The sequence of parameters to calculate only the gradients of - the specified parameters. Each sequence of parameters corresponds to a circuit in - ``circuits``. Defaults to None, which means that the gradients of all parameters in - each circuit are calculated. None in the sequence means that the gradients of all - parameters in the corresponding circuit are calculated. - options: Primitive backend runtime options used for circuit execution. - The order of priority is: options in ``run`` method > gradient's - default options > primitive's default setting. - Higher priority setting overrides lower priority setting - Returns: - The job object of the gradients of the sampling probability. The i-th result - corresponds to ``circuits[i]`` evaluated with parameters bound as ``parameter_values[i]``. - The j-th quasi-probability distribution in the i-th result corresponds to the gradients of - the sampling probability for the j-th parameter in ``circuits[i]``. - - Raises: - ValueError: Invalid arguments are given. - """ - if isinstance(circuits, QuantumCircuit): - # Allow a single circuit to be passed in. - circuits = (circuits,) - if parameters is None: - # If parameters is None, we calculate the gradients of all parameters in each circuit. - parameters = [circuit.parameters for circuit in circuits] - else: - # If parameters is not None, we calculate the gradients of the specified parameters. - # None in parameters means that the gradients of all parameters in the corresponding - # circuit are calculated. - parameters = [ - params if params is not None else circuits[i].parameters - for i, params in enumerate(parameters) - ] - # Validate the arguments. - self._validate_arguments(circuits, parameter_values, parameters) - # The priority of run option is as follows: - # options in `run` method > gradient's default options > primitive's default options. - opts = copy(self._default_options) - opts.update_options(**options) - job = AlgorithmJob(self._run, circuits, parameter_values, parameters, **opts.__dict__) - job.submit() - return job - - @abstractmethod - def _run( - self, - circuits: Sequence[QuantumCircuit], - parameter_values: Sequence[Sequence[float]], - parameters: Sequence[Sequence[Parameter]], - **options, - ) -> SamplerGradientResult: - """Compute the sampler gradients on the given circuits.""" - raise NotImplementedError() - - def _preprocess( - self, - circuits: Sequence[QuantumCircuit], - parameter_values: Sequence[Sequence[float]], - parameters: Sequence[Sequence[Parameter]], - supported_gates: Sequence[str], - ) -> tuple[Sequence[QuantumCircuit], Sequence[Sequence[float]], Sequence[set[Parameter]]]: - """Preprocess the gradient. This makes a gradient circuit for each circuit. The gradient - circuit is a transpiled circuit by using the supported gates, and has unique parameters. - ``parameter_values`` and ``parameters`` are also updated to match the gradient circuit. - - Args: - circuits: The list of quantum circuits to compute the gradients. - parameter_values: The list of parameter values to be bound to the circuit. - parameters: The sequence of parameters to calculate only the gradients of the specified - parameters. - supported_gates: The supported gates used to transpile the circuit. - - Returns: - The list of gradient circuits, the list of parameter values, and the list of parameters. - parameter_values and parameters are updated to match the gradient circuit. - """ - translator = TranslateParameterizedGates(supported_gates) - g_circuits, g_parameter_values, g_parameters = [], [], [] - for circuit, parameter_value_, parameters_ in zip(circuits, parameter_values, parameters): - circuit_key = _circuit_key(circuit) - if circuit_key not in self._gradient_circuit_cache: - unrolled = translator(circuit) - self._gradient_circuit_cache[circuit_key] = _assign_unique_parameters(unrolled) - gradient_circuit = self._gradient_circuit_cache[circuit_key] - g_circuits.append(gradient_circuit.gradient_circuit) - g_parameter_values.append( - _make_gradient_parameter_values(circuit, gradient_circuit, parameter_value_) - ) - g_parameters.append(_make_gradient_parameters(gradient_circuit, parameters_)) - return g_circuits, g_parameter_values, g_parameters - - def _postprocess( - self, - results: SamplerGradientResult, - circuits: Sequence[QuantumCircuit], - parameter_values: Sequence[Sequence[float]], - parameters: Sequence[Sequence[Parameter] | None], - ) -> SamplerGradientResult: - """Postprocess the gradient. This computes the gradient of the original circuit from the - gradient of the gradient circuit by using the chain rule. - - Args: - results: The results of the gradient of the gradient circuits. - circuits: The list of quantum circuits to compute the gradients. - parameter_values: The list of parameter values to be bound to the circuit. - parameters: The sequence of parameters to calculate only the gradients of the specified - parameters. - - Returns: - The results of the gradient of the original circuits. - """ - gradients, metadata = [], [] - for idx, (circuit, parameter_values_, parameters_) in enumerate( - zip(circuits, parameter_values, parameters) - ): - gradient_circuit = self._gradient_circuit_cache[_circuit_key(circuit)] - g_parameters = _make_gradient_parameters(gradient_circuit, parameters_) - # Make a map from the gradient parameter to the respective index in the gradient. - g_parameter_indices = {param: i for i, param in enumerate(g_parameters)} - # Compute the original gradient from the gradient of the gradient circuit - # by using the chain rule. - gradient = [] - for parameter in parameters_: - grad_dist: dict[int, float] = defaultdict(float) - for g_parameter, coeff in gradient_circuit.parameter_map[parameter]: - # Compute the coefficient - if isinstance(coeff, ParameterExpression): - local_map = { - p: parameter_values_[circuit.parameters.data.index(p)] - for p in coeff.parameters - } - bound_coeff = coeff.bind(local_map) - else: - bound_coeff = coeff - # The original gradient is a sum of the gradients of the parameters in the - # gradient circuit multiplied by the coefficients. - unique_gradient = results.gradients[idx][g_parameter_indices[g_parameter]] - for key, value in unique_gradient.items(): - grad_dist[key] += float(bound_coeff) * value - gradient.append(dict(grad_dist)) - gradients.append(gradient) - metadata.append([{"parameters": parameters_}]) - return SamplerGradientResult( - gradients=gradients, metadata=metadata, options=results.options - ) - - @staticmethod - def _validate_arguments( - circuits: Sequence[QuantumCircuit], - parameter_values: Sequence[Sequence[float]], - parameters: Sequence[Sequence[Parameter]], - ) -> None: - """Validate the arguments of the ``run`` method. - - Args: - circuits: The list of quantum circuits to compute the gradients. - parameter_values: The list of parameter values to be bound to the circuit. - parameters: The sequence of parameters to calculate only the gradients of the specified - parameters. - - Raises: - ValueError: Invalid arguments are given. - """ - if len(circuits) != len(parameter_values): - raise ValueError( - f"The number of circuits ({len(circuits)}) does not match " - f"the number of parameter value sets ({len(parameter_values)})." - ) - - for i, (circuit, parameter_value) in enumerate(zip(circuits, parameter_values)): - if not circuit.num_parameters: - raise ValueError(f"The {i}-th circuit is not parameterised.") - - if len(parameter_value) != circuit.num_parameters: - raise ValueError( - f"The number of values ({len(parameter_value)}) does not match " - f"the number of parameters ({circuit.num_parameters}) for the {i}-th circuit." - ) - - if len(circuits) != len(parameters): - raise ValueError( - f"The number of circuits ({len(circuits)}) does not match " - f"the number of the specified parameter sets ({len(parameters)})." - ) - - for i, (circuit, parameters_) in enumerate(zip(circuits, parameters)): - if not set(parameters_).issubset(circuit.parameters): - raise ValueError( - f"The {i}-th parameter set contains parameters not present in the " - f"{i}-th circuit." - ) - - @property - def options(self) -> Options: - """Return the union of sampler options setting and gradient default options, - where, if the same field is set in both, the gradient's default options override - the primitive's default setting. - - Returns: - The gradient default + sampler options. - """ - return self._get_local_options(self._default_options.__dict__) - - def update_default_options(self, **options): - """Update the gradient's default options setting. - - Args: - **options: The fields to update the default options. - """ - - self._default_options.update_options(**options) - - def _get_local_options(self, options: Options) -> Options: - """Return the union of the primitive's default setting, - the gradient default options, and the options in the ``run`` method. - The order of priority is: options in ``run`` method > gradient's - default options > primitive's default setting. - - Args: - options: The fields to update the options - - Returns: - The gradient default + sampler + run options. - """ - opts = copy(self._sampler.options) - opts.update_options(**options) - return opts diff --git a/qiskit/algorithms/gradients/base/estimator_gradient_result.py b/qiskit/algorithms/gradients/base/estimator_gradient_result.py deleted file mode 100644 index ada3bdb2b7bf..000000000000 --- a/qiskit/algorithms/gradients/base/estimator_gradient_result.py +++ /dev/null @@ -1,35 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2022. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. -""" -Estimator result class -""" - -from __future__ import annotations - -from dataclasses import dataclass -from typing import Any - -import numpy as np - -from qiskit.providers import Options - - -@dataclass(frozen=True) -class EstimatorGradientResult: - """Result of EstimatorGradient.""" - - gradients: list[np.ndarray] - """The gradients of the expectation values.""" - metadata: list[dict[str, Any]] - """Additional information about the job.""" - options: Options - """Primitive runtime options for the execution of the job.""" diff --git a/qiskit/algorithms/gradients/base/qgt_result.py b/qiskit/algorithms/gradients/base/qgt_result.py deleted file mode 100644 index f110e1c68381..000000000000 --- a/qiskit/algorithms/gradients/base/qgt_result.py +++ /dev/null @@ -1,39 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2022, 2023. -# -# 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. -""" -QGT result class -""" - -from __future__ import annotations - -from dataclasses import dataclass -from typing import Any - -import numpy as np - -from qiskit.providers import Options - -from ..utils import DerivativeType - - -@dataclass(frozen=True) -class QGTResult: - """Result of QGT.""" - - qgts: list[np.ndarray] - """The QGT.""" - derivative_type: DerivativeType - """The type of derivative.""" - metadata: list[dict[str, Any]] - """Additional information about the job.""" - options: Options - """Primitive runtime options for the execution of the job.""" diff --git a/qiskit/algorithms/gradients/base/sampler_gradient_result.py b/qiskit/algorithms/gradients/base/sampler_gradient_result.py deleted file mode 100644 index b78b468f1b9f..000000000000 --- a/qiskit/algorithms/gradients/base/sampler_gradient_result.py +++ /dev/null @@ -1,33 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2022. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. -""" -Sampler result class -""" - -from __future__ import annotations - -from typing import Any -from dataclasses import dataclass - -from qiskit.providers import Options - - -@dataclass(frozen=True) -class SamplerGradientResult: - """Result of SamplerGradient.""" - - gradients: list[list[dict[int, float]]] - """The gradients of the sample probabilities.""" - metadata: list[dict[str, Any]] - """Additional information about the job.""" - options: Options - """Primitive runtime options for the execution of the job.""" diff --git a/qiskit/algorithms/gradients/finite_diff/__init__.py b/qiskit/algorithms/gradients/finite_diff/__init__.py deleted file mode 100644 index 8f5fd2a37f84..000000000000 --- a/qiskit/algorithms/gradients/finite_diff/__init__.py +++ /dev/null @@ -1,11 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2022, 2023 -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. diff --git a/qiskit/algorithms/gradients/finite_diff/finite_diff_estimator_gradient.py b/qiskit/algorithms/gradients/finite_diff/finite_diff_estimator_gradient.py deleted file mode 100644 index ea1b987c2ff2..000000000000 --- a/qiskit/algorithms/gradients/finite_diff/finite_diff_estimator_gradient.py +++ /dev/null @@ -1,148 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2022. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. - -"""Gradient of Sampler with Finite difference method.""" - -from __future__ import annotations - -from collections.abc import Sequence -from typing import Literal - -import numpy as np - -from qiskit.circuit import Parameter, QuantumCircuit -from qiskit.opflow import PauliSumOp -from qiskit.primitives import BaseEstimator -from qiskit.providers import Options -from qiskit.quantum_info.operators.base_operator import BaseOperator - -from ..base.base_estimator_gradient import BaseEstimatorGradient -from ..base.estimator_gradient_result import EstimatorGradientResult - -from ...exceptions import AlgorithmError - - -class FiniteDiffEstimatorGradient(BaseEstimatorGradient): - """ - Compute the gradients of the expectation values by finite difference method [1]. - - **Reference:** - [1] `Finite difference method `_ - """ - - def __init__( - self, - estimator: BaseEstimator, - epsilon: float, - options: Options | None = None, - *, - method: Literal["central", "forward", "backward"] = "central", - ): - r""" - Args: - estimator: The estimator used to compute the gradients. - epsilon: The offset size for the finite difference gradients. - options: Primitive backend runtime options used for circuit execution. - The order of priority is: options in ``run`` method > gradient's - default options > primitive's default setting. - Higher priority setting overrides lower priority setting - method: The computation method of the gradients. - - - ``central`` computes :math:`\frac{f(x+e)-f(x-e)}{2e}`, - - ``forward`` computes :math:`\frac{f(x+e) - f(x)}{e}`, - - ``backward`` computes :math:`\frac{f(x)-f(x-e)}{e}` - - where :math:`e` is epsilon. - - Raises: - ValueError: If ``epsilon`` is not positive. - TypeError: If ``method`` is invalid. - """ - if epsilon <= 0: - raise ValueError(f"epsilon ({epsilon}) should be positive.") - self._epsilon = epsilon - if method not in ("central", "forward", "backward"): - raise TypeError( - f"The argument method should be central, forward, or backward: {method} is given." - ) - self._method = method - super().__init__(estimator, options) - - def _run( - self, - circuits: Sequence[QuantumCircuit], - observables: Sequence[BaseOperator | PauliSumOp], - parameter_values: Sequence[Sequence[float]], - parameters: Sequence[Sequence[Parameter]], - **options, - ) -> EstimatorGradientResult: - """Compute the estimator gradients on the given circuits.""" - job_circuits, job_observables, job_param_values, metadata = [], [], [], [] - all_n = [] - - for circuit, observable, parameter_values_, parameters_ in zip( - circuits, observables, parameter_values, parameters - ): - # Indices of parameters to be differentiated - indices = [circuit.parameters.data.index(p) for p in parameters_] - metadata.append({"parameters": parameters_}) - - # Combine inputs into a single job to reduce overhead. - offset = np.identity(circuit.num_parameters)[indices, :] - if self._method == "central": - plus = parameter_values_ + self._epsilon * offset - minus = parameter_values_ - self._epsilon * offset - n = 2 * len(indices) - job_circuits.extend([circuit] * n) - job_observables.extend([observable] * n) - job_param_values.extend(plus.tolist() + minus.tolist()) - all_n.append(n) - elif self._method == "forward": - plus = parameter_values_ + self._epsilon * offset - n = len(indices) + 1 - job_circuits.extend([circuit] * n) - job_observables.extend([observable] * n) - job_param_values.extend([parameter_values_] + plus.tolist()) - all_n.append(n) - elif self._method == "backward": - minus = parameter_values_ - self._epsilon * offset - n = len(indices) + 1 - job_circuits.extend([circuit] * n) - job_observables.extend([observable] * n) - job_param_values.extend([parameter_values_] + minus.tolist()) - all_n.append(n) - - # Run the single job with all circuits. - job = self._estimator.run(job_circuits, job_observables, job_param_values, **options) - try: - results = job.result() - except Exception as exc: - raise AlgorithmError("Estimator job failed.") from exc - - # Compute the gradients - gradients = [] - partial_sum_n = 0 - for n in all_n: - if self._method == "central": - result = results.values[partial_sum_n : partial_sum_n + n] - gradient = (result[: n // 2] - result[n // 2 :]) / (2 * self._epsilon) - elif self._method == "forward": - result = results.values[partial_sum_n : partial_sum_n + n] - gradient = (result[1:] - result[0]) / self._epsilon - elif self._method == "backward": - result = results.values[partial_sum_n : partial_sum_n + n] - gradient = (result[0] - result[1:]) / self._epsilon - partial_sum_n += n - gradients.append(gradient) - - opt = self._get_local_options(options) - return EstimatorGradientResult(gradients=gradients, metadata=metadata, options=opt) diff --git a/qiskit/algorithms/gradients/finite_diff/finite_diff_sampler_gradient.py b/qiskit/algorithms/gradients/finite_diff/finite_diff_sampler_gradient.py deleted file mode 100644 index bc250286c828..000000000000 --- a/qiskit/algorithms/gradients/finite_diff/finite_diff_sampler_gradient.py +++ /dev/null @@ -1,161 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2022. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. - -"""Gradient of Sampler with Finite difference method.""" - -from __future__ import annotations - -from collections import defaultdict -from typing import Literal, Sequence - -import numpy as np - -from qiskit.circuit import Parameter, QuantumCircuit -from qiskit.primitives import BaseSampler -from qiskit.providers import Options - -from ..base.base_sampler_gradient import BaseSamplerGradient -from ..base.sampler_gradient_result import SamplerGradientResult - -from ...exceptions import AlgorithmError - - -class FiniteDiffSamplerGradient(BaseSamplerGradient): - """ - Compute the gradients of the sampling probability by finite difference method [1]. - - **Reference:** - [1] `Finite difference method `_ - """ - - def __init__( - self, - sampler: BaseSampler, - epsilon: float, - options: Options | None = None, - *, - method: Literal["central", "forward", "backward"] = "central", - ): - r""" - Args: - sampler: The sampler used to compute the gradients. - epsilon: The offset size for the finite difference gradients. - options: Primitive backend runtime options used for circuit execution. - The order of priority is: options in ``run`` method > gradient's - default options > primitive's default setting. - Higher priority setting overrides lower priority setting - method: The computation method of the gradients. - - - ``central`` computes :math:`\frac{f(x+e)-f(x-e)}{2e}`, - - ``forward`` computes :math:`\frac{f(x+e) - f(x)}{e}`, - - ``backward`` computes :math:`\frac{f(x)-f(x-e)}{e}` - - where :math:`e` is epsilon. - - Raises: - ValueError: If ``epsilon`` is not positive. - TypeError: If ``method`` is invalid. - """ - if epsilon <= 0: - raise ValueError(f"epsilon ({epsilon}) should be positive.") - self._epsilon = epsilon - if method not in ("central", "forward", "backward"): - raise TypeError( - f"The argument method should be central, forward, or backward: {method} is given." - ) - self._method = method - super().__init__(sampler, options) - - def _run( - self, - circuits: Sequence[QuantumCircuit], - parameter_values: Sequence[Sequence[float]], - parameters: Sequence[Sequence[Parameter]], - **options, - ) -> SamplerGradientResult: - """Compute the sampler gradients on the given circuits.""" - job_circuits, job_param_values, metadata = [], [], [] - all_n = [] - for circuit, parameter_values_, parameters_ in zip(circuits, parameter_values, parameters): - # Indices of parameters to be differentiated - indices = [circuit.parameters.data.index(p) for p in parameters_] - metadata.append({"parameters": parameters_}) - # Combine inputs into a single job to reduce overhead. - offset = np.identity(circuit.num_parameters)[indices, :] - if self._method == "central": - plus = parameter_values_ + self._epsilon * offset - minus = parameter_values_ - self._epsilon * offset - n = 2 * len(indices) - job_circuits.extend([circuit] * n) - job_param_values.extend(plus.tolist() + minus.tolist()) - all_n.append(n) - elif self._method == "forward": - plus = parameter_values_ + self._epsilon * offset - n = len(indices) + 1 - job_circuits.extend([circuit] * n) - job_param_values.extend([parameter_values_] + plus.tolist()) - all_n.append(n) - elif self._method == "backward": - minus = parameter_values_ - self._epsilon * offset - n = len(indices) + 1 - job_circuits.extend([circuit] * n) - job_param_values.extend([parameter_values_] + minus.tolist()) - all_n.append(n) - - # Run the single job with all circuits. - job = self._sampler.run(job_circuits, job_param_values, **options) - try: - results = job.result() - except Exception as exc: - raise AlgorithmError("Sampler job failed.") from exc - - # Compute the gradients. - gradients = [] - partial_sum_n = 0 - for n in all_n: - gradient = [] - if self._method == "central": - result = results.quasi_dists[partial_sum_n : partial_sum_n + n] - for dist_plus, dist_minus in zip(result[: n // 2], result[n // 2 :]): - grad_dist: dict[int, float] = defaultdict(float) - for key, value in dist_plus.items(): - grad_dist[key] += value / (2 * self._epsilon) - for key, value in dist_minus.items(): - grad_dist[key] -= value / (2 * self._epsilon) - gradient.append(dict(grad_dist)) - elif self._method == "forward": - result = results.quasi_dists[partial_sum_n : partial_sum_n + n] - dist_zero = result[0] - for dist_plus in result[1:]: - grad_dist = defaultdict(float) - for key, value in dist_plus.items(): - grad_dist[key] += value / self._epsilon - for key, value in dist_zero.items(): - grad_dist[key] -= value / self._epsilon - gradient.append(dict(grad_dist)) - - elif self._method == "backward": - result = results.quasi_dists[partial_sum_n : partial_sum_n + n] - dist_zero = result[0] - for dist_minus in result[1:]: - grad_dist = defaultdict(float) - for key, value in dist_zero.items(): - grad_dist[key] += value / self._epsilon - for key, value in dist_minus.items(): - grad_dist[key] -= value / self._epsilon - gradient.append(dict(grad_dist)) - - partial_sum_n += n - gradients.append(gradient) - - opt = self._get_local_options(options) - return SamplerGradientResult(gradients=gradients, metadata=metadata, options=opt) diff --git a/qiskit/algorithms/gradients/lin_comb/__init__.py b/qiskit/algorithms/gradients/lin_comb/__init__.py deleted file mode 100644 index 8f5fd2a37f84..000000000000 --- a/qiskit/algorithms/gradients/lin_comb/__init__.py +++ /dev/null @@ -1,11 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2022, 2023 -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. diff --git a/qiskit/algorithms/gradients/lin_comb/lin_comb_estimator_gradient.py b/qiskit/algorithms/gradients/lin_comb/lin_comb_estimator_gradient.py deleted file mode 100644 index 93deb48b5820..000000000000 --- a/qiskit/algorithms/gradients/lin_comb/lin_comb_estimator_gradient.py +++ /dev/null @@ -1,195 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2022, 2023. -# -# 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. -""" -Gradient of probabilities with linear combination of unitaries (LCU) -""" - -from __future__ import annotations - -from collections.abc import Sequence - -import numpy as np - -from qiskit.circuit import Parameter, QuantumCircuit -from qiskit.opflow import PauliSumOp -from qiskit.primitives import BaseEstimator -from qiskit.primitives.utils import init_observable, _circuit_key -from qiskit.providers import Options -from qiskit.quantum_info.operators.base_operator import BaseOperator - -from ..base.base_estimator_gradient import BaseEstimatorGradient -from ..base.estimator_gradient_result import EstimatorGradientResult -from ..utils import DerivativeType, _make_lin_comb_gradient_circuit, _make_lin_comb_observables - -from ...exceptions import AlgorithmError - - -class LinCombEstimatorGradient(BaseEstimatorGradient): - """Compute the gradients of the expectation values. - This method employs a linear combination of unitaries [1]. - - **Reference:** - [1] Schuld et al., Evaluating analytic gradients on quantum hardware, 2018 - `arXiv:1811.11184 `_ - """ - - SUPPORTED_GATES = [ - "rx", - "ry", - "rz", - "rzx", - "rzz", - "ryy", - "rxx", - "cx", - "cy", - "cz", - "ccx", - "swap", - "iswap", - "h", - "t", - "s", - "sdg", - "x", - "y", - "z", - ] - - def __init__( - self, - estimator: BaseEstimator, - derivative_type: DerivativeType = DerivativeType.REAL, - options: Options | None = None, - ): - r""" - Args: - estimator: The estimator used to compute the gradients. - derivative_type: The type of derivative. Can be either ``DerivativeType.REAL`` - ``DerivativeType.IMAG``, or ``DerivativeType.COMPLEX``. Defaults to - ``DerivativeType.REAL``. - - - ``DerivativeType.REAL`` computes :math:`2 \mathrm{Re}[⟨ψ(ω)|O(θ)|dω ψ(ω)〉]`. - - ``DerivativeType.IMAG`` computes :math:`2 \mathrm{Im}[⟨ψ(ω)|O(θ)|dω ψ(ω)〉]`. - - ``DerivativeType.COMPLEX`` computes :math:`2 ⟨ψ(ω)|O(θ)|dω ψ(ω)〉`. - - options: Primitive backend runtime options used for circuit execution. - The order of priority is: options in ``run`` method > gradient's - default options > primitive's default setting. - Higher priority setting overrides lower priority setting. - """ - self._lin_comb_cache: dict[tuple, dict[Parameter, QuantumCircuit]] = {} - super().__init__(estimator, options, derivative_type=derivative_type) - - @BaseEstimatorGradient.derivative_type.setter - def derivative_type(self, derivative_type: DerivativeType) -> None: - """Set the derivative type.""" - self._derivative_type = derivative_type - - def _run( - self, - circuits: Sequence[QuantumCircuit], - observables: Sequence[BaseOperator | PauliSumOp], - parameter_values: Sequence[Sequence[float]], - parameters: Sequence[Sequence[Parameter]], - **options, - ) -> EstimatorGradientResult: - """Compute the estimator gradients on the given circuits.""" - g_circuits, g_parameter_values, g_parameters = self._preprocess( - circuits, parameter_values, parameters, self.SUPPORTED_GATES - ) - results = self._run_unique( - g_circuits, observables, g_parameter_values, g_parameters, **options - ) - return self._postprocess(results, circuits, parameter_values, parameters) - - def _run_unique( - self, - circuits: Sequence[QuantumCircuit], - observables: Sequence[BaseOperator | PauliSumOp], - parameter_values: Sequence[Sequence[float]], - parameters: Sequence[Sequence[Parameter]], - **options, - ) -> EstimatorGradientResult: - """Compute the estimator gradients on the given circuits.""" - job_circuits, job_observables, job_param_values, metadata = [], [], [], [] - all_n = [] - for circuit, observable, parameter_values_, parameters_ in zip( - circuits, observables, parameter_values, parameters - ): - # Prepare circuits for the gradient of the specified parameters. - meta = {"parameters": parameters_} - circuit_key = _circuit_key(circuit) - if circuit_key not in self._lin_comb_cache: - # Cache the circuits for the linear combination of unitaries. - # We only cache the circuits for the specified parameters in the future. - self._lin_comb_cache[circuit_key] = _make_lin_comb_gradient_circuit( - circuit, add_measurement=False - ) - lin_comb_circuits = self._lin_comb_cache[circuit_key] - gradient_circuits = [] - for param in parameters_: - gradient_circuits.append(lin_comb_circuits[param]) - n = len(gradient_circuits) - # Make the observable as :class:`~qiskit.quantum_info.SparsePauliOp` and - # add an ancillary operator to compute the gradient. - observable = init_observable(observable) - observable_1, observable_2 = _make_lin_comb_observables( - observable, self._derivative_type - ) - # If its derivative type is `DerivativeType.COMPLEX`, calculate the gradient - # of the real and imaginary parts separately. - meta["derivative_type"] = self.derivative_type - metadata.append(meta) - # Combine inputs into a single job to reduce overhead. - if self._derivative_type == DerivativeType.COMPLEX: - job_circuits.extend(gradient_circuits * 2) - job_observables.extend([observable_1] * n + [observable_2] * n) - job_param_values.extend([parameter_values_] * 2 * n) - all_n.append(2 * n) - else: - job_circuits.extend(gradient_circuits) - job_observables.extend([observable_1] * n) - job_param_values.extend([parameter_values_] * n) - all_n.append(n) - - # Run the single job with all circuits. - job = self._estimator.run( - job_circuits, - job_observables, - job_param_values, - **options, - ) - try: - results = job.result() - except AlgorithmError as exc: - raise AlgorithmError("Estimator job failed.") from exc - - # Compute the gradients. - gradients = [] - partial_sum_n = 0 - for n in all_n: - # this disable is needed as Pylint does not understand derivative_type is a property if - # it is only defined in the base class and the getter is in the child - # pylint: disable=comparison-with-callable - if self.derivative_type == DerivativeType.COMPLEX: - gradient = np.zeros(n // 2, dtype="complex") - gradient.real = results.values[partial_sum_n : partial_sum_n + n // 2] - gradient.imag = results.values[partial_sum_n + n // 2 : partial_sum_n + n] - - else: - gradient = np.real(results.values[partial_sum_n : partial_sum_n + n]) - partial_sum_n += n - gradients.append(gradient) - - opt = self._get_local_options(options) - return EstimatorGradientResult(gradients=gradients, metadata=metadata, options=opt) diff --git a/qiskit/algorithms/gradients/lin_comb/lin_comb_qgt.py b/qiskit/algorithms/gradients/lin_comb/lin_comb_qgt.py deleted file mode 100644 index 0a9e05a9344a..000000000000 --- a/qiskit/algorithms/gradients/lin_comb/lin_comb_qgt.py +++ /dev/null @@ -1,257 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2022, 2023. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. -""" -A class for the Linear Combination Quantum Gradient Tensor. -""" - -from __future__ import annotations - -from collections.abc import Sequence - -import numpy as np - -from qiskit.circuit import Parameter, QuantumCircuit -from qiskit.primitives import BaseEstimator -from qiskit.primitives.utils import _circuit_key -from qiskit.providers import Options -from qiskit.quantum_info import SparsePauliOp - -from ..base.base_qgt import BaseQGT -from .lin_comb_estimator_gradient import LinCombEstimatorGradient -from ..base.qgt_result import QGTResult -from ..utils import DerivativeType, _make_lin_comb_qgt_circuit, _make_lin_comb_observables - -from ...exceptions import AlgorithmError - - -class LinCombQGT(BaseQGT): - """Computes the Quantum Geometric Tensor (QGT) given a pure, parameterized quantum state. - - This method employs a linear combination of unitaries [1]. - - **Reference:** - - [1]: Schuld et al., "Evaluating analytic gradients on quantum hardware" (2018). - `arXiv:1811.11184 `_ - """ - - SUPPORTED_GATES = [ - "rx", - "ry", - "rz", - "rzx", - "rzz", - "ryy", - "rxx", - "cx", - "cy", - "cz", - "ccx", - "swap", - "iswap", - "h", - "t", - "s", - "sdg", - "x", - "y", - "z", - ] - - def __init__( - self, - estimator: BaseEstimator, - phase_fix: bool = True, - derivative_type: DerivativeType = DerivativeType.COMPLEX, - options: Options | None = None, - ): - r""" - Args: - estimator: The estimator used to compute the QGT. - phase_fix: Whether to calculate the second term (phase fix) of the QGT, which is - :math:`\langle\partial_i \psi | \psi \rangle \langle\psi | \partial_j \psi \rangle`. - Default to ``True``. - derivative_type: The type of derivative. Can be either ``DerivativeType.REAL`` - ``DerivativeType.IMAG``, or ``DerivativeType.COMPLEX``. Defaults to - ``DerivativeType.REAL``. - - - ``DerivativeType.REAL`` computes - - .. math:: - - \mathrm{Re(QGT)}_{ij}= \mathrm{Re}[\langle \partial_i \psi | \partial_j \psi \rangle - - \langle\partial_i \psi | \psi \rangle \langle\psi | \partial_j \psi \rangle]. - - - ``DerivativeType.IMAG`` computes - - .. math:: - - \mathrm{Re(QGT)}_{ij}= \mathrm{Im}[\langle \partial_i \psi | \partial_j \psi \rangle - - \langle\partial_i \psi | \psi \rangle \langle\psi | \partial_j \psi \rangle]. - - - ``DerivativeType.COMPLEX`` computes - - .. math:: - - \mathrm{QGT}_{ij}= [\langle \partial_i \psi | \partial_j \psi \rangle - - \langle\partial_i \psi | \psi \rangle \langle\psi | \partial_j \psi \rangle]. - - options: Backend runtime options used for circuit execution. The order of priority is: - options in ``run`` method > QGT's default options > primitive's default - setting. Higher priority setting overrides lower priority setting. - """ - super().__init__(estimator, phase_fix, derivative_type, options=options) - self._gradient = LinCombEstimatorGradient( - estimator, derivative_type=DerivativeType.COMPLEX, options=options - ) - self._lin_comb_qgt_circuit_cache: dict[ - tuple, dict[tuple[Parameter, Parameter], QuantumCircuit] - ] = {} - - def _run( - self, - circuits: Sequence[QuantumCircuit], - parameter_values: Sequence[Sequence[float]], - parameters: Sequence[Sequence[Parameter]], - **options, - ) -> QGTResult: - """Compute the QGT on the given circuits.""" - g_circuits, g_parameter_values, g_parameters = self._preprocess( - circuits, parameter_values, parameters, self.SUPPORTED_GATES - ) - results = self._run_unique(g_circuits, g_parameter_values, g_parameters, **options) - return self._postprocess(results, circuits, parameter_values, parameters) - - def _run_unique( - self, - circuits: Sequence[QuantumCircuit], - parameter_values: Sequence[Sequence[float]], - parameters: Sequence[Sequence[Parameter]], - **options, - ) -> QGTResult: - """Compute the QGTs on the given circuits.""" - job_circuits, job_observables, job_param_values, metadata = [], [], [], [] - all_n, all_m, phase_fixes = [], [], [] - - for circuit, parameter_values_, parameters_ in zip(circuits, parameter_values, parameters): - # Prepare circuits for the gradient of the specified parameters. - parameters_ = [p for p in circuit.parameters if p in parameters_] - meta = {"parameters": parameters_} - metadata.append(meta) - - # Compute the first term in the QGT - circuit_key = _circuit_key(circuit) - if circuit_key not in self._lin_comb_qgt_circuit_cache: - # generate the all of the circuits for the first term in the QGT and cache them. - # Only the circuit related to specified parameters will be executed. - # In the future, we can generate the specified circuits on demand. - self._lin_comb_qgt_circuit_cache[circuit_key] = _make_lin_comb_qgt_circuit(circuit) - lin_comb_qgt_circuits = self._lin_comb_qgt_circuit_cache[circuit_key] - - qgt_circuits = [] - rows, cols = np.triu_indices(len(parameters_)) - for row, col in zip(rows, cols): - param_i = parameters_[row] - param_j = parameters_[col] - qgt_circuits.append(lin_comb_qgt_circuits[(param_i, param_j)]) - - observable = SparsePauliOp.from_list([("I" * circuit.num_qubits, 1)]) - observable_1, observable_2 = _make_lin_comb_observables( - observable, self._derivative_type - ) - - n = len(qgt_circuits) - if self._derivative_type == DerivativeType.COMPLEX: - job_circuits.extend(qgt_circuits * 2) - job_observables.extend([observable_1] * n + [observable_2] * n) - job_param_values.extend([parameter_values_] * 2 * n) - all_m.append(len(parameters_)) - all_n.append(2 * n) - else: - job_circuits.extend(qgt_circuits) - job_observables.extend([observable_1] * n) - job_param_values.extend([parameter_values_] * n) - all_m.append(len(parameters_)) - all_n.append(n) - - # Run the single job with all circuits. - job = self._estimator.run( - job_circuits, - job_observables, - job_param_values, - **options, - ) - - if self._phase_fix: - # Compute the second term in the QGT if phase fix is enabled. - phase_fix_obs = [ - SparsePauliOp.from_list([("I" * circuit.num_qubits, 1)]) for circuit in circuits - ] - phase_fix_job = self._gradient.run( - circuits=circuits, - observables=phase_fix_obs, - parameter_values=parameter_values, - parameters=parameters, - **options, - ) - - try: - results = job.result() - if self._phase_fix: - gradient_results = phase_fix_job.result() - except AlgorithmError as exc: - raise AlgorithmError("Estimator job or gradient job failed.") from exc - - # Compute the phase fix - if self._phase_fix: - for gradient in gradient_results.gradients: - phase_fix = np.outer(np.conjugate(gradient), gradient) - # Select the real or imaginary part of the phase fix if needed - if self.derivative_type == DerivativeType.REAL: - phase_fix = np.real(phase_fix) - elif self.derivative_type == DerivativeType.IMAG: - phase_fix = np.imag(phase_fix) - phase_fixes.append(phase_fix) - else: - phase_fixes = [0 for i in range(len(circuits))] - # Compute the QGT - qgts = [] - partial_sum_n = 0 - for i, (n, m) in enumerate(zip(all_n, all_m)): - qgt = np.zeros((m, m), dtype="complex") - # Compute the first term in the QGT - if self.derivative_type == DerivativeType.COMPLEX: - qgt[np.triu_indices(m)] = results.values[partial_sum_n : partial_sum_n + n // 2] - qgt[np.triu_indices(m)] += ( - 1j * results.values[partial_sum_n + n // 2 : partial_sum_n + n] - ) - elif self.derivative_type == DerivativeType.REAL: - qgt[np.triu_indices(m)] = results.values[partial_sum_n : partial_sum_n + n] - elif self.derivative_type == DerivativeType.IMAG: - qgt[np.triu_indices(m)] = 1j * results.values[partial_sum_n : partial_sum_n + n] - - # Add the conjugate of the upper triangle to the lower triangle - qgt += np.triu(qgt, k=1).conjugate().T - if self.derivative_type == DerivativeType.REAL: - qgt = np.real(qgt) - elif self.derivative_type == DerivativeType.IMAG: - qgt = np.imag(qgt) - - # Subtract the phase fix from the QGT - qgt = qgt - phase_fixes[i] - partial_sum_n += n - qgts.append(qgt / 4) - - opt = self._get_local_options(options) - return QGTResult( - qgts=qgts, derivative_type=self.derivative_type, metadata=metadata, options=opt - ) diff --git a/qiskit/algorithms/gradients/lin_comb/lin_comb_sampler_gradient.py b/qiskit/algorithms/gradients/lin_comb/lin_comb_sampler_gradient.py deleted file mode 100644 index 759e77d460bb..000000000000 --- a/qiskit/algorithms/gradients/lin_comb/lin_comb_sampler_gradient.py +++ /dev/null @@ -1,147 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2022. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. -""" -Gradient of probabilities with linear combination of unitaries (LCU) -""" - -from __future__ import annotations - -from collections import defaultdict -from collections.abc import Sequence - -from qiskit.circuit import Parameter, QuantumCircuit -from qiskit.primitives import BaseSampler -from qiskit.primitives.utils import _circuit_key -from qiskit.providers import Options - -from ..base.base_sampler_gradient import BaseSamplerGradient -from ..base.sampler_gradient_result import SamplerGradientResult -from ..utils import _make_lin_comb_gradient_circuit - -from ...exceptions import AlgorithmError - - -class LinCombSamplerGradient(BaseSamplerGradient): - """Compute the gradients of the sampling probability. - This method employs a linear combination of unitaries [1]. - - **Reference:** - [1] Schuld et al., Evaluating analytic gradients on quantum hardware, 2018 - `arXiv:1811.11184 `_ - """ - - SUPPORTED_GATES = [ - "rx", - "ry", - "rz", - "rzx", - "rzz", - "ryy", - "rxx", - "cx", - "cy", - "cz", - "ccx", - "swap", - "iswap", - "h", - "t", - "s", - "sdg", - "x", - "y", - "z", - ] - - def __init__(self, sampler: BaseSampler, options: Options | None = None): - """ - Args: - sampler: The sampler used to compute the gradients. - options: Primitive backend runtime options used for circuit execution. - The order of priority is: options in ``run`` method > gradient's - default options > primitive's default setting. - Higher priority setting overrides lower priority setting - """ - self._lin_comb_cache: dict[tuple, dict[Parameter, QuantumCircuit]] = {} - super().__init__(sampler, options) - - def _run( - self, - circuits: Sequence[QuantumCircuit], - parameter_values: Sequence[Sequence[float]], - parameters: Sequence[Sequence[Parameter]], - **options, - ) -> SamplerGradientResult: - """Compute the estimator gradients on the given circuits.""" - g_circuits, g_parameter_values, g_parameters = self._preprocess( - circuits, parameter_values, parameters, self.SUPPORTED_GATES - ) - results = self._run_unique(g_circuits, g_parameter_values, g_parameters, **options) - return self._postprocess(results, circuits, parameter_values, parameters) - - def _run_unique( - self, - circuits: Sequence[QuantumCircuit], - parameter_values: Sequence[Sequence[float]], - parameters: Sequence[Sequence[Parameter]], - **options, - ) -> SamplerGradientResult: - """Compute the sampler gradients on the given circuits.""" - job_circuits, job_param_values, metadata = [], [], [] - all_n = [] - for circuit, parameter_values_, parameters_ in zip(circuits, parameter_values, parameters): - # Prepare circuits for the gradient of the specified parameters. - metadata.append({"parameters": parameters_}) - circuit_key = _circuit_key(circuit) - if circuit_key not in self._lin_comb_cache: - # Cache the circuits for the linear combination of unitaries. - # We only cache the circuits for the specified parameters in the future. - self._lin_comb_cache[circuit_key] = _make_lin_comb_gradient_circuit( - circuit, add_measurement=True - ) - lin_comb_circuits = self._lin_comb_cache[circuit_key] - gradient_circuits = [] - for param in parameters_: - gradient_circuits.append(lin_comb_circuits[param]) - # Combine inputs into a single job to reduce overhead. - n = len(gradient_circuits) - job_circuits.extend(gradient_circuits) - job_param_values.extend([parameter_values_] * n) - all_n.append(n) - - # Run the single job with all circuits. - job = self._sampler.run(job_circuits, job_param_values, **options) - try: - results = job.result() - except Exception as exc: - raise AlgorithmError("Sampler job failed.") from exc - - # Compute the gradients. - gradients = [] - partial_sum_n = 0 - for i, n in enumerate(all_n): - gradient = [] - result = results.quasi_dists[partial_sum_n : partial_sum_n + n] - m = 2 ** circuits[i].num_qubits - for dist in result: - grad_dist: dict[int, float] = defaultdict(float) - for key, value in dist.items(): - if key < m: - grad_dist[key] += value - else: - grad_dist[key - m] -= value - gradient.append(dict(grad_dist)) - gradients.append(gradient) - partial_sum_n += n - - opt = self._get_local_options(options) - return SamplerGradientResult(gradients=gradients, metadata=metadata, options=opt) diff --git a/qiskit/algorithms/gradients/param_shift/__init__.py b/qiskit/algorithms/gradients/param_shift/__init__.py deleted file mode 100644 index 8f5fd2a37f84..000000000000 --- a/qiskit/algorithms/gradients/param_shift/__init__.py +++ /dev/null @@ -1,11 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2022, 2023 -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. diff --git a/qiskit/algorithms/gradients/param_shift/param_shift_estimator_gradient.py b/qiskit/algorithms/gradients/param_shift/param_shift_estimator_gradient.py deleted file mode 100644 index ef334a291e6e..000000000000 --- a/qiskit/algorithms/gradients/param_shift/param_shift_estimator_gradient.py +++ /dev/null @@ -1,123 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2022. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. -""" -Gradient of probabilities with parameter shift -""" - -from __future__ import annotations - -from collections.abc import Sequence - -from qiskit.circuit import Parameter, QuantumCircuit -from qiskit.opflow import PauliSumOp -from qiskit.quantum_info.operators.base_operator import BaseOperator - -from ..base.base_estimator_gradient import BaseEstimatorGradient -from ..base.estimator_gradient_result import EstimatorGradientResult -from ..utils import _make_param_shift_parameter_values - -from ...exceptions import AlgorithmError - - -class ParamShiftEstimatorGradient(BaseEstimatorGradient): - """ - Compute the gradients of the expectation values by the parameter shift rule [1]. - - **Reference:** - [1] Schuld, M., Bergholm, V., Gogolin, C., Izaac, J., and Killoran, N. Evaluating analytic - gradients on quantum hardware, `DOI `_ - """ - - SUPPORTED_GATES = [ - "x", - "y", - "z", - "h", - "rx", - "ry", - "rz", - "p", - "cx", - "cy", - "cz", - "ryy", - "rxx", - "rzz", - "rzx", - ] - - def _run( - self, - circuits: Sequence[QuantumCircuit], - observables: Sequence[BaseOperator | PauliSumOp], - parameter_values: Sequence[Sequence[float]], - parameters: Sequence[Sequence[Parameter]], - **options, - ) -> EstimatorGradientResult: - """Compute the gradients of the expectation values by the parameter shift rule.""" - g_circuits, g_parameter_values, g_parameters = self._preprocess( - circuits, parameter_values, parameters, self.SUPPORTED_GATES - ) - results = self._run_unique( - g_circuits, observables, g_parameter_values, g_parameters, **options - ) - return self._postprocess(results, circuits, parameter_values, parameters) - - def _run_unique( - self, - circuits: Sequence[QuantumCircuit], - observables: Sequence[BaseOperator | PauliSumOp], - parameter_values: Sequence[Sequence[float]], - parameters: Sequence[Sequence[Parameter]], - **options, - ) -> EstimatorGradientResult: - """Compute the estimator gradients on the given circuits.""" - job_circuits, job_observables, job_param_values, metadata = [], [], [], [] - all_n = [] - for circuit, observable, parameter_values_, parameters_ in zip( - circuits, observables, parameter_values, parameters - ): - metadata.append({"parameters": parameters_}) - # Make parameter values for the parameter shift rule. - param_shift_parameter_values = _make_param_shift_parameter_values( - circuit, parameter_values_, parameters_ - ) - # Combine inputs into a single job to reduce overhead. - n = len(param_shift_parameter_values) - job_circuits.extend([circuit] * n) - job_observables.extend([observable] * n) - job_param_values.extend(param_shift_parameter_values) - all_n.append(n) - - # Run the single job with all circuits. - job = self._estimator.run( - job_circuits, - job_observables, - job_param_values, - **options, - ) - try: - results = job.result() - except Exception as exc: - raise AlgorithmError("Estimator job failed.") from exc - - # Compute the gradients. - gradients = [] - partial_sum_n = 0 - for n in all_n: - result = results.values[partial_sum_n : partial_sum_n + n] - gradient_ = (result[: n // 2] - result[n // 2 :]) / 2 - gradients.append(gradient_) - partial_sum_n += n - - opt = self._get_local_options(options) - return EstimatorGradientResult(gradients=gradients, metadata=metadata, options=opt) diff --git a/qiskit/algorithms/gradients/param_shift/param_shift_sampler_gradient.py b/qiskit/algorithms/gradients/param_shift/param_shift_sampler_gradient.py deleted file mode 100644 index 642f4b002cd9..000000000000 --- a/qiskit/algorithms/gradients/param_shift/param_shift_sampler_gradient.py +++ /dev/null @@ -1,117 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2022. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. -""" -Gradient of probabilities with parameter shift -""" - -from __future__ import annotations - -from collections import defaultdict -from collections.abc import Sequence - -from qiskit.circuit import Parameter, QuantumCircuit - -from ..base.base_sampler_gradient import BaseSamplerGradient -from ..base.sampler_gradient_result import SamplerGradientResult -from ..utils import _make_param_shift_parameter_values - -from ...exceptions import AlgorithmError - - -class ParamShiftSamplerGradient(BaseSamplerGradient): - """ - Compute the gradients of the sampling probability by the parameter shift rule [1]. - - **Reference:** - [1] Schuld, M., Bergholm, V., Gogolin, C., Izaac, J., and Killoran, N. Evaluating analytic - gradients on quantum hardware, `DOI `_ - """ - - SUPPORTED_GATES = [ - "x", - "y", - "z", - "h", - "rx", - "ry", - "rz", - "p", - "cx", - "cy", - "cz", - "ryy", - "rxx", - "rzz", - "rzx", - ] - - def _run( - self, - circuits: Sequence[QuantumCircuit], - parameter_values: Sequence[Sequence[float]], - parameters: Sequence[Sequence[Parameter]], - **options, - ) -> SamplerGradientResult: - """Compute the estimator gradients on the given circuits.""" - g_circuits, g_parameter_values, g_parameters = self._preprocess( - circuits, parameter_values, parameters, self.SUPPORTED_GATES - ) - results = self._run_unique(g_circuits, g_parameter_values, g_parameters, **options) - return self._postprocess(results, circuits, parameter_values, parameters) - - def _run_unique( - self, - circuits: Sequence[QuantumCircuit], - parameter_values: Sequence[Sequence[float]], - parameters: Sequence[Sequence[Parameter]], - **options, - ) -> SamplerGradientResult: - """Compute the sampler gradients on the given circuits.""" - job_circuits, job_param_values, metadata = [], [], [] - all_n = [] - for circuit, parameter_values_, parameters_ in zip(circuits, parameter_values, parameters): - metadata.append({"parameters": parameters_}) - # Make parameter values for the parameter shift rule. - param_shift_parameter_values = _make_param_shift_parameter_values( - circuit, parameter_values_, parameters_ - ) - # Combine inputs into a single job to reduce overhead. - n = len(param_shift_parameter_values) - job_circuits.extend([circuit] * n) - job_param_values.extend(param_shift_parameter_values) - all_n.append(n) - - # Run the single job with all circuits. - job = self._sampler.run(job_circuits, job_param_values, **options) - try: - results = job.result() - except Exception as exc: - raise AlgorithmError("Estimator job failed.") from exc - - # Compute the gradients. - gradients = [] - partial_sum_n = 0 - for n in all_n: - gradient = [] - result = results.quasi_dists[partial_sum_n : partial_sum_n + n] - for dist_plus, dist_minus in zip(result[: n // 2], result[n // 2 :]): - grad_dist: dict[int, float] = defaultdict(float) - for key, val in dist_plus.items(): - grad_dist[key] += val / 2 - for key, val in dist_minus.items(): - grad_dist[key] -= val / 2 - gradient.append(dict(grad_dist)) - gradients.append(gradient) - partial_sum_n += n - - opt = self._get_local_options(options) - return SamplerGradientResult(gradients=gradients, metadata=metadata, options=opt) diff --git a/qiskit/algorithms/gradients/qfi.py b/qiskit/algorithms/gradients/qfi.py deleted file mode 100644 index 94aa86fde56a..000000000000 --- a/qiskit/algorithms/gradients/qfi.py +++ /dev/null @@ -1,171 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2022, 2023 -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. -""" -A class for the Quantum Fisher Information. -""" - -from __future__ import annotations - -from abc import ABC -from collections.abc import Sequence -from copy import copy - -from qiskit.circuit import Parameter, QuantumCircuit -from qiskit.providers import Options - -from .base.base_qgt import BaseQGT -from .lin_comb.lin_comb_estimator_gradient import DerivativeType -from .qfi_result import QFIResult - -from ..algorithm_job import AlgorithmJob -from ..exceptions import AlgorithmError - - -class QFI(ABC): - r"""Computes the Quantum Fisher Information (QFI) given a pure, - parameterized quantum state. QFI is defined as: - - .. math:: - - \mathrm{QFI}_{ij}= 4 \mathrm{Re}[\langle \partial_i \psi | \partial_j \psi \rangle - - \langle\partial_i \psi | \psi \rangle \langle\psi | \partial_j \psi \rangle]. - """ - - def __init__( - self, - qgt: BaseQGT, - options: Options | None = None, - ): - r""" - Args: - qgt: The quantum geometric tensor used to compute the QFI. - options: Backend runtime options used for circuit execution. The order of priority is: - options in ``run`` method > QFI's default options > primitive's default - setting. Higher priority setting overrides lower priority setting. - """ - self._qgt: BaseQGT = qgt - self._default_options = Options() - if options is not None: - self._default_options.update_options(**options) - - def run( - self, - circuits: Sequence[QuantumCircuit], - parameter_values: Sequence[Sequence[float]], - parameters: Sequence[Sequence[Parameter] | None] | None = None, - **options, - ) -> AlgorithmJob: - """Run the job of the QFIs on the given circuits. - - Args: - circuits: The list of quantum circuits to compute the QFIs. - parameter_values: The list of parameter values to be bound to the circuit. - parameters: The sequence of parameters to calculate only the QFIs of - the specified parameters. Each sequence of parameters corresponds to a circuit in - ``circuits``. Defaults to None, which means that the QFIs of all parameters in - each circuit are calculated. - options: Primitive backend runtime options used for circuit execution. - The order of priority is: options in ``run`` method > QFI's - default options > QGT's default setting. - Higher priority setting overrides lower priority setting. - - Returns: - The job object of the QFIs of the expectation values. The i-th result corresponds to - ``circuits[i]`` evaluated with parameters bound as ``parameter_values[i]``. - """ - - if isinstance(circuits, QuantumCircuit): - # Allow a single circuit to be passed in. - circuits = (circuits,) - - if parameters is None: - # If parameters is None, we calculate the gradients of all parameters in each circuit. - parameters = [circuit.parameters for circuit in circuits] - else: - # If parameters is not None, we calculate the gradients of the specified parameters. - # None in parameters means that the gradients of all parameters in the corresponding - # circuit are calculated. - parameters = [ - params if params is not None else circuits[i].parameters - for i, params in enumerate(parameters) - ] - # The priority of run option is as follows: - # options in ``run`` method > QFI's default options > QGT's default setting. - opts = copy(self._default_options) - opts.update_options(**options) - job = AlgorithmJob(self._run, circuits, parameter_values, parameters, **opts.__dict__) - job.submit() - return job - - def _run( - self, - circuits: Sequence[QuantumCircuit], - parameter_values: Sequence[Sequence[float]], - parameters: Sequence[Sequence[Parameter]], - **options, - ) -> QFIResult: - """Compute the QFI on the given circuits.""" - # Set the derivative type to real - temp_derivative_type, self._qgt.derivative_type = ( - self._qgt.derivative_type, - DerivativeType.REAL, - ) - job = self._qgt.run(circuits, parameter_values, parameters, **options) - - try: - result = job.result() - except AlgorithmError as exc: - raise AlgorithmError("Estimator job or gradient job failed.") from exc - - self._qgt.derivative_type = temp_derivative_type - - return QFIResult( - qfis=[4 * qgt.real for qgt in result.qgts], - metadata=result.metadata, - options=result.options, - ) - - @property - def options(self) -> Options: - """Return the union of QGT's options setting and QFI's default options, - where, if the same field is set in both, the QFI's default options override - the QGT's default setting. - - Returns: - The QFI default + QGT options. - """ - return self._get_local_options(self._default_options.__dict__) - - def update_default_options(self, **options): - """Update the gradient's default options setting. - - Args: - **options: The fields to update the default options. - """ - - self._default_options.update_options(**options) - - def _get_local_options(self, options: Options) -> Options: - """Return the union of the QFI default setting, - the QGT default options, and the options in the ``run`` method. - The order of priority is: options in ``run`` method > QFI's default options > QGT's - default setting. - - Args: - options: The fields to update the options - - Returns: - The QFI default + QGT default + run options. - """ - opts = copy(self._qgt.options) - opts.update_options(**options) - return opts diff --git a/qiskit/algorithms/gradients/qfi_result.py b/qiskit/algorithms/gradients/qfi_result.py deleted file mode 100644 index 47a04021584d..000000000000 --- a/qiskit/algorithms/gradients/qfi_result.py +++ /dev/null @@ -1,35 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2022. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. -""" -QFI result class -""" - -from __future__ import annotations - -from dataclasses import dataclass -from typing import Any - -import numpy as np - -from qiskit.providers import Options - - -@dataclass(frozen=True) -class QFIResult: - """Result of QFI.""" - - qfis: list[np.ndarray] - """The QFI.""" - metadata: list[dict[str, Any]] - """Additional information about the job.""" - options: Options - """Primitive runtime options for the execution of the job.""" diff --git a/qiskit/algorithms/gradients/reverse/__init__.py b/qiskit/algorithms/gradients/reverse/__init__.py deleted file mode 100644 index fdb172d367f0..000000000000 --- a/qiskit/algorithms/gradients/reverse/__init__.py +++ /dev/null @@ -1,11 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2022. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. diff --git a/qiskit/algorithms/gradients/reverse/bind.py b/qiskit/algorithms/gradients/reverse/bind.py deleted file mode 100644 index 7660f7c836d0..000000000000 --- a/qiskit/algorithms/gradients/reverse/bind.py +++ /dev/null @@ -1,53 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2022, 2023. -# -# 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. - -"""Bind values to a parametrized circuit, accepting binds for non-existing parameters in the circuit.""" - -from __future__ import annotations -from collections.abc import Iterable - -from qiskit.circuit import QuantumCircuit, Parameter - -# pylint: disable=inconsistent-return-statements -def bind( - circuits: QuantumCircuit | Iterable[QuantumCircuit], - parameter_binds: dict[Parameter, float], - inplace: bool = False, -) -> QuantumCircuit | Iterable[QuantumCircuit] | None: - """Bind parameters in a circuit (or list of circuits). - - This method also allows passing parameter binds to parameters that are not in the circuit, - and thereby differs to :meth:`.QuantumCircuit.assign_parameters`. - - Args: - circuits: Input circuit(s). - parameter_binds: A dictionary with ``{Parameter: float}`` pairs determining the values to - which the free parameters in the circuit(s) are bound. - inplace: If ``True``, bind the values in place, otherwise return circuit copies. - - Returns: - The bound circuits, if ``inplace=False``, otherwise None. - - """ - if not isinstance(circuits, Iterable): - circuits = [circuits] - return_list = False - else: - return_list = True - - bound = [] - for circuit in circuits: - existing_parameter_binds = {p: parameter_binds[p] for p in circuit.parameters} - bound.append(circuit.assign_parameters(existing_parameter_binds, inplace=inplace)) - - if not inplace: - return bound if return_list else bound[0] diff --git a/qiskit/algorithms/gradients/reverse/derive_circuit.py b/qiskit/algorithms/gradients/reverse/derive_circuit.py deleted file mode 100644 index 96a2bead60cd..000000000000 --- a/qiskit/algorithms/gradients/reverse/derive_circuit.py +++ /dev/null @@ -1,157 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2022, 2023. -# -# 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. - -"""Split a circuit into subcircuits, each containing a single parameterized gate.""" - -from __future__ import annotations -import itertools -from collections.abc import Sequence - -from qiskit.circuit import QuantumCircuit, Parameter, Gate -from qiskit.circuit.library import RXGate, RYGate, RZGate, CRXGate, CRYGate, CRZGate - - -def gradient_lookup(gate: Gate) -> list[tuple[complex, QuantumCircuit]]: - """Returns a circuit implementing the gradient of the input gate. - - Args: - gate: The gate whose derivative is returned. - - Returns: - The derivative of the input gate as list of ``(coeff, circuit)`` pairs, - where the sum of all ``coeff * circuit`` elements describes the full derivative. - The circuit is the unitary part of the derivative with a potential separate ``coeff``. - The output is a list as derivatives of e.g. controlled gates can only be described - as a sum of ``coeff * circuit`` pairs. - - Raises: - NotImplementedError: If the derivative of ``gate`` is not implemented. - """ - - param = gate.params[0] - if isinstance(gate, RXGate): - derivative = QuantumCircuit(gate.num_qubits) - derivative.rx(param, 0) - derivative.x(0) - return [(-0.5j, derivative)] - if isinstance(gate, RYGate): - derivative = QuantumCircuit(gate.num_qubits) - derivative.ry(param, 0) - derivative.y(0) - return [(-0.5j, derivative)] - if isinstance(gate, RZGate): - derivative = QuantumCircuit(gate.num_qubits) - derivative.rz(param, 0) - derivative.z(0) - return [(-0.5j, derivative)] - if isinstance(gate, CRXGate): - proj1 = QuantumCircuit(gate.num_qubits) - proj1.rx(param, 1) - proj1.x(1) - - proj2 = QuantumCircuit(gate.num_qubits) - proj2.z(0) - proj2.rx(param, 1) - proj2.x(1) - - return [(-0.25j, proj1), (0.25j, proj2)] - if isinstance(gate, CRYGate): - proj1 = QuantumCircuit(gate.num_qubits) - proj1.ry(param, 1) - proj1.y(1) - - proj2 = QuantumCircuit(gate.num_qubits) - proj2.z(0) - proj2.ry(param, 1) - proj2.y(1) - - return [(-0.25j, proj1), (0.25j, proj2)] - if isinstance(gate, CRZGate): - proj1 = QuantumCircuit(gate.num_qubits) - proj1.rz(param, 1) - proj1.z(1) - - proj2 = QuantumCircuit(gate.num_qubits) - proj2.z(0) - proj2.rz(param, 1) - proj2.z(1) - - return [(-0.25j, proj1), (0.25j, proj2)] - raise NotImplementedError("Cannot implement gradient for", gate) - - -def derive_circuit( - circuit: QuantumCircuit, parameter: Parameter -) -> Sequence[tuple[complex, QuantumCircuit]]: - """Return the analytic gradient expression of the input circuit wrt. a single parameter. - - Returns a list of ``(coeff, gradient_circuit)`` tuples, where the derivative of the circuit is - given by the sum of the gradient circuits multiplied by their coefficient. - - For example, the circuit:: - - ┌───┐┌───────┐┌─────┐ - q: ┤ H ├┤ Rx(x) ├┤ Sdg ├ - └───┘└───────┘└─────┘ - - returns the coefficient `-0.5j` and the circuit equivalent to:: - - ┌───┐┌───────┐┌───┐┌─────┐ - q: ┤ H ├┤ Rx(x) ├┤ X ├┤ Sdg ├ - └───┘└───────┘└───┘└─────┘ - - as the derivative of `Rx(x)` is `-0.5j Rx(x) X`. - - Args: - circuit: The quantum circuit to derive. - parameter: The parameter with respect to which we derive. - - Returns: - A list of ``(coeff, gradient_circuit)`` tuples. - - Raises: - ValueError: If ``parameter`` is of the wrong type. - ValueError: If ``parameter`` is not in this circuit. - NotImplementedError: If a non-unique parameter is added, as the product rule is not yet - supported in this function. - """ - # this is added as useful user-warning, since sometimes ``ParameterExpression``s are - # passed around instead of ``Parameter``s - if not isinstance(parameter, Parameter): - raise ValueError(f"parameter must be of type Parameter, not {type(parameter)}.") - - if parameter not in circuit.parameters: - raise ValueError(f"The parameter {parameter} is not in this circuit.") - - if len(circuit._parameter_table[parameter]) > 1: - raise NotImplementedError("No product rule support yet, circuit parameters must be unique.") - - summands, op_context = [], [] - for i, op in enumerate(circuit.data): - gate = op.operation - op_context.append((op.qubits, op.clbits)) - if parameter in gate.params: - coeffs_and_grads = gradient_lookup(gate) - summands += [coeffs_and_grads] - else: - summands += [[(1, gate)]] - - gradient = [] - for product_rule_term in itertools.product(*summands): - summand_circuit = QuantumCircuit(*circuit.qregs) - c = 1 - for i, term in enumerate(product_rule_term): - c *= term[0] - summand_circuit.data.append([term[1], *op_context[i]]) - gradient += [(c, summand_circuit.copy())] - - return gradient diff --git a/qiskit/algorithms/gradients/reverse/reverse_gradient.py b/qiskit/algorithms/gradients/reverse/reverse_gradient.py deleted file mode 100644 index c3bf8005d377..000000000000 --- a/qiskit/algorithms/gradients/reverse/reverse_gradient.py +++ /dev/null @@ -1,200 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2022. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. - -"""Estimator gradients with the classically efficient reverse mode.""" - -from __future__ import annotations -from collections.abc import Sequence -import logging - -import numpy as np - -from qiskit.circuit import QuantumCircuit, Parameter -from qiskit.quantum_info.operators.base_operator import BaseOperator -from qiskit.quantum_info import Statevector -from qiskit.opflow import PauliSumOp -from qiskit.primitives import Estimator - -from .bind import bind -from .derive_circuit import derive_circuit -from .split_circuits import split - -from ..base.base_estimator_gradient import BaseEstimatorGradient -from ..base.estimator_gradient_result import EstimatorGradientResult -from ..utils import DerivativeType - -logger = logging.getLogger(__name__) - - -class ReverseEstimatorGradient(BaseEstimatorGradient): - """Estimator gradients with the classically efficient reverse mode. - - .. note:: - - This gradient implementation is based on statevector manipulations and scales - exponentially with the number of qubits. However, for small system sizes it can be very fast - compared to circuit-based gradients. - - This class implements the calculation of the expectation gradient as described in - [1]. By keeping track of two statevectors and iteratively sweeping through each parameterized - gate, this method scales only linearly with the number of parameters. - - **References:** - - [1]: Jones, T. and Gacon, J. "Efficient calculation of gradients in classical simulations - of variational quantum algorithms" (2020). - `arXiv:2009.02823 `_. - - """ - - SUPPORTED_GATES = ["rx", "ry", "rz", "cp", "crx", "cry", "crz"] - - def __init__(self, derivative_type: DerivativeType = DerivativeType.REAL): - """ - Args: - derivative_type: Defines whether the real, imaginary or real plus imaginary part - of the gradient is returned. - """ - dummy_estimator = Estimator() # this is required by the base class, but not used - super().__init__(dummy_estimator, derivative_type=derivative_type) - - @BaseEstimatorGradient.derivative_type.setter - def derivative_type(self, derivative_type: DerivativeType) -> None: - """Set the derivative type.""" - self._derivative_type = derivative_type - - def _run( - self, - circuits: Sequence[QuantumCircuit], - observables: Sequence[BaseOperator | PauliSumOp], - parameter_values: Sequence[Sequence[float]], - parameters: Sequence[Sequence[Parameter]], - **options, - ) -> EstimatorGradientResult: - """Compute the gradients of the expectation values by the parameter shift rule.""" - g_circuits, g_parameter_values, g_parameters = self._preprocess( - circuits, parameter_values, parameters, self.SUPPORTED_GATES - ) - results = self._run_unique( - g_circuits, observables, g_parameter_values, g_parameters, **options - ) - return self._postprocess(results, circuits, parameter_values, parameters) - - def _run_unique( - self, - circuits: Sequence[QuantumCircuit], - observables: Sequence[BaseOperator | PauliSumOp], - parameter_values: Sequence[Sequence[float]], - parameters: Sequence[Sequence[Parameter]], - **options, # pylint: disable=unused-argument - ) -> EstimatorGradientResult: - num_gradients = len(circuits) - gradients = [] - metadata = [] - - for i in range(num_gradients): - # temporary variables for easier access - circuit = circuits[i] - parameters_ = parameters[i] - observable = observables[i] - values = parameter_values[i] - - # the metadata only contains the parameters as there are no run configs here - metadata.append( - { - "parameters": parameters_, - "derivative_type": self.derivative_type, - } - ) - - # keep track of the parameter order of the circuit, as the circuit splitting might - # produce a list of unitaries in a different order - # original_parameter_order = [p for p in circuit.parameters if p in parameters_] - - # split the circuit and generate lists of unitaries [U_1, U_2, ...] and - # parameters [p_1, p_2, ...] in these unitaries - unitaries, paramlist = split(circuit, parameters=parameters_) - - parameter_binds = dict(zip(circuit.parameters, values)) - bound_circuit = bind(circuit, parameter_binds) - - # initialize state variables -- we use the same naming as in the paper - phi = Statevector(bound_circuit) - lam = _evolve_by_operator(observable, phi) - - # store gradients in a dictionary to return them in the correct order - grads = {param: 0j for param in parameters_} - - num_parameters = len(unitaries) - for j in reversed(range(num_parameters)): - unitary_j = unitaries[j] - - # We currently only support gates with a single parameter -- which is reflected - # in self.SUPPORTED_GATES -- but generally we could also support gates with multiple - # parameters per gate - parameter_j = paramlist[j][0] - - # get the analytic gradient d U_j / d p_j and bind the gate - deriv = derive_circuit(unitary_j, parameter_j) - for _, gate in deriv: - bind(gate, parameter_binds, inplace=True) - - # iterate the state variable - unitary_j_dagger = bind(unitary_j, parameter_binds).inverse() - phi = phi.evolve(unitary_j_dagger) - - # compute current gradient - grad = sum( - coeff * lam.conjugate().data.dot(phi.evolve(gate).data) for coeff, gate in deriv - ) - - # Compute the full gradient (real and complex parts) as all information is available. - # Later, based on the derivative type, cast to real/imag/complex. - grads[parameter_j] += grad - - if j > 0: - lam = lam.evolve(unitary_j_dagger) - - gradient = np.array(list(grads.values())) - gradients.append(self._to_derivtype(gradient)) - - result = EstimatorGradientResult(gradients, metadata=metadata, options={}) - return result - - def _to_derivtype(self, gradient): - # this disable is needed as Pylint does not understand derivative_type is a property if - # it is only defined in the base class and the getter is in the child - # pylint: disable=comparison-with-callable - if self.derivative_type == DerivativeType.REAL: - return 2 * np.real(gradient) - if self.derivative_type == DerivativeType.IMAG: - return 2 * np.imag(gradient) - - return 2 * gradient - - -def _evolve_by_operator(operator, state): - """Evolve the Statevector state by operator.""" - - # try casting to sparse matrix and use sparse matrix-vector multiplication, which is - # a lot faster than using Statevector.evolve - if isinstance(operator, PauliSumOp): - operator = operator.primitive * operator.coeff - - try: - spmatrix = operator.to_matrix(sparse=True) - evolved = spmatrix @ state.data - return Statevector(evolved) - except (TypeError, AttributeError): - logger.info("Operator is not castable to a sparse matrix, using Statevector.evolve.") - - return state.evolve(operator) diff --git a/qiskit/algorithms/gradients/reverse/reverse_qgt.py b/qiskit/algorithms/gradients/reverse/reverse_qgt.py deleted file mode 100644 index 0b845ee96961..000000000000 --- a/qiskit/algorithms/gradients/reverse/reverse_qgt.py +++ /dev/null @@ -1,252 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2023. -# -# 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. - -"""QGT with the classically efficient reverse mode.""" - -from __future__ import annotations -from collections.abc import Sequence -import logging - -import numpy as np - -from qiskit.circuit import QuantumCircuit, Parameter -from qiskit.quantum_info import Statevector -from qiskit.providers import Options -from qiskit.primitives import Estimator - -from ..base.base_qgt import BaseQGT -from ..base.qgt_result import QGTResult -from ..utils import DerivativeType - -from .split_circuits import split -from .bind import bind -from .derive_circuit import derive_circuit - -logger = logging.getLogger(__name__) - - -class ReverseQGT(BaseQGT): - """QGT calculation with the classically efficient reverse mode. - - .. note:: - - This QGT implementation is based on statevector manipulations and scales exponentially - with the number of qubits. However, for small system sizes it can be very fast - compared to circuit-based gradients. - - This class implements the calculation of the QGT as described in [1]. - By keeping track of three statevectors and iteratively sweeping through each parameterized - gate, this method scales only quadratically with the number of parameters. - - **References:** - - [1]: Jones, T. "Efficient classical calculation of the Quantum Natural Gradient" (2020). - `arXiv:2011.02991 `_. - - """ - - SUPPORTED_GATES = ["rx", "ry", "rz", "cp", "crx", "cry", "crz"] - - def __init__( - self, phase_fix: bool = True, derivative_type: DerivativeType = DerivativeType.COMPLEX - ): - """ - Args: - phase_fix: Whether or not to include the phase fix. - derivative_type: Determines whether the complex QGT or only the real or imaginary - parts are calculated. - """ - dummy_estimator = Estimator() # this method does not need an estimator - super().__init__(dummy_estimator, phase_fix, derivative_type) - - @property - def options(self) -> Options: - """There are no options for the reverse QGT, returns an empty options dict. - - Returns: - Empty options. - """ - return Options() - - def _run( - self, - circuits: Sequence[QuantumCircuit], - parameter_values: Sequence[Sequence[float]], - parameter_sets: Sequence[set[Parameter]], - **options, - ) -> QGTResult: - """Compute the QGT on the given circuits.""" - g_circuits, g_parameter_values, g_parameter_sets = self._preprocess( - circuits, parameter_values, parameter_sets, self.SUPPORTED_GATES - ) - results = self._run_unique(g_circuits, g_parameter_values, g_parameter_sets, **options) - return self._postprocess(results, circuits, parameter_values, parameter_sets) - - def _run_unique( - self, - circuits: Sequence[QuantumCircuit], - parameter_values: Sequence[Sequence[float]], - parameter_sets: Sequence[set[Parameter]], - **options, # pylint: disable=unused-argument - ) -> QGTResult: - num_qgts = len(circuits) - qgts = [] - metadata = [] - - for k in range(num_qgts): - values = np.asarray(parameter_values[k]) - circuit = circuits[k] - parameters = list(parameter_sets[k]) - - num_parameters = len(parameters) - original_parameter_order = [p for p in circuit.parameters if p in parameters] - metadata.append({"parameters": original_parameter_order}) - - unitaries, paramlist = split(circuit, parameters=parameters) - - # initialize the phase fix vector and the hessian part ``metric`` - num_parameters = len(unitaries) - phase_fixes = np.zeros(num_parameters, dtype=complex) - metric = np.zeros((num_parameters, num_parameters), dtype=complex) - - # initialize the state variables -- naming convention is the same as the paper - parameter_binds = dict(zip(circuit.parameters, values)) - bound_unitaries = bind(unitaries, parameter_binds) - - chi = Statevector(bound_unitaries[0]) - psi = chi.copy() - phi = Statevector.from_int(0, (2,) * circuit.num_qubits) - - # Get the analytic gradient of the first unitary - # Note: We currently only support gates with a single parameter -- which is reflected - # in self.SUPPORTED_GATES -- but generally we could also support gates with multiple - # parameters per gate. This is the reason for the second 0-index. - deriv = derive_circuit(unitaries[0], paramlist[0][0]) - for _, gate in deriv: - bind(gate, parameter_binds, inplace=True) - - grad_coeffs = [coeff for coeff, _ in deriv] - grad_states = [phi.evolve(gate) for _, gate in deriv] - - # compute phase fix (optional) and the hessian part - if self._phase_fix: - phase_fixes[0] = _phasefix_term(chi, grad_coeffs, grad_states) - - metric[0, 0] = _l_term(grad_coeffs, grad_states, grad_coeffs, grad_states) - - for j in range(1, num_parameters): - lam = psi.copy() - phi = psi.copy() - - # get the analytic gradient d U_j / d p_j and apply it - deriv = derive_circuit(unitaries[j], paramlist[j][0]) - - for _, gate in deriv: - bind(gate, parameter_binds, inplace=True) - - # compute |phi> (in general it's a sum of states and coeffs) - grad_coeffs = [coeff for coeff, _ in deriv] - grad_states = [phi.evolve(gate) for _, gate in deriv] - - # compute the digaonal element L_{j, j} - metric[j, j] += _l_term(grad_coeffs, grad_states, grad_coeffs, grad_states) - - # compute the off diagonal elements L_{i, j} - for i in reversed(range(j)): - # apply U_{i + 1}_dg - unitary_ip_inv = bound_unitaries[i + 1].inverse() - grad_states = [state.evolve(unitary_ip_inv) for state in grad_states] - - lam = lam.evolve(bound_unitaries[i].inverse()) - - # get the gradient d U_i / d p_i and apply it - deriv = derive_circuit(unitaries[i], paramlist[i][0]) - for _, gate in deriv: - bind(gate, parameter_binds, inplace=True) - - grad_coeffs_mu = [coeff for coeff, _ in deriv] - grad_states_mu = [lam.evolve(gate) for _, gate in deriv] - - metric[i, j] += _l_term( - grad_coeffs_mu, grad_states_mu, grad_coeffs, grad_states - ) - - if self._phase_fix: - phase_fixes[j] += _phasefix_term(chi, grad_coeffs, grad_states) - - psi = psi.evolve(bound_unitaries[j]) - - # The following code stacks the QGT together and maps the values into the - # correct original order of parameters - - # map circuit parameter to global index in the circuit - param_to_circuit = { - param: index for index, param in enumerate(original_parameter_order) - } - # map global index to the local index used in the calculation, the new index can - # now be accessed by remap[index] - remap = { - index: param_to_circuit[_extract_parameter(plist[0])] - for index, plist in enumerate(paramlist) - } - - qgt = np.zeros((num_parameters, num_parameters), dtype=complex) - for i in range(num_parameters): - iloc = remap[i] - for j in range(num_parameters): - jloc = remap[j] - if i <= j: - qgt[iloc, jloc] += metric[i, j] - else: - qgt[iloc, jloc] += np.conj(metric[j, i]) - - qgt[iloc, jloc] -= np.conj(phase_fixes[i]) * phase_fixes[j] - - # append and cast to real/imag if required - qgts.append(self._to_derivtype(qgt)) - - result = QGTResult(qgts, self.derivative_type, metadata, options=None) - return result - - def _to_derivtype(self, qgt): - if self.derivative_type == DerivativeType.REAL: - return np.real(qgt) - if self.derivative_type == DerivativeType.IMAG: - return np.imag(qgt) - - return qgt - - -def _l_term(coeffs_i, states_i, coeffs_j, states_j): - return sum( - sum( - np.conj(coeff_i) * coeff_j * np.conj(state_i.data).dot(state_j.data) - for coeff_i, state_i in zip(coeffs_i, states_i) - ) - for coeff_j, state_j in zip(coeffs_j, states_j) - ) - - -def _phasefix_term(chi, coeffs, states): - return sum( - coeff_i * np.conj(chi.data).dot(state_i.data) for coeff_i, state_i in zip(coeffs, states) - ) - - -def _extract_parameter(expression): - if isinstance(expression, Parameter): - return expression - - if len(expression.parameters) > 1: - raise ValueError("Expression has more than one parameter.") - - return list(expression.parameters)[0] diff --git a/qiskit/algorithms/gradients/reverse/split_circuits.py b/qiskit/algorithms/gradients/reverse/split_circuits.py deleted file mode 100644 index b2bdf2b5375b..000000000000 --- a/qiskit/algorithms/gradients/reverse/split_circuits.py +++ /dev/null @@ -1,68 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2022. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. - -"""Split a circuit into subcircuits, each containing a single parameterized gate.""" - -from __future__ import annotations - -from collections.abc import Iterable -from qiskit.circuit import QuantumCircuit, ParameterExpression, Parameter - - -def split( - circuit: QuantumCircuit, - parameters: Iterable[Parameter] | None = None, -) -> tuple[list[QuantumCircuit], list[list[Parameter]]]: - """Split the circuit at ParameterExpressions. - - Args: - circuit: The circuit to split. - parameters: The parameters at which to split. If None, split at each parameter. - - Returns: - A list of the split circuits along with a list of which parameters are in the subcircuits. - """ - circuits = [] - corresponding_parameters = [] - - sub = QuantumCircuit(*circuit.qregs, *circuit.cregs) - for inst in circuit.data: - # check if new split must be created - if parameters is None: - params = [ - param - for param in inst.operation.params - if isinstance(param, ParameterExpression) and len(param.parameters) > 0 - ] - else: - if inst.operation.definition is not None: - free_inst_params = inst.operation.definition.parameters - else: - free_inst_params = {} - - params = [p for p in parameters if p in free_inst_params] - - new_split = bool(len(params) > 0) - - if new_split: - sub.append(inst) - circuits.append(sub) - corresponding_parameters.append(params) - sub = QuantumCircuit(*circuit.qregs, *circuit.cregs) - else: - sub.append(inst) - - # handle leftover gates - if len(sub.data) > 0: - circuits[-1].compose(sub, inplace=True) - - return circuits, corresponding_parameters diff --git a/qiskit/algorithms/gradients/spsa/__init__.py b/qiskit/algorithms/gradients/spsa/__init__.py deleted file mode 100644 index 8f5fd2a37f84..000000000000 --- a/qiskit/algorithms/gradients/spsa/__init__.py +++ /dev/null @@ -1,11 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2022, 2023 -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. diff --git a/qiskit/algorithms/gradients/spsa/spsa_estimator_gradient.py b/qiskit/algorithms/gradients/spsa/spsa_estimator_gradient.py deleted file mode 100644 index 021bcb5803f0..000000000000 --- a/qiskit/algorithms/gradients/spsa/spsa_estimator_gradient.py +++ /dev/null @@ -1,135 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2022. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. - -"""Gradient of Sampler with Finite difference method.""" - -from __future__ import annotations - -from collections.abc import Sequence - -import numpy as np - -from qiskit.circuit import Parameter, QuantumCircuit -from qiskit.opflow import PauliSumOp -from qiskit.primitives import BaseEstimator -from qiskit.providers import Options -from qiskit.quantum_info.operators.base_operator import BaseOperator - -from ..base.base_estimator_gradient import BaseEstimatorGradient -from ..base.estimator_gradient_result import EstimatorGradientResult - -from ...exceptions import AlgorithmError - - -class SPSAEstimatorGradient(BaseEstimatorGradient): - """ - Compute the gradients of the expectation value by the Simultaneous Perturbation Stochastic - Approximation (SPSA) [1]. - - **Reference:** - [1] J. C. Spall, Adaptive stochastic approximation by the simultaneous perturbation method in - IEEE Transactions on Automatic Control, vol. 45, no. 10, pp. 1839-1853, Oct 2020, - `doi: 10.1109/TAC.2000.880982 `_ - """ - - def __init__( - self, - estimator: BaseEstimator, - epsilon: float, - batch_size: int = 1, - seed: int | None = None, - options: Options | None = None, - ): - """ - Args: - estimator: The estimator used to compute the gradients. - epsilon: The offset size for the SPSA gradients. - batch_size: The number of gradients to average. - seed: The seed for a random perturbation vector. - options: Primitive backend runtime options used for circuit execution. - The order of priority is: options in ``run`` method > gradient's - default options > primitive's default setting. - Higher priority setting overrides lower priority setting - - Raises: - ValueError: If ``epsilon`` is not positive. - """ - if epsilon <= 0: - raise ValueError(f"epsilon ({epsilon}) should be positive.") - self._epsilon = epsilon - self._batch_size = batch_size - self._seed = np.random.default_rng(seed) - - super().__init__(estimator, options) - - def _run( - self, - circuits: Sequence[QuantumCircuit], - observables: Sequence[BaseOperator | PauliSumOp], - parameter_values: Sequence[Sequence[float]], - parameters: Sequence[Sequence[Parameter]], - **options, - ) -> EstimatorGradientResult: - """Compute the estimator gradients on the given circuits.""" - job_circuits, job_observables, job_param_values, metadata, offsets = [], [], [], [], [] - all_n = [] - for circuit, observable, parameter_values_, parameters_ in zip( - circuits, observables, parameter_values, parameters - ): - # Indices of parameters to be differentiated. - indices = [circuit.parameters.data.index(p) for p in parameters_] - metadata.append({"parameters": parameters_}) - # Make random perturbation vectors. - offset = [ - (-1) ** (self._seed.integers(0, 2, len(circuit.parameters))) - for _ in range(self._batch_size) - ] - plus = [parameter_values_ + self._epsilon * offset_ for offset_ in offset] - minus = [parameter_values_ - self._epsilon * offset_ for offset_ in offset] - offsets.append(offset) - - # Combine inputs into a single job to reduce overhead. - job_circuits.extend([circuit] * 2 * self._batch_size) - job_observables.extend([observable] * 2 * self._batch_size) - job_param_values.extend(plus + minus) - all_n.append(2 * self._batch_size) - - # Run the single job with all circuits. - job = self._estimator.run( - job_circuits, - job_observables, - job_param_values, - **options, - ) - try: - results = job.result() - except Exception as exc: - raise AlgorithmError("Estimator job failed.") from exc - - # Compute the gradients. - gradients = [] - partial_sum_n = 0 - for i, n in enumerate(all_n): - result = results.values[partial_sum_n : partial_sum_n + n] - partial_sum_n += n - n = len(result) // 2 - diffs = (result[:n] - result[n:]) / (2 * self._epsilon) - # Calculate the gradient for each batch. Note that (``diff`` / ``offset``) is the gradient - # since ``offset`` is a perturbation vector of 1s and -1s. - batch_gradients = np.array([diff / offset for diff, offset in zip(diffs, offsets[i])]) - # Take the average of the batch gradients. - gradient = np.mean(batch_gradients, axis=0) - indices = [circuits[i].parameters.data.index(p) for p in metadata[i]["parameters"]] - gradients.append(gradient[indices]) - - opt = self._get_local_options(options) - return EstimatorGradientResult(gradients=gradients, metadata=metadata, options=opt) diff --git a/qiskit/algorithms/gradients/spsa/spsa_sampler_gradient.py b/qiskit/algorithms/gradients/spsa/spsa_sampler_gradient.py deleted file mode 100644 index be23274d7ebc..000000000000 --- a/qiskit/algorithms/gradients/spsa/spsa_sampler_gradient.py +++ /dev/null @@ -1,136 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2022. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. - -"""Gradient of Sampler with Finite difference method.""" - -from __future__ import annotations - -from collections import defaultdict -from collections.abc import Sequence - -import numpy as np - -from qiskit.circuit import Parameter, QuantumCircuit -from qiskit.primitives import BaseSampler -from qiskit.providers import Options - -from ..base.base_sampler_gradient import BaseSamplerGradient -from ..base.sampler_gradient_result import SamplerGradientResult - -from ...exceptions import AlgorithmError - - -class SPSASamplerGradient(BaseSamplerGradient): - """ - Compute the gradients of the sampling probability by the Simultaneous Perturbation Stochastic - Approximation (SPSA) [1]. - - **Reference:** - [1] J. C. Spall, Adaptive stochastic approximation by the simultaneous perturbation method in - IEEE Transactions on Automatic Control, vol. 45, no. 10, pp. 1839-1853, Oct 2020, - `doi: 10.1109/TAC.2000.880982 `_. - """ - - def __init__( - self, - sampler: BaseSampler, - epsilon: float, - batch_size: int = 1, - seed: int | None = None, - options: Options | None = None, - ): - """ - Args: - sampler: The sampler used to compute the gradients. - epsilon: The offset size for the SPSA gradients. - batch_size: number of gradients to average. - seed: The seed for a random perturbation vector. - options: Primitive backend runtime options used for circuit execution. - The order of priority is: options in ``run`` method > gradient's - default options > primitive's default setting. - Higher priority setting overrides lower priority setting - - Raises: - ValueError: If ``epsilon`` is not positive. - """ - if epsilon <= 0: - raise ValueError(f"epsilon ({epsilon}) should be positive.") - self._batch_size = batch_size - self._epsilon = epsilon - self._seed = np.random.default_rng(seed) - - super().__init__(sampler, options) - - def _run( - self, - circuits: Sequence[QuantumCircuit], - parameter_values: Sequence[Sequence[float]], - parameters: Sequence[Sequence[Parameter]], - **options, - ) -> SamplerGradientResult: - """Compute the sampler gradients on the given circuits.""" - job_circuits, job_param_values, metadata, offsets = [], [], [], [] - all_n = [] - for circuit, parameter_values_, parameters_ in zip(circuits, parameter_values, parameters): - # Indices of parameters to be differentiated. - indices = [circuit.parameters.data.index(p) for p in parameters_] - metadata.append({"parameters": parameters_}) - offset = np.array( - [ - (-1) ** (self._seed.integers(0, 2, len(circuit.parameters))) - for _ in range(self._batch_size) - ] - ) - plus = [parameter_values_ + self._epsilon * offset_ for offset_ in offset] - minus = [parameter_values_ - self._epsilon * offset_ for offset_ in offset] - offsets.append(offset) - - # Combine inputs into a single job to reduce overhead. - n = 2 * self._batch_size - job_circuits.extend([circuit] * n) - job_param_values.extend(plus + minus) - all_n.append(n) - - # Run the single job with all circuits. - job = self._sampler.run(job_circuits, job_param_values, **options) - try: - results = job.result() - except Exception as exc: - raise AlgorithmError("Sampler job failed.") from exc - - # Compute the gradients. - gradients = [] - partial_sum_n = 0 - for i, n in enumerate(all_n): - dist_diffs = {} - result = results.quasi_dists[partial_sum_n : partial_sum_n + n] - for j, (dist_plus, dist_minus) in enumerate(zip(result[: n // 2], result[n // 2 :])): - dist_diff: dict[int, float] = defaultdict(float) - for key, value in dist_plus.items(): - dist_diff[key] += value / (2 * self._epsilon) - for key, value in dist_minus.items(): - dist_diff[key] -= value / (2 * self._epsilon) - dist_diffs[j] = dist_diff - gradient = [] - indices = [circuits[i].parameters.data.index(p) for p in metadata[i]["parameters"]] - for j in indices: - gradient_j: dict[int, float] = defaultdict(float) - for k in range(self._batch_size): - for key, value in dist_diffs[k].items(): - gradient_j[key] += value * offsets[i][k][j] - gradient_j = {key: value / self._batch_size for key, value in gradient_j.items()} - gradient.append(gradient_j) - gradients.append(gradient) - partial_sum_n += n - - opt = self._get_local_options(options) - return SamplerGradientResult(gradients=gradients, metadata=metadata, options=opt) diff --git a/qiskit/algorithms/gradients/utils.py b/qiskit/algorithms/gradients/utils.py deleted file mode 100644 index f46911ee26ff..000000000000 --- a/qiskit/algorithms/gradients/utils.py +++ /dev/null @@ -1,375 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2022, 2023. -# -# 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. - -""" -Utility functions for gradients -""" - -from __future__ import annotations - -from collections import defaultdict -from collections.abc import Sequence -from dataclasses import dataclass -from enum import Enum - -import numpy as np - -from qiskit.circuit import ( - ClassicalRegister, - Gate, - Instruction, - Parameter, - ParameterExpression, - QuantumCircuit, - QuantumRegister, -) -from qiskit.circuit.library.standard_gates import ( - CXGate, - CYGate, - CZGate, - RXGate, - RXXGate, - RYGate, - RYYGate, - RZGate, - RZXGate, - RZZGate, - XGate, -) -from qiskit.quantum_info import SparsePauliOp - - -################################################################################ -## Gradient circuits and Enum -################################################################################ -class DerivativeType(Enum): - """Types of derivative.""" - - REAL = "real" - IMAG = "imag" - COMPLEX = "complex" - - -@dataclass -class GradientCircuit: - """Gradient circuit with unique parameters and mapping information.""" - - gradient_circuit: QuantumCircuit - """An internal quantum circuit with unique parameters used to calculate the gradient""" - parameter_map: dict[Parameter, list[tuple[Parameter, float | ParameterExpression]]] - """A dictionary maps the parameters of ``circuit`` to the parameters of ``gradient_circuit`` with - coefficients""" - gradient_parameter_map: dict[Parameter, ParameterExpression] - """A dictionary maps the parameters of ``gradient_circuit`` to the parameter expressions of - ``circuit``""" - - -@dataclass -class LinearCombGradientCircuit: - """Gradient circuit for the linear combination of unitaries method.""" - - gradient_circuit: QuantumCircuit - """A gradient circuit for the linear combination of unitaries method.""" - coeff: float | ParameterExpression - """A coefficient corresponds to the gradient circuit.""" - - -################################################################################ -## Parameter shift gradient -################################################################################ -def _make_param_shift_parameter_values( - circuit: QuantumCircuit, - parameter_values: np.ndarray | list[float], - parameters: Sequence[Parameter], -) -> list[np.ndarray]: - """Returns a list of parameter values with offsets for parameter shift rule. - - Args: - circuit: The original quantum circuit - parameter_values: parameter values to be added to the base parameter values. - parameters: The parameters to be shifted. - - Returns: - A list of parameter values with offsets for parameter shift rule. - """ - indices = [circuit.parameters.data.index(p) for p in parameters] - offset = np.identity(circuit.num_parameters)[indices, :] - plus_offsets = parameter_values + offset * np.pi / 2 - minus_offsets = parameter_values - offset * np.pi / 2 - return plus_offsets.tolist() + minus_offsets.tolist() - - -################################################################################ -## Linear combination gradient and Linear combination QGT -################################################################################ -def _make_lin_comb_gradient_circuit( - circuit: QuantumCircuit, add_measurement: bool = False -) -> dict[Parameter, QuantumCircuit]: - """Makes a circuit that computes the linear combination of the gradient circuits.""" - circuit_temp = circuit.copy() - qr_aux = QuantumRegister(1, "qr_aux") - cr_aux = ClassicalRegister(1, "cr_aux") - circuit_temp.add_register(qr_aux) - circuit_temp.add_register(cr_aux) - circuit_temp.h(qr_aux) - circuit_temp.data.insert(0, circuit_temp.data.pop()) - circuit_temp.sdg(qr_aux) - circuit_temp.data.insert(1, circuit_temp.data.pop()) - - lin_comb_circuits = {} - for i, instruction in enumerate(circuit_temp.data): - if instruction.operation.is_parameterized(): - for p in instruction.operation.params[0].parameters: - gate = _gate_gradient(instruction.operation) - lin_comb_circuit = circuit_temp.copy() - # insert `gate` to i-th position - lin_comb_circuit.append(gate, [qr_aux[0]] + list(instruction.qubits), []) - lin_comb_circuit.data.insert(i, lin_comb_circuit.data.pop()) - lin_comb_circuit.h(qr_aux) - if add_measurement: - lin_comb_circuit.measure(qr_aux, cr_aux) - lin_comb_circuits[p] = lin_comb_circuit - - return lin_comb_circuits - - -def _gate_gradient(gate: Gate) -> Instruction: - """Returns the derivative of the gate""" - # pylint: disable=too-many-return-statements - if isinstance(gate, RXGate): - return CXGate() - if isinstance(gate, RYGate): - return CYGate() - if isinstance(gate, RZGate): - return CZGate() - if isinstance(gate, RXXGate): - cxx_circ = QuantumCircuit(3) - cxx_circ.cx(0, 1) - cxx_circ.cx(0, 2) - cxx = cxx_circ.to_instruction() - return cxx - if isinstance(gate, RYYGate): - cyy_circ = QuantumCircuit(3) - cyy_circ.cy(0, 1) - cyy_circ.cy(0, 2) - cyy = cyy_circ.to_instruction() - return cyy - if isinstance(gate, RZZGate): - czz_circ = QuantumCircuit(3) - czz_circ.cz(0, 1) - czz_circ.cz(0, 2) - czz = czz_circ.to_instruction() - return czz - if isinstance(gate, RZXGate): - czx_circ = QuantumCircuit(3) - czx_circ.cx(0, 2) - czx_circ.cz(0, 1) - czx = czx_circ.to_instruction() - return czx - raise TypeError(f"Unrecognized parameterized gate, {gate}") - - -def _make_lin_comb_qgt_circuit( - circuit: QuantumCircuit, add_measurement: bool = False -) -> dict[tuple[Parameter, Parameter], QuantumCircuit]: - """Makes a circuit that computes the linear combination of the QGT circuits.""" - circuit_temp = circuit.copy() - qr_aux = QuantumRegister(1, "aux") - circuit_temp.add_register(qr_aux) - if add_measurement: - cr_aux = ClassicalRegister(1, "aux") - circuit_temp.add_bits(cr_aux) - circuit_temp.h(qr_aux) - circuit_temp.data.insert(0, circuit_temp.data.pop()) - - lin_comb_qgt_circuits = {} - for i, instruction_i in enumerate(circuit_temp.data): - if not instruction_i.operation.is_parameterized(): - continue - for j, instruction_j in enumerate(circuit_temp.data): - if not instruction_j.operation.is_parameterized(): - continue - # Calculate the QGT of the i-th gate with respect to the j-th gate. - param_i = instruction_i.operation.params[0] - param_j = instruction_j.operation.params[0] - - for p_i in param_i.parameters: - for p_j in param_j.parameters: - if circuit_temp.parameters.data.index(p_i) > circuit_temp.parameters.data.index( - p_j - ): - continue - gate_i = _gate_gradient(instruction_i.operation) - gate_j = _gate_gradient(instruction_j.operation) - lin_comb_qgt_circuit = circuit_temp.copy() - if i < j: - # insert gate_j to j-th position - lin_comb_qgt_circuit.append( - gate_j, [qr_aux[0]] + list(instruction_j.qubits), [] - ) - lin_comb_qgt_circuit.data.insert(j, lin_comb_qgt_circuit.data.pop()) - # insert gate_i to i-th position with two X gates at its sides - lin_comb_qgt_circuit.append(XGate(), [qr_aux[0]], []) - lin_comb_qgt_circuit.data.insert(i, lin_comb_qgt_circuit.data.pop()) - lin_comb_qgt_circuit.append( - gate_i, [qr_aux[0]] + list(instruction_i.qubits), [] - ) - lin_comb_qgt_circuit.data.insert(i, lin_comb_qgt_circuit.data.pop()) - lin_comb_qgt_circuit.append(XGate(), [qr_aux[0]], []) - lin_comb_qgt_circuit.data.insert(i, lin_comb_qgt_circuit.data.pop()) - else: - # insert gate_i to i-th position - lin_comb_qgt_circuit.append( - gate_i, [qr_aux[0]] + list(instruction_i.qubits), [] - ) - lin_comb_qgt_circuit.data.insert(i, lin_comb_qgt_circuit.data.pop()) - # insert gate_j to j-th position with two X gates at its sides - lin_comb_qgt_circuit.append(XGate(), [qr_aux[0]], []) - lin_comb_qgt_circuit.data.insert(j, lin_comb_qgt_circuit.data.pop()) - lin_comb_qgt_circuit.append( - gate_j, [qr_aux[0]] + list(instruction_j.qubits), [] - ) - lin_comb_qgt_circuit.data.insert(j, lin_comb_qgt_circuit.data.pop()) - lin_comb_qgt_circuit.append(XGate(), [qr_aux[0]], []) - lin_comb_qgt_circuit.data.insert(j, lin_comb_qgt_circuit.data.pop()) - - lin_comb_qgt_circuit.h(qr_aux) - if add_measurement: - lin_comb_qgt_circuit.measure(qr_aux, cr_aux) - lin_comb_qgt_circuits[(p_i, p_j)] = lin_comb_qgt_circuit - - return lin_comb_qgt_circuits - - -def _make_lin_comb_observables( - observable: SparsePauliOp, - derivative_type: DerivativeType, -) -> tuple[SparsePauliOp, SparsePauliOp | None]: - """Make the observable with an ancillary operator for the linear combination gradient. - - Args: - observable: The observable. - derivative_type: The type of derivative. Can be either ``DerivativeType.REAL`` - ``DerivativeType.IMAG``, or ``DerivativeType.COMPLEX``. - - Returns: - The observable with an ancillary operator for the linear combination gradient. - - Raises: - ValueError: If the derivative type is not supported. - """ - if derivative_type == DerivativeType.REAL: - return observable.expand(SparsePauliOp.from_list([("Z", 1)])), None - elif derivative_type == DerivativeType.IMAG: - return observable.expand(SparsePauliOp.from_list([("Y", -1)])), None - elif derivative_type == DerivativeType.COMPLEX: - return observable.expand(SparsePauliOp.from_list([("Z", 1)])), observable.expand( - SparsePauliOp.from_list([("Y", -1)]) - ) - else: - raise ValueError(f"Derivative type {derivative_type} is not supported.") - - -################################################################################ -## Preprocess -################################################################################ -def _assign_unique_parameters( - circuit: QuantumCircuit, -) -> GradientCircuit: - """Assign unique parameters to the circuit. - - Args: - circuit: The circuit to assign unique parameters. - - Returns: - The circuit with unique parameters and the mapping from the original parameters to the - unique parameters. - """ - gradient_circuit = circuit.copy_empty_like(f"{circuit.name}_gradient") - parameter_map = defaultdict(list) - gradient_parameter_map = {} - num_gradient_parameters = 0 - for instruction in circuit.data: - if instruction.operation.is_parameterized(): - new_op_params = [] - for angle in instruction.operation.params: - new_parameter = Parameter(f"__gθ{num_gradient_parameters}") - new_op_params.append(new_parameter) - num_gradient_parameters += 1 - for parameter in angle.parameters: - parameter_map[parameter].append((new_parameter, angle.gradient(parameter))) - gradient_parameter_map[new_parameter] = angle - instruction.operation.params = new_op_params - gradient_circuit.append(instruction.operation, instruction.qubits, instruction.clbits) - # For the global phase - gradient_circuit.global_phase = circuit.global_phase - if isinstance(gradient_circuit.global_phase, ParameterExpression): - substitution_map = {} - for parameter in gradient_circuit.global_phase.parameters: - if parameter in parameter_map: - substitution_map[parameter] = parameter_map[parameter][0][0] - else: - new_parameter = Parameter(f"__gθ{num_gradient_parameters}") - substitution_map[parameter] = new_parameter - parameter_map[parameter].append((new_parameter, 1)) - num_gradient_parameters += 1 - gradient_circuit.global_phase = gradient_circuit.global_phase.subs(substitution_map) - return GradientCircuit(gradient_circuit, parameter_map, gradient_parameter_map) - - -def _make_gradient_parameter_values( - circuit: QuantumCircuit, - gradient_circuit: GradientCircuit, - parameter_values: np.ndarray, -) -> np.ndarray: - """Makes parameter values for the gradient circuit. - - Args: - circuit: The original quantum circuit - gradient_circuit: The gradient circuit - parameter_values: The parameter values for the original circuit - parameter_set: The parameter set to calculate gradients - - Returns: - The parameter values for the gradient circuit. - """ - g_circuit = gradient_circuit.gradient_circuit - g_parameter_values = np.empty(len(g_circuit.parameters)) - for i, g_parameter in enumerate(g_circuit.parameters): - expr = gradient_circuit.gradient_parameter_map[g_parameter] - bound_expr = expr.bind( - {p: parameter_values[circuit.parameters.data.index(p)] for p in expr.parameters} - ) - g_parameter_values[i] = float(bound_expr) - return g_parameter_values - - -def _make_gradient_parameters( - gradient_circuit: GradientCircuit, - parameters: Sequence[Parameter], -) -> Sequence[Parameter]: - """Makes parameter set for the gradient circuit. - - Args: - gradient_circuit: The gradient circuit - parameters: The parameters in the original circuit to calculate gradients - - Returns: - The parameters in the gradient circuit to calculate gradients. - """ - g_parameters = [ - g_parameter - for parameter in parameters - for g_parameter, _ in gradient_circuit.parameter_map[parameter] - ] - # make g_parameters unique and return it. - return list(dict.fromkeys(g_parameters)) diff --git a/qiskit/algorithms/list_or_dict.py b/qiskit/algorithms/list_or_dict.py deleted file mode 100644 index 95314dd79a3b..000000000000 --- a/qiskit/algorithms/list_or_dict.py +++ /dev/null @@ -1,18 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2022. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. - -"""Introduced new type to maintain readability.""" - -from typing import TypeVar, List, Union, Optional, Dict - -_T = TypeVar("_T") # Pylint does not allow single character class names. -ListOrDict = Union[List[Optional[_T]], Dict[str, _T]] diff --git a/qiskit/algorithms/minimum_eigen_solvers/__init__.py b/qiskit/algorithms/minimum_eigen_solvers/__init__.py deleted file mode 100644 index 3d7d18023b75..000000000000 --- a/qiskit/algorithms/minimum_eigen_solvers/__init__.py +++ /dev/null @@ -1,27 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2020. -# -# 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. - -"""Minimum Eigen Solvers Package""" - -from .vqe import VQE, VQEResult -from .qaoa import QAOA -from .numpy_minimum_eigen_solver import NumPyMinimumEigensolver -from .minimum_eigen_solver import MinimumEigensolver, MinimumEigensolverResult - -__all__ = [ - "VQE", - "VQEResult", - "QAOA", - "NumPyMinimumEigensolver", - "MinimumEigensolver", - "MinimumEigensolverResult", -] diff --git a/qiskit/algorithms/minimum_eigen_solvers/minimum_eigen_solver.py b/qiskit/algorithms/minimum_eigen_solvers/minimum_eigen_solver.py deleted file mode 100644 index 28afb68dcbf2..000000000000 --- a/qiskit/algorithms/minimum_eigen_solvers/minimum_eigen_solver.py +++ /dev/null @@ -1,143 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2020, 2022. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. - -"""The Minimum Eigensolver interface""" -from __future__ import annotations - -from abc import ABC, abstractmethod - -import numpy as np - -from qiskit.opflow import OperatorBase -from qiskit.utils.deprecation import deprecate_func -from ..algorithm_result import AlgorithmResult -from ..list_or_dict import ListOrDict - - -class MinimumEigensolver(ABC): - """Deprecated: Minimum Eigensolver Interface. - - The Minimum Eigensolver interface has been superseded by the - :class:`qiskit.algorithms.minimum_eigensolvers.MinimumEigensolver` interface. - This interface will be deprecated in a future release and subsequently - removed after that. - - Algorithms that can compute a minimum eigenvalue for an operator - may implement this interface to allow different algorithms to be - used interchangeably. - """ - - @deprecate_func( - additional_msg=( - "Instead, use the interface " - "``qiskit.algorithms.minimum_eigensolvers.MinimumEigensolver``. " - "See https://qisk.it/algo_migration for a migration guide." - ), - since="0.24.0", - package_name="qiskit-terra", - ) - def __init__(self) -> None: - pass - - @abstractmethod - def compute_minimum_eigenvalue( - self, operator: OperatorBase, aux_operators: ListOrDict[OperatorBase] | None = None - ) -> "MinimumEigensolverResult": - """ - Computes minimum eigenvalue. Operator and aux_operators can be supplied here and - if not None will override any already set into algorithm so it can be reused with - different operators. While an operator is required by algorithms, aux_operators - are optional. To 'remove' a previous aux_operators array use an empty list here. - - Args: - operator: Qubit operator of the Observable - aux_operators: Optional list of auxiliary operators to be evaluated with the - eigenstate of the minimum eigenvalue main result and their expectation values - returned. For instance in chemistry these can be dipole operators, total particle - count operators so we can get values for these at the ground state. - - Returns: - MinimumEigensolverResult - """ - return MinimumEigensolverResult() - - @classmethod - def supports_aux_operators(cls) -> bool: - """Whether computing the expectation value of auxiliary operators is supported. - - If the minimum eigensolver computes an eigenstate of the main operator then it - can compute the expectation value of the aux_operators for that state. Otherwise - they will be ignored. - - Returns: - True if aux_operator expectations can be evaluated, False otherwise - """ - return False - - -class MinimumEigensolverResult(AlgorithmResult): - """Deprecated: Minimum Eigensolver Result. - - The MinimumEigensolverResult class has been superseded by the - :class:`qiskit.algorithms.minimum_eigensolvers.MinimumEigensolverResult` class. - This class will be deprecated in a future release and subsequently - removed after that. - - """ - - @deprecate_func( - additional_msg=( - "Instead, use the class " - "``qiskit.algorithms.minimum_eigensolvers.MinimumEigensolverResult``. " - "See https://qisk.it/algo_migration for a migration guide." - ), - since="0.24.0", - package_name="qiskit-terra", - ) - def __init__(self) -> None: - super().__init__() - self._eigenvalue: complex | None = None - self._eigenstate: np.ndarray | None = None - self._aux_operator_eigenvalues: ListOrDict[tuple[complex, complex]] | None = None - - @property - def eigenvalue(self) -> complex | None: - """returns eigen value""" - return self._eigenvalue - - @eigenvalue.setter - def eigenvalue(self, value: complex) -> None: - """set eigen value""" - self._eigenvalue = value - - @property - def eigenstate(self) -> np.ndarray | None: - """return eigen state""" - return self._eigenstate - - @eigenstate.setter - def eigenstate(self, value: np.ndarray) -> None: - """set eigen state""" - self._eigenstate = value - - @property - def aux_operator_eigenvalues(self) -> ListOrDict[tuple[complex, complex]] | None: - """Return aux operator expectation values. - - These values are in fact tuples formatted as (mean, standard deviation). - """ - return self._aux_operator_eigenvalues - - @aux_operator_eigenvalues.setter - def aux_operator_eigenvalues(self, value: ListOrDict[tuple[complex, complex]]) -> None: - """set aux operator eigen values""" - self._aux_operator_eigenvalues = value diff --git a/qiskit/algorithms/minimum_eigen_solvers/numpy_minimum_eigen_solver.py b/qiskit/algorithms/minimum_eigen_solvers/numpy_minimum_eigen_solver.py deleted file mode 100644 index 5bc249d498e9..000000000000 --- a/qiskit/algorithms/minimum_eigen_solvers/numpy_minimum_eigen_solver.py +++ /dev/null @@ -1,106 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2020, 2022. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. - -"""The Numpy Minimum Eigensolver algorithm.""" -from __future__ import annotations - -import logging -import warnings -from collections.abc import Callable - -import numpy as np - -from qiskit.opflow import OperatorBase -from qiskit.utils.deprecation import deprecate_func -from ..eigen_solvers.numpy_eigen_solver import NumPyEigensolver -from .minimum_eigen_solver import MinimumEigensolver, MinimumEigensolverResult -from ..list_or_dict import ListOrDict - -logger = logging.getLogger(__name__) - - -class NumPyMinimumEigensolver(MinimumEigensolver): - """ - Deprecated: Numpy Minimum Eigensolver algorithm. - - The NumPyMinimumEigensolver class has been superseded by the - :class:`qiskit.algorithms.minimum_eigensolvers.NumPyMinimumEigensolver` class. - This class will be deprecated in a future release and subsequently - removed after that. - - """ - - @deprecate_func( - additional_msg=( - "Instead, use the class " - "``qiskit.algorithms.minimum_eigensolvers.NumPyMinimumEigensolver``. " - "See https://qisk.it/algo_migration for a migration guide." - ), - since="0.24.0", - package_name="qiskit-terra", - ) - def __init__( - self, - filter_criterion: Callable[ - [list | np.ndarray, float, ListOrDict[float] | None], bool - ] = None, - ) -> None: - """ - Args: - filter_criterion: callable that allows to filter eigenvalues/eigenstates. The minimum - eigensolver is only searching over feasible states and returns an eigenstate that - has the smallest eigenvalue among feasible states. The callable has the signature - `filter(eigenstate, eigenvalue, aux_values)` and must return a boolean to indicate - whether to consider this value or not. If there is no - feasible element, the result can even be empty. - """ - with warnings.catch_warnings(): - warnings.simplefilter("ignore") - super().__init__() - self._ces = NumPyEigensolver(filter_criterion=filter_criterion) - self._ret = MinimumEigensolverResult() - - @property - def filter_criterion( - self, - ) -> Callable[[list | np.ndarray, float, ListOrDict[float] | None], bool] | None: - """returns the filter criterion if set""" - return self._ces.filter_criterion - - @filter_criterion.setter - def filter_criterion( - self, - filter_criterion: Callable[[list | np.ndarray, float, ListOrDict[float] | None], bool] - | None, - ) -> None: - """set the filter criterion""" - self._ces.filter_criterion = filter_criterion - - @classmethod - def supports_aux_operators(cls) -> bool: - return NumPyEigensolver.supports_aux_operators() - - def compute_minimum_eigenvalue( - self, operator: OperatorBase, aux_operators: ListOrDict[OperatorBase] | None = None - ) -> MinimumEigensolverResult: - super().compute_minimum_eigenvalue(operator, aux_operators) - result_ces = self._ces.compute_eigenvalues(operator, aux_operators) - self._ret = MinimumEigensolverResult() - if result_ces.eigenvalues is not None and len(result_ces.eigenvalues) > 0: - self._ret.eigenvalue = result_ces.eigenvalues[0] - self._ret.eigenstate = result_ces.eigenstates[0] - if result_ces.aux_operator_eigenvalues: - self._ret.aux_operator_eigenvalues = result_ces.aux_operator_eigenvalues[0] - - logger.debug("MinimumEigensolver:\n%s", self._ret) - - return self._ret diff --git a/qiskit/algorithms/minimum_eigen_solvers/qaoa.py b/qiskit/algorithms/minimum_eigen_solvers/qaoa.py deleted file mode 100644 index 42e8c8546d9c..000000000000 --- a/qiskit/algorithms/minimum_eigen_solvers/qaoa.py +++ /dev/null @@ -1,186 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2018, 2021. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. - -"""The Quantum Approximate Optimization Algorithm.""" -from __future__ import annotations - -import warnings -from collections.abc import Callable - -import numpy as np - -from qiskit.algorithms.optimizers import Minimizer, Optimizer -from qiskit.circuit import QuantumCircuit -from qiskit.opflow import OperatorBase, ExpectationBase -from qiskit.opflow.gradients import GradientBase -from qiskit.providers import Backend -from qiskit.utils.quantum_instance import QuantumInstance -from qiskit.utils.validation import validate_min -from qiskit.utils.deprecation import deprecate_func -from qiskit.circuit.library.n_local.qaoa_ansatz import QAOAAnsatz -from qiskit.algorithms.minimum_eigen_solvers.vqe import VQE - - -class QAOA(VQE): - """ - Deprecated: Quantum Approximate Optimization Algorithm. - - The QAOA class has been superseded by the - :class:`qiskit.algorithms.minimum_eigensolvers.QAOA` class. - This class will be deprecated in a future release and subsequently - removed after that. - - `QAOA `__ is a well-known algorithm for finding approximate - solutions to combinatorial-optimization problems. - - The QAOA implementation directly extends :class:`VQE` and inherits VQE's optimization structure. - However, unlike VQE, which can be configured with arbitrary ansatzes, - QAOA uses its own fine-tuned ansatz, which comprises :math:`p` parameterized global - :math:`x` rotations and :math:`p` different parameterizations of the problem hamiltonian. - QAOA is thus principally configured by the single integer parameter, *p*, - which dictates the depth of the ansatz, and thus affects the approximation quality. - - An optional array of :math:`2p` parameter values, as the *initial_point*, may be provided as the - starting **beta** and **gamma** parameters (as identically named in the - original `QAOA paper `__) for the QAOA ansatz. - - An operator or a parameterized quantum circuit may optionally also be provided as a custom - `mixer` Hamiltonian. This allows, as discussed in - `this paper `__ for quantum annealing, - and in `this paper `__ for QAOA, - to run constrained optimization problems where the mixer constrains - the evolution to a feasible subspace of the full Hilbert space. - """ - - @deprecate_func( - additional_msg=( - "Instead, use the class ``qiskit.algorithms.minimum_eigensolvers.QAOA``. " - "See https://qisk.it/algo_migration for a migration guide." - ), - since="0.24.0", - package_name="qiskit-terra", - ) - def __init__( - self, - optimizer: Optimizer | Minimizer | None = None, - reps: int = 1, - initial_state: QuantumCircuit | None = None, - mixer: QuantumCircuit | OperatorBase = None, - initial_point: np.ndarray | None = None, - gradient: GradientBase | Callable[[np.ndarray | list], list] | None = None, - expectation: ExpectationBase | None = None, - include_custom: bool = False, - max_evals_grouped: int = 1, - callback: Callable[[int, np.ndarray, float, float], None] | None = None, - quantum_instance: QuantumInstance | Backend | None = None, - ) -> None: - """ - Args: - optimizer: A classical optimizer, see also :class:`~qiskit.algorithms.VQE` for - more details on the possible types. - reps: the integer parameter :math:`p` as specified in https://arxiv.org/abs/1411.4028, - Has a minimum valid value of 1. - initial_state: An optional initial state to prepend the QAOA circuit with - mixer: the mixer Hamiltonian to evolve with or a custom quantum circuit. Allows support - of optimizations in constrained subspaces as per https://arxiv.org/abs/1709.03489 - as well as warm-starting the optimization as introduced - in http://arxiv.org/abs/2009.10095. - initial_point: An optional initial point (i.e. initial parameter values) - for the optimizer. If ``None`` then it will simply compute a random one. - gradient: An optional gradient operator respectively a gradient function used for - optimization. - expectation: The Expectation converter for taking the average value of the - Observable over the ansatz state function. When None (the default) an - :class:`~qiskit.opflow.expectations.ExpectationFactory` is used to select - an appropriate expectation based on the operator and backend. When using Aer - qasm_simulator backend, with paulis, it is however much faster to leverage custom - Aer function for the computation but, although VQE performs much faster - with it, the outcome is ideal, with no shot noise, like using a state vector - simulator. If you are just looking for the quickest performance when choosing Aer - qasm_simulator and the lack of shot noise is not an issue then set `include_custom` - parameter here to True (defaults to False). - include_custom: When `expectation` parameter here is None setting this to True will - allow the factory to include the custom Aer pauli expectation. - max_evals_grouped: Max number of evaluations performed simultaneously. Signals the - given optimizer that more than one set of parameters can be supplied so that - potentially the expectation values can be computed in parallel. Typically this is - possible when a finite difference gradient is used by the optimizer such that - multiple points to compute the gradient can be passed and if computed in parallel - improve overall execution time. Ignored if a gradient operator or function is - given. - callback: a callback that can access the intermediate data during the optimization. - Four parameter values are passed to the callback as follows during each evaluation - by the optimizer for its current set of parameters as it works towards the minimum. - These are: the evaluation count, the optimizer parameters for the - ansatz, the evaluated mean and the evaluated standard deviation. - quantum_instance: Quantum Instance or Backend - """ - validate_min("reps", reps, 1) - - self._reps = reps - self._mixer = mixer - self._initial_state = initial_state - self._cost_operator: OperatorBase | None = None - - with warnings.catch_warnings(): - warnings.simplefilter("ignore") - super().__init__( - ansatz=None, - optimizer=optimizer, - initial_point=initial_point, - gradient=gradient, - expectation=expectation, - include_custom=include_custom, - max_evals_grouped=max_evals_grouped, - callback=callback, - quantum_instance=quantum_instance, - ) - - def _check_operator_ansatz(self, operator: OperatorBase) -> None: - # Recreates a circuit based on operator parameter. - if operator != self._cost_operator: - self._cost_operator = operator - self.ansatz = QAOAAnsatz( - operator, self._reps, initial_state=self._initial_state, mixer_operator=self._mixer - ).decompose() # TODO remove decompose once #6674 is fixed - - @property - def initial_state(self) -> QuantumCircuit | None: - """ - Returns: - Returns the initial state. - """ - return self._initial_state - - @initial_state.setter - def initial_state(self, initial_state: QuantumCircuit | None) -> None: - """ - Args: - initial_state: Initial state to set. - """ - self._initial_state = initial_state - - @property - def mixer(self) -> QuantumCircuit | OperatorBase: - """ - Returns: - Returns the mixer. - """ - return self._mixer - - @mixer.setter - def mixer(self, mixer: QuantumCircuit | OperatorBase) -> None: - """ - Args: - mixer: Mixer to set. - """ - self._mixer = mixer diff --git a/qiskit/algorithms/minimum_eigen_solvers/vqe.py b/qiskit/algorithms/minimum_eigen_solvers/vqe.py deleted file mode 100644 index 43bec3c772e7..000000000000 --- a/qiskit/algorithms/minimum_eigen_solvers/vqe.py +++ /dev/null @@ -1,751 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2018, 2022. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. - -"""The Variational Quantum Eigensolver algorithm. - -See https://arxiv.org/abs/1304.3061 -""" - -from __future__ import annotations - -import logging -import warnings -from collections.abc import Callable -from time import time - -import numpy as np - -from qiskit.circuit import Parameter, QuantumCircuit -from qiskit.circuit.library import RealAmplitudes -from qiskit.opflow import ( - CircuitSampler, - CircuitStateFn, - ExpectationBase, - ExpectationFactory, - ListOp, - OperatorBase, - PauliSumOp, - StateFn, -) -from qiskit.opflow.gradients import GradientBase -from qiskit.providers import Backend -from qiskit.utils import QuantumInstance, algorithm_globals -from qiskit.utils.backend_utils import is_aer_provider -from qiskit.utils.validation import validate_min -from qiskit.utils.deprecation import deprecate_func - -from ..aux_ops_evaluator import eval_observables -from ..exceptions import AlgorithmError -from ..list_or_dict import ListOrDict -from ..optimizers import SLSQP, Minimizer, Optimizer -from ..variational_algorithm import VariationalAlgorithm, VariationalResult -from .minimum_eigen_solver import MinimumEigensolver, MinimumEigensolverResult - -logger = logging.getLogger(__name__) - - -class VQE(VariationalAlgorithm, MinimumEigensolver): - r"""Deprecated: Variational Quantum Eigensolver algorithm. - - The VQE class has been superseded by the - :class:`qiskit.algorithms.minimum_eigensolvers.VQE` class. - This class will be deprecated in a future release and subsequently - removed after that. - - `VQE `__ is a quantum algorithm that uses a - variational technique to find - the minimum eigenvalue of the Hamiltonian :math:`H` of a given system. - - An instance of VQE requires defining two algorithmic sub-components: - a trial state (a.k.a. ansatz) which is a :class:`QuantumCircuit`, and one of the classical - :mod:`~qiskit.algorithms.optimizers`. The ansatz is varied, via its set of parameters, by the - optimizer, such that it works towards a state, as determined by the parameters applied to the - ansatz, that will result in the minimum expectation value being measured of the input operator - (Hamiltonian). - - An optional array of parameter values, via the *initial_point*, may be provided as the - starting point for the search of the minimum eigenvalue. This feature is particularly useful - such as when there are reasons to believe that the solution point is close to a particular - point. As an example, when building the dissociation profile of a molecule, - it is likely that using the previous computed optimal solution as the starting - initial point for the next interatomic distance is going to reduce the number of iterations - necessary for the variational algorithm to converge. It provides an - `initial point tutorial `__ detailing this use case. - - The length of the *initial_point* list value must match the number of the parameters - expected by the ansatz being used. If the *initial_point* is left at the default - of ``None``, then VQE will look to the ansatz for a preferred value, based on its - given initial state. If the ansatz returns ``None``, - then a random point will be generated within the parameter bounds set, as per above. - If the ansatz provides ``None`` as the lower bound, then VQE - will default it to :math:`-2\pi`; similarly, if the ansatz returns ``None`` - as the upper bound, the default value will be :math:`2\pi`. - - The optimizer can either be one of Qiskit's optimizers, such as - :class:`~qiskit.algorithms.optimizers.SPSA` or a callable with the following signature: - - .. note:: - - The callable _must_ have the argument names ``fun, x0, jac, bounds`` as indicated - in the following code block. - - .. code-block:: python - - from qiskit.algorithms.optimizers import OptimizerResult - - def my_minimizer(fun, x0, jac=None, bounds=None) -> OptimizerResult: - # Note that the callable *must* have these argument names! - # Args: - # fun (callable): the function to minimize - # x0 (np.ndarray): the initial point for the optimization - # jac (callable, optional): the gradient of the objective function - # bounds (list, optional): a list of tuples specifying the parameter bounds - - result = OptimizerResult() - result.x = # optimal parameters - result.fun = # optimal function value - return result - - The above signature also allows to directly pass any SciPy minimizer, for instance as - - .. code-block:: python - - from functools import partial - from scipy.optimize import minimize - - optimizer = partial(minimize, method="L-BFGS-B") - - """ - - @deprecate_func( - additional_msg=( - "Instead, use the class ``qiskit.algorithms.minimum_eigensolvers.VQE``. " - "See https://qisk.it/algo_migration for a migration guide." - ), - since="0.24.0", - package_name="qiskit-terra", - ) - def __init__( - self, - ansatz: QuantumCircuit | None = None, - optimizer: Optimizer | Minimizer | None = None, - initial_point: np.ndarray | None = None, - gradient: GradientBase | Callable | None = None, - expectation: ExpectationBase | None = None, - include_custom: bool = False, - max_evals_grouped: int = 1, - callback: Callable[[int, np.ndarray, float, float], None] | None = None, - quantum_instance: QuantumInstance | Backend | None = None, - ) -> None: - """ - - Args: - ansatz: A parameterized circuit used as Ansatz for the wave function. - optimizer: A classical optimizer. Can either be a Qiskit optimizer or a callable - that takes an array as input and returns a Qiskit or SciPy optimization result. - initial_point: An optional initial point (i.e. initial parameter values) - for the optimizer. If ``None`` then VQE will look to the ansatz for a preferred - point and if not will simply compute a random one. - gradient: An optional gradient function or operator for optimizer. - expectation: The Expectation converter for taking the average value of the - Observable over the ansatz state function. When ``None`` (the default) an - :class:`~qiskit.opflow.expectations.ExpectationFactory` is used to select - an appropriate expectation based on the operator and backend. When using Aer - qasm_simulator backend, with paulis, it is however much faster to leverage custom - Aer function for the computation but, although VQE performs much faster - with it, the outcome is ideal, with no shot noise, like using a state vector - simulator. If you are just looking for the quickest performance when choosing Aer - qasm_simulator and the lack of shot noise is not an issue then set `include_custom` - parameter here to ``True`` (defaults to ``False``). - include_custom: When `expectation` parameter here is None setting this to ``True`` will - allow the factory to include the custom Aer pauli expectation. - max_evals_grouped: Max number of evaluations performed simultaneously. Signals the - given optimizer that more than one set of parameters can be supplied so that - potentially the expectation values can be computed in parallel. Typically this is - possible when a finite difference gradient is used by the optimizer such that - multiple points to compute the gradient can be passed and if computed in parallel - improve overall execution time. Deprecated if a gradient operator or function is - given. - callback: a callback that can access the intermediate data during the optimization. - Four parameter values are passed to the callback as follows during each evaluation - by the optimizer for its current set of parameters as it works towards the minimum. - These are: the evaluation count, the optimizer parameters for the - ansatz, the evaluated mean and the evaluated standard deviation.` - quantum_instance: Quantum Instance or Backend - - """ - validate_min("max_evals_grouped", max_evals_grouped, 1) - - with warnings.catch_warnings(): - warnings.simplefilter("ignore") - super().__init__() - - self._max_evals_grouped = max_evals_grouped - self._circuit_sampler: CircuitSampler | None = None - self._expectation = None - self.expectation = expectation - self._include_custom = include_custom - - self._ansatz: QuantumCircuit | None = None - self.ansatz = ansatz - - self._optimizer: Optimizer | None = None - self.optimizer = optimizer - - self._initial_point: np.ndarray | None = None - self.initial_point = initial_point - self._gradient: GradientBase | Callable | None = None - self.gradient = gradient - self._quantum_instance: QuantumInstance | None = None - - if quantum_instance is not None: - self.quantum_instance = quantum_instance - - self._eval_time = None - self._eval_count = 0 - self._callback: Callable[[int, np.ndarray, float, float], None] | None = None - self.callback = callback - - logger.info(self.print_settings()) - - # TODO remove this once the stateful methods are deleted - self._ret: VQEResult | None = None - - @property - def ansatz(self) -> QuantumCircuit: - """Returns the ansatz.""" - return self._ansatz - - @ansatz.setter - def ansatz(self, ansatz: QuantumCircuit | None): - """Sets the ansatz. - - Args: - ansatz: The parameterized circuit used as an ansatz. - If None is passed, RealAmplitudes is used by default. - - """ - if ansatz is None: - ansatz = RealAmplitudes() - - self._ansatz = ansatz - - @property - def gradient(self) -> GradientBase | Callable | None: - """Returns the gradient.""" - return self._gradient - - @gradient.setter - def gradient(self, gradient: GradientBase | Callable | None): - """Sets the gradient.""" - self._gradient = gradient - - @property - def quantum_instance(self) -> QuantumInstance | None: - """Returns quantum instance.""" - return self._quantum_instance - - @quantum_instance.setter - def quantum_instance(self, quantum_instance: QuantumInstance | Backend) -> None: - """Sets quantum_instance""" - if not isinstance(quantum_instance, QuantumInstance): - quantum_instance = QuantumInstance(quantum_instance) - - self._quantum_instance = quantum_instance - self._circuit_sampler = CircuitSampler( - quantum_instance, param_qobj=is_aer_provider(quantum_instance.backend) - ) - - @property - def initial_point(self) -> np.ndarray | None: - """Returns initial point""" - return self._initial_point - - @initial_point.setter - def initial_point(self, initial_point: np.ndarray): - """Sets initial point""" - self._initial_point = initial_point - - @property - def max_evals_grouped(self) -> int: - """Returns max_evals_grouped""" - return self._max_evals_grouped - - @max_evals_grouped.setter - def max_evals_grouped(self, max_evals_grouped: int): - """Sets max_evals_grouped""" - self._max_evals_grouped = max_evals_grouped - self.optimizer.set_max_evals_grouped(max_evals_grouped) - - @property - def include_custom(self) -> bool: - """Returns include_custom""" - return self._include_custom - - @include_custom.setter - def include_custom(self, include_custom: bool): - """Sets include_custom. If set to another value than the one that was previsously set, - the expectation attribute is reset to None. - """ - if include_custom != self._include_custom: - self._include_custom = include_custom - self.expectation = None - - @property - def callback(self) -> Callable[[int, np.ndarray, float, float], None] | None: - """Returns callback""" - return self._callback - - @callback.setter - def callback(self, callback: Callable[[int, np.ndarray, float, float], None] | None): - """Sets callback""" - self._callback = callback - - @property - def expectation(self) -> ExpectationBase | None: - """The expectation value algorithm used to construct the expectation measurement from - the observable.""" - return self._expectation - - @expectation.setter - def expectation(self, exp: ExpectationBase | None) -> None: - self._expectation = exp - - def _check_operator_ansatz(self, operator: OperatorBase): - """Check that the number of qubits of operator and ansatz match.""" - if operator is not None and self.ansatz is not None: - if operator.num_qubits != self.ansatz.num_qubits: - # try to set the number of qubits on the ansatz, if possible - try: - self.ansatz.num_qubits = operator.num_qubits - except AttributeError as ex: - raise AlgorithmError( - "The number of qubits of the ansatz does not match the " - "operator, and the ansatz does not allow setting the " - "number of qubits using `num_qubits`." - ) from ex - - @property - def optimizer(self) -> Optimizer: - """Returns optimizer""" - return self._optimizer - - @optimizer.setter - def optimizer(self, optimizer: Optimizer | None): - """Sets the optimizer attribute. - - Args: - optimizer: The optimizer to be used. If None is passed, SLSQP is used by default. - - """ - if optimizer is None: - optimizer = SLSQP() - - if isinstance(optimizer, Optimizer): - optimizer.set_max_evals_grouped(self.max_evals_grouped) - - self._optimizer = optimizer - - @property - def setting(self): - """Prepare the setting of VQE as a string.""" - ret = f"Algorithm: {self.__class__.__name__}\n" - params = "" - for key, value in self.__dict__.items(): - if key[0] == "_": - if "initial_point" in key and value is None: - params += "-- {}: {}\n".format(key[1:], "Random seed") - else: - params += f"-- {key[1:]}: {value}\n" - ret += f"{params}" - return ret - - def print_settings(self): - """ - Preparing the setting of VQE into a string. - - Returns: - str: the formatted setting of VQE - """ - ret = "\n" - ret += "==================== Setting of {} ============================\n".format( - self.__class__.__name__ - ) - ret += f"{self.setting}" - ret += "===============================================================\n" - if self.ansatz is not None: - ret += "{}".format(self.ansatz.draw(output="text")) - else: - ret += "ansatz has not been set" - ret += "===============================================================\n" - if callable(self.optimizer): - ret += "Optimizer is custom callable\n" - else: - ret += f"{self._optimizer.setting}" - ret += "===============================================================\n" - return ret - - def construct_expectation( - self, - parameter: list[float] | list[Parameter] | np.ndarray, - operator: OperatorBase, - return_expectation: bool = False, - ) -> OperatorBase | tuple[OperatorBase, ExpectationBase]: - r""" - Generate the ansatz circuit and expectation value measurement, and return their - runnable composition. - - Args: - parameter: Parameters for the ansatz circuit. - operator: Qubit operator of the Observable - return_expectation: If True, return the ``ExpectationBase`` expectation converter used - in the construction of the expectation value. Useful e.g. to compute the standard - deviation of the expectation value. - - Returns: - The Operator equalling the measurement of the ansatz :class:`StateFn` by the - Observable's expectation :class:`StateFn`, and, optionally, the expectation converter. - - Raises: - AlgorithmError: If no operator has been provided. - AlgorithmError: If no expectation is passed and None could be inferred via the - ExpectationFactory. - """ - if operator is None: - raise AlgorithmError("The operator was never provided.") - - self._check_operator_ansatz(operator) - - # if expectation was never created, try to create one - if self.expectation is None: - expectation = ExpectationFactory.build( - operator=operator, - backend=self.quantum_instance, - include_custom=self._include_custom, - ) - else: - expectation = self.expectation - - wave_function = self.ansatz.assign_parameters(parameter) - - observable_meas = expectation.convert(StateFn(operator, is_measurement=True)) - ansatz_circuit_op = CircuitStateFn(wave_function) - expect_op = observable_meas.compose(ansatz_circuit_op).reduce() - - if return_expectation: - return expect_op, expectation - - return expect_op - - def construct_circuit( - self, - parameter: list[float] | list[Parameter] | np.ndarray, - operator: OperatorBase, - ) -> list[QuantumCircuit]: - """Return the circuits used to compute the expectation value. - - Args: - parameter: Parameters for the ansatz circuit. - operator: Qubit operator of the Observable - - Returns: - A list of the circuits used to compute the expectation value. - """ - expect_op = self.construct_expectation(parameter, operator).to_circuit_op() - - circuits = [] - - # recursively extract circuits - def extract_circuits(op): - if isinstance(op, CircuitStateFn): - circuits.append(op.primitive) - elif isinstance(op, ListOp): - for op_i in op.oplist: - extract_circuits(op_i) - - extract_circuits(expect_op) - - return circuits - - @classmethod - def supports_aux_operators(cls) -> bool: - return True - - def compute_minimum_eigenvalue( - self, operator: OperatorBase, aux_operators: ListOrDict[OperatorBase] | None = None - ) -> MinimumEigensolverResult: - super().compute_minimum_eigenvalue(operator, aux_operators) - - if self.quantum_instance is None: - raise AlgorithmError( - "A QuantumInstance or Backend must be supplied to run the quantum algorithm." - ) - self.quantum_instance.circuit_summary = True - - # this sets the size of the ansatz, so it must be called before the initial point - # validation - self._check_operator_ansatz(operator) - - # set an expectation for this algorithm run (will be reset to None at the end) - initial_point = _validate_initial_point(self.initial_point, self.ansatz) - - bounds = _validate_bounds(self.ansatz) - # We need to handle the array entries being zero or Optional i.e. having value None - if aux_operators: - zero_op = PauliSumOp.from_list([("I" * self.ansatz.num_qubits, 0)]) - - # Convert the None and zero values when aux_operators is a list. - # Drop None and convert zero values when aux_operators is a dict. - if isinstance(aux_operators, list): - key_op_iterator = enumerate(aux_operators) - converted: ListOrDict[OperatorBase] = [zero_op] * len(aux_operators) - else: - key_op_iterator = aux_operators.items() - converted = {} - for key, op in key_op_iterator: - if op is not None: - converted[key] = zero_op if op == 0 else op - - aux_operators = converted - - else: - aux_operators = None - - # Convert the gradient operator into a callable function that is compatible with the - # optimization routine. - if isinstance(self._gradient, GradientBase): - gradient = self._gradient.gradient_wrapper( - ~StateFn(operator) @ StateFn(self.ansatz), - bind_params=list(self.ansatz.parameters), - backend=self._quantum_instance, - ) - else: - gradient = self._gradient - - self._eval_count = 0 - energy_evaluation, expectation = self.get_energy_evaluation( - operator, return_expectation=True - ) - - start_time = time() - - if callable(self.optimizer): - opt_result = self.optimizer( # pylint: disable=not-callable - fun=energy_evaluation, x0=initial_point, jac=gradient, bounds=bounds - ) - else: - opt_result = self.optimizer.minimize( - fun=energy_evaluation, x0=initial_point, jac=gradient, bounds=bounds - ) - - eval_time = time() - start_time - - result = VQEResult() - result.optimal_point = opt_result.x - result.optimal_parameters = dict(zip(self.ansatz.parameters, opt_result.x)) - result.optimal_value = opt_result.fun - result.cost_function_evals = opt_result.nfev - result.optimizer_time = eval_time - result.eigenvalue = opt_result.fun + 0j - result.eigenstate = self._get_eigenstate(result.optimal_parameters) - - logger.info( - "Optimization complete in %s seconds.\nFound opt_params %s in %s evals", - eval_time, - result.optimal_point, - self._eval_count, - ) - - # TODO delete as soon as get_optimal_vector etc are removed - self._ret = result - - if aux_operators is not None: - bound_ansatz = self.ansatz.assign_parameters(result.optimal_point) - - aux_values = eval_observables( - self.quantum_instance, bound_ansatz, aux_operators, expectation=expectation - ) - result.aux_operator_eigenvalues = aux_values - - return result - - def get_energy_evaluation( - self, - operator: OperatorBase, - return_expectation: bool = False, - ) -> Callable[[np.ndarray], float | list[float]] | tuple[ - Callable[[np.ndarray], float | list[float]], ExpectationBase - ]: - """Returns a function handle to evaluates the energy at given parameters for the ansatz. - - This is the objective function to be passed to the optimizer that is used for evaluation. - - Args: - operator: The operator whose energy to evaluate. - return_expectation: If True, return the ``ExpectationBase`` expectation converter used - in the construction of the expectation value. Useful e.g. to evaluate other - operators with the same expectation value converter. - - - Returns: - Energy of the hamiltonian of each parameter, and, optionally, the expectation - converter. - - Raises: - RuntimeError: If the circuit is not parameterized (i.e. has 0 free parameters). - - """ - num_parameters = self.ansatz.num_parameters - if num_parameters == 0: - raise RuntimeError("The ansatz must be parameterized, but has 0 free parameters.") - - ansatz_params = self.ansatz.parameters - expect_op, expectation = self.construct_expectation( - ansatz_params, operator, return_expectation=True - ) - - def energy_evaluation(parameters): - parameter_sets = np.reshape(parameters, (-1, num_parameters)) - # Create dict associating each parameter with the lists of parameterization values for it - param_bindings = dict(zip(ansatz_params, parameter_sets.transpose().tolist())) - - start_time = time() - sampled_expect_op = self._circuit_sampler.convert(expect_op, params=param_bindings) - means = np.real(sampled_expect_op.eval()) - - if self._callback is not None: - variance = np.real(expectation.compute_variance(sampled_expect_op)) - estimator_error = np.sqrt(variance / self.quantum_instance.run_config.shots) - for i, param_set in enumerate(parameter_sets): - self._eval_count += 1 - self._callback(self._eval_count, param_set, means[i], estimator_error[i]) - else: - self._eval_count += len(means) - - end_time = time() - logger.info( - "Energy evaluation returned %s - %.5f (ms), eval count: %s", - means, - (end_time - start_time) * 1000, - self._eval_count, - ) - - return means if len(means) > 1 else means[0] - - if return_expectation: - return energy_evaluation, expectation - - return energy_evaluation - - def _get_eigenstate(self, optimal_parameters) -> list[float] | dict[str, int]: - """Get the simulation outcome of the ansatz, provided with parameters.""" - optimal_circuit = self.ansatz.assign_parameters(optimal_parameters) - state_fn = self._circuit_sampler.convert(StateFn(optimal_circuit)).eval() - if self.quantum_instance.is_statevector: - state = state_fn.primitive.data # VectorStateFn -> Statevector -> np.array - else: - state = state_fn.to_dict_fn().primitive # SparseVectorStateFn -> DictStateFn -> dict - - return state - - -class VQEResult(VariationalResult, MinimumEigensolverResult): - """Deprecated: VQE Result. - - The VQEResult class has been superseded by the - :class:`qiskit.algorithms.minimum_eigensolvers.VQEResult` class. - This class will be deprecated in a future release and subsequently - removed after that. - - """ - - @deprecate_func( - additional_msg=( - "Instead, use the class ``qiskit.algorithms.minimum_eigensolvers.VQEResult``. " - "See https://qisk.it/algo_migration for a migration guide." - ), - since="0.24.0", - package_name="qiskit-terra", - ) - def __init__(self) -> None: - with warnings.catch_warnings(): - warnings.simplefilter("ignore") - super().__init__() - self._cost_function_evals: int | None = None - - @property - def cost_function_evals(self) -> int | None: - """Returns number of cost optimizer evaluations""" - return self._cost_function_evals - - @cost_function_evals.setter - def cost_function_evals(self, value: int) -> None: - """Sets number of cost function evaluations""" - self._cost_function_evals = value - - @property - def eigenstate(self) -> np.ndarray | None: - """return eigen state""" - return self._eigenstate - - @eigenstate.setter - def eigenstate(self, value: np.ndarray) -> None: - """set eigen state""" - self._eigenstate = value - - -def _validate_initial_point(point, ansatz): - expected_size = ansatz.num_parameters - - # try getting the initial point from the ansatz - if point is None and hasattr(ansatz, "preferred_init_points"): - point = ansatz.preferred_init_points - # if the point is None choose a random initial point - - if point is None: - # get bounds if ansatz has them set, otherwise use [-2pi, 2pi] for each parameter - bounds = getattr(ansatz, "parameter_bounds", None) - if bounds is None: - bounds = [(-2 * np.pi, 2 * np.pi)] * expected_size - - # replace all Nones by [-2pi, 2pi] - lower_bounds = [] - upper_bounds = [] - for lower, upper in bounds: - lower_bounds.append(lower if lower is not None else -2 * np.pi) - upper_bounds.append(upper if upper is not None else 2 * np.pi) - - # sample from within bounds - point = algorithm_globals.random.uniform(lower_bounds, upper_bounds) - - elif len(point) != expected_size: - raise ValueError( - f"The dimension of the initial point ({len(point)}) does not match the " - f"number of parameters in the circuit ({expected_size})." - ) - - return point - - -def _validate_bounds(ansatz): - if hasattr(ansatz, "parameter_bounds") and ansatz.parameter_bounds is not None: - bounds = ansatz.parameter_bounds - if len(bounds) != ansatz.num_parameters: - raise ValueError( - f"The number of bounds ({len(bounds)}) does not match the number of " - f"parameters in the circuit ({ansatz.num_parameters})." - ) - else: - bounds = [(None, None)] * ansatz.num_parameters - - return bounds diff --git a/qiskit/algorithms/minimum_eigensolvers/__init__.py b/qiskit/algorithms/minimum_eigensolvers/__init__.py deleted file mode 100644 index d7406c09860b..000000000000 --- a/qiskit/algorithms/minimum_eigensolvers/__init__.py +++ /dev/null @@ -1,66 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2022. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. - -""" -============================================================================ -Minimum Eigensolvers Package (:mod:`qiskit.algorithms.minimum_eigensolvers`) -============================================================================ - -.. currentmodule:: qiskit.algorithms.minimum_eigensolvers - -Minimum Eigensolvers -==================== -.. autosummary:: - :toctree: ../stubs/ - - MinimumEigensolver - NumPyMinimumEigensolver - VQE - AdaptVQE - SamplingMinimumEigensolver - SamplingVQE - QAOA - -.. autosummary:: - :toctree: ../stubs/ - - MinimumEigensolverResult - NumPyMinimumEigensolverResult - VQEResult - AdaptVQEResult - SamplingMinimumEigensolverResult - SamplingVQEResult -""" - -from .adapt_vqe import AdaptVQE, AdaptVQEResult -from .minimum_eigensolver import MinimumEigensolver, MinimumEigensolverResult -from .numpy_minimum_eigensolver import NumPyMinimumEigensolver, NumPyMinimumEigensolverResult -from .vqe import VQE, VQEResult -from .sampling_mes import SamplingMinimumEigensolver, SamplingMinimumEigensolverResult -from .sampling_vqe import SamplingVQE, SamplingVQEResult -from .qaoa import QAOA - -__all__ = [ - "AdaptVQE", - "AdaptVQEResult", - "MinimumEigensolver", - "MinimumEigensolverResult", - "NumPyMinimumEigensolver", - "NumPyMinimumEigensolverResult", - "VQE", - "VQEResult", - "SamplingMinimumEigensolver", - "SamplingMinimumEigensolverResult", - "SamplingVQE", - "SamplingVQEResult", - "QAOA", -] diff --git a/qiskit/algorithms/minimum_eigensolvers/adapt_vqe.py b/qiskit/algorithms/minimum_eigensolvers/adapt_vqe.py deleted file mode 100644 index b2b371f70970..000000000000 --- a/qiskit/algorithms/minimum_eigensolvers/adapt_vqe.py +++ /dev/null @@ -1,422 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2022, 2023. -# -# 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. - -"""An implementation of the AdaptVQE algorithm.""" -from __future__ import annotations - -from collections.abc import Sequence -from enum import Enum - -import re -import logging -import warnings -from typing import Any - -import numpy as np - -from qiskit import QiskitError -from qiskit.algorithms.list_or_dict import ListOrDict -from qiskit.quantum_info.operators.base_operator import BaseOperator -from qiskit.opflow import OperatorBase, PauliSumOp -from qiskit.circuit.library import EvolvedOperatorAnsatz -from qiskit.utils.deprecation import deprecate_arg, deprecate_func -from qiskit.utils.validation import validate_min - -from .minimum_eigensolver import MinimumEigensolver -from .vqe import VQE, VQEResult -from ..observables_evaluator import estimate_observables -from ..variational_algorithm import VariationalAlgorithm - - -logger = logging.getLogger(__name__) - - -class TerminationCriterion(Enum): - """A class enumerating the various finishing criteria.""" - - CONVERGED = "Threshold converged" - CYCLICITY = "Aborted due to a cyclic selection of evolution operators" - MAXIMUM = "Maximum number of iterations reached" - - -class AdaptVQE(VariationalAlgorithm, MinimumEigensolver): - """The Adaptive Variational Quantum Eigensolver algorithm. - - `AdaptVQE `__ is a quantum algorithm which creates a compact - ansatz from a set of evolution operators. It iteratively extends the ansatz circuit, by - selecting the building block that leads to the largest gradient from a set of candidates. In - chemistry, this is usually a list of orbital excitations. Thus, a common choice of ansatz to be - used with this algorithm is the Unitary Coupled Cluster ansatz implemented in Qiskit Nature. - This results in a wavefunction ansatz which is uniquely adapted to the operator whose minimum - eigenvalue is being determined. This class relies on a supplied instance of :class:`~.VQE` to - find the minimum eigenvalue. The performance of AdaptVQE significantly depends on the - minimization routine. - - .. code-block:: python - - from qiskit.algorithms.minimum_eigensolvers import AdaptVQE, VQE - from qiskit.algorithms.optimizers import SLSQP - from qiskit.primitives import Estimator - from qiskit.circuit.library import EvolvedOperatorAnsatz - - # get your Hamiltonian - hamiltonian = ... - - # construct your ansatz - ansatz = EvolvedOperatorAnsatz(...) - - vqe = VQE(Estimator(), ansatz, SLSQP()) - - adapt_vqe = AdaptVQE(vqe) - - eigenvalue, _ = adapt_vqe.compute_minimum_eigenvalue(hamiltonian) - - The following attributes can be set via the initializer but can also be read and updated once - the AdaptVQE object has been constructed. - - Attributes: - solver: a :class:`~.VQE` instance used internally to compute the minimum eigenvalues. - It is a requirement that the :attr:`~.VQE.ansatz` of this solver is of type - :class:`~qiskit.circuit.library.EvolvedOperatorAnsatz`. - gradient_threshold: once all gradients have an absolute value smaller than this threshold, - the algorithm has converged and terminates. - eigenvalue_threshold: once the eigenvalue has changed by less than this threshold from one - iteration to the next, the algorithm has converged and terminates. When this case - occurs, the excitation included in the final iteration did not result in a significant - improvement of the eigenvalue and, thus, the results from this iteration are not - considered. - max_iterations: the maximum number of iterations for the adaptive loop. If ``None``, the - algorithm is not bound in its number of iterations. - """ - - @deprecate_arg( - "threshold", - since="0.24.0", - package_name="qiskit-terra", - pending=True, - new_alias="gradient_threshold", - ) - def __init__( - self, - solver: VQE, - *, - gradient_threshold: float = 1e-5, - eigenvalue_threshold: float = 1e-5, - max_iterations: int | None = None, - threshold: float | None = None, # pylint: disable=unused-argument - ) -> None: - """ - Args: - solver: a :class:`~.VQE` instance used internally to compute the minimum eigenvalues. - It is a requirement that the :attr:`~.VQE.ansatz` of this solver is of type - :class:`~qiskit.circuit.library.EvolvedOperatorAnsatz`. - gradient_threshold: once all gradients have an absolute value smaller than this - threshold, the algorithm has converged and terminates. - eigenvalue_threshold: once the eigenvalue has changed by less than this threshold from - one iteration to the next, the algorithm has converged and terminates. When this - case occurs, the excitation included in the final iteration did not result in a - significant improvement of the eigenvalue and, thus, the results from this iteration - are not considered. - max_iterations: the maximum number of iterations for the adaptive loop. If ``None``, the - algorithm is not bound in its number of iterations. - threshold: once all gradients have an absolute value smaller than this threshold, the - algorithm has converged and terminates. Defaults to ``1e-5``. - """ - with warnings.catch_warnings(): - warnings.filterwarnings("ignore", category=DeprecationWarning) - validate_min("gradient_threshold", gradient_threshold, 1e-15) - validate_min("eigenvalue_threshold", eigenvalue_threshold, 1e-15) - - self.solver = solver - self.gradient_threshold = gradient_threshold - self.eigenvalue_threshold = eigenvalue_threshold - self.max_iterations = max_iterations - self._tmp_ansatz: EvolvedOperatorAnsatz | None = None - self._excitation_pool: list[OperatorBase] = [] - self._excitation_list: list[OperatorBase] = [] - - @property - @deprecate_func( - since="0.24.0", - package_name="qiskit-terra", - pending=True, - is_property=True, - additional_msg="Instead, use the gradient_threshold attribute.", - ) - def threshold(self) -> float: - """The threshold for the gradients. - - Once all gradients have an absolute value smaller than this threshold, the algorithm has - converged and terminates. - """ - return self.gradient_threshold - - @threshold.setter - @deprecate_func( - since="0.24.0", - package_name="qiskit-terra", - pending=True, - is_property=True, - additional_msg="Instead, use the gradient_threshold attribute.", - ) - def threshold(self, threshold: float) -> None: - self.gradient_threshold = threshold - - @property - def initial_point(self) -> Sequence[float] | None: - """Returns the initial point of the internal :class:`~.VQE` solver.""" - return self.solver.initial_point - - @initial_point.setter - def initial_point(self, value: Sequence[float] | None) -> None: - """Sets the initial point of the internal :class:`~.VQE` solver.""" - self.solver.initial_point = value - - @classmethod - def supports_aux_operators(cls) -> bool: - return True - - def _compute_gradients( - self, - theta: list[float], - operator: BaseOperator | OperatorBase, - ) -> list[tuple[complex, dict[str, Any]]]: - """ - Computes the gradients for all available excitation operators. - - Args: - theta: List of (up to now) optimal parameters. - operator: operator whose gradient needs to be computed. - Returns: - List of pairs consisting of the computed gradient and excitation operator. - """ - # The excitations operators are applied later as exp(i*theta*excitation). - # For this commutator, we need to explicitly pull in the imaginary phase. - commutators = [1j * (operator @ exc - exc @ operator) for exc in self._excitation_pool] - res = estimate_observables(self.solver.estimator, self.solver.ansatz, commutators, theta) - return res - - @staticmethod - def _check_cyclicity(indices: list[int]) -> bool: - """ - Auxiliary function to check for cycles in the indices of the selected excitations. - - Args: - indices: The list of chosen gradient indices. - - Returns: - Whether repeating sequences of indices have been detected. - """ - cycle_regex = re.compile(r"(\b.+ .+\b)( \b\1\b)+") - # reg-ex explanation: - # 1. (\b.+ .+\b) will match at least two numbers and try to match as many as possible. The - # word boundaries in the beginning and end ensure that now numbers are split into digits. - # 2. the match of this part is placed into capture group 1 - # 3. ( \b\1\b)+ will match a space followed by the contents of capture group 1 (again - # delimited by word boundaries to avoid separation into digits). - # -> this results in any sequence of at least two numbers being detected - match = cycle_regex.search(" ".join(map(str, indices))) - logger.debug("Cycle detected: %s", match) - # Additionally we also need to check whether the last two numbers are identical, because the - # reg-ex above will only find cycles of at least two consecutive numbers. - # It is sufficient to assert that the last two numbers are different due to the iterative - # nature of the algorithm. - return match is not None or (len(indices) > 1 and indices[-2] == indices[-1]) - - def compute_minimum_eigenvalue( - self, - operator: BaseOperator | PauliSumOp, - aux_operators: ListOrDict[BaseOperator | PauliSumOp] | None = None, - ) -> AdaptVQEResult: - """Computes the minimum eigenvalue. - - Args: - operator: Operator whose minimum eigenvalue we want to find. - aux_operators: Additional auxiliary operators to evaluate. - - Raises: - TypeError: If an ansatz other than :class:`~.EvolvedOperatorAnsatz` is provided. - QiskitError: If all evaluated gradients lie below the convergence threshold in the first - iteration of the algorithm. - - Returns: - An :class:`~.AdaptVQEResult` which is a :class:`~.VQEResult` but also but also - includes runtime information about the AdaptVQE algorithm like the number of iterations, - termination criterion, and the final maximum gradient. - """ - if not isinstance(self.solver.ansatz, EvolvedOperatorAnsatz): - raise TypeError("The AdaptVQE ansatz must be of the EvolvedOperatorAnsatz type.") - - # Overwrite the solver's ansatz with the initial state - self._tmp_ansatz = self.solver.ansatz - self._excitation_pool = self._tmp_ansatz.operators - self.solver.ansatz = self._tmp_ansatz.initial_state - - prev_op_indices: list[int] = [] - prev_raw_vqe_result: VQEResult | None = None - raw_vqe_result: VQEResult | None = None - theta: list[float] = [] - max_grad: tuple[complex, dict[str, Any] | None] = (0.0, None) - self._excitation_list = [] - history: list[complex] = [] - iteration = 0 - while self.max_iterations is None or iteration < self.max_iterations: - iteration += 1 - logger.info("--- Iteration #%s ---", str(iteration)) - # compute gradients - logger.debug("Computing gradients") - cur_grads = self._compute_gradients(theta, operator) - # pick maximum gradient - max_grad_index, max_grad = max( - enumerate(cur_grads), key=lambda item: np.abs(item[1][0]) - ) - logger.info( - "Found maximum gradient %s at index %s", - str(np.abs(max_grad[0])), - str(max_grad_index), - ) - # log gradients - if np.abs(max_grad[0]) < self.gradient_threshold: - if iteration == 1: - raise QiskitError( - "All gradients have been evaluated to lie below the convergence threshold " - "during the first iteration of the algorithm. Try to either tighten the " - "convergence threshold or pick a different ansatz." - ) - logger.info( - "AdaptVQE terminated successfully with a final maximum gradient: %s", - str(np.abs(max_grad[0])), - ) - termination_criterion = TerminationCriterion.CONVERGED - break - # store maximum gradient's index for cycle detection - prev_op_indices.append(max_grad_index) - # check indices of picked gradients for cycles - if self._check_cyclicity(prev_op_indices): - logger.info("Alternating sequence found. Finishing.") - logger.info("Final maximum gradient: %s", str(np.abs(max_grad[0]))) - termination_criterion = TerminationCriterion.CYCLICITY - break - # add new excitation to self._ansatz - logger.info( - "Adding new operator to the ansatz: %s", str(self._excitation_pool[max_grad_index]) - ) - self._excitation_list.append(self._excitation_pool[max_grad_index]) - theta.append(0.0) - # setting up the ansatz for the VQE iteration - self._tmp_ansatz.operators = self._excitation_list - self.solver.ansatz = self._tmp_ansatz - self.solver.initial_point = theta - # evaluating the eigenvalue with the internal VQE - prev_raw_vqe_result = raw_vqe_result - raw_vqe_result = self.solver.compute_minimum_eigenvalue(operator) - theta = raw_vqe_result.optimal_point.tolist() - # checking convergence based on the change in eigenvalue - if iteration > 1: - eigenvalue_diff = np.abs(raw_vqe_result.eigenvalue - history[-1]) - if eigenvalue_diff < self.eigenvalue_threshold: - logger.info( - "AdaptVQE terminated successfully with a final change in eigenvalue: %s", - str(eigenvalue_diff), - ) - termination_criterion = TerminationCriterion.CONVERGED - logger.debug( - "Reverting the addition of the last excitation to the ansatz since it " - "resulted in a change of the eigenvalue below the configured threshold." - ) - self._excitation_list.pop() - theta.pop() - self._tmp_ansatz.operators = self._excitation_list - self.solver.ansatz = self._tmp_ansatz - self.solver.initial_point = theta - raw_vqe_result = prev_raw_vqe_result - break - # appending the computed eigenvalue to the tracking history - history.append(raw_vqe_result.eigenvalue) - logger.info("Current eigenvalue: %s", str(raw_vqe_result.eigenvalue)) - else: - # reached maximum number of iterations - termination_criterion = TerminationCriterion.MAXIMUM - logger.info("Maximum number of iterations reached. Finishing.") - logger.info("Final maximum gradient: %s", str(np.abs(max_grad[0]))) - - result = AdaptVQEResult() - result.combine(raw_vqe_result) - result.num_iterations = iteration - result.final_max_gradient = max_grad[0] - result.termination_criterion = termination_criterion - result.eigenvalue_history = history - - # once finished evaluate auxiliary operators if any - if aux_operators is not None: - aux_values = estimate_observables( - self.solver.estimator, self.solver.ansatz, aux_operators, result.optimal_point - ) - result.aux_operators_evaluated = aux_values - - logger.info("The final eigenvalue is: %s", str(result.eigenvalue)) - self.solver.ansatz.operators = self._excitation_pool - return result - - -class AdaptVQEResult(VQEResult): - """AdaptVQE Result.""" - - def __init__(self) -> None: - super().__init__() - self._num_iterations: int | None = None - self._final_max_gradient: float | None = None - self._termination_criterion: str = "" - self._eigenvalue_history: list[float] | None = None - - @property - def num_iterations(self) -> int: - """Returns the number of iterations.""" - return self._num_iterations - - @num_iterations.setter - def num_iterations(self, value: int) -> None: - """Sets the number of iterations.""" - self._num_iterations = value - - @property - def final_max_gradient(self) -> float: - """Returns the final maximum gradient.""" - return self._final_max_gradient - - @final_max_gradient.setter - def final_max_gradient(self, value: float) -> None: - """Sets the final maximum gradient.""" - self._final_max_gradient = value - - @property - def termination_criterion(self) -> str: - """Returns the termination criterion.""" - return self._termination_criterion - - @termination_criterion.setter - def termination_criterion(self, value: str) -> None: - """Sets the termination criterion.""" - self._termination_criterion = value - - @property - def eigenvalue_history(self) -> list[float]: - """Returns the history of computed eigenvalues. - - The history's length matches the number of iterations and includes the final computed value. - """ - return self._eigenvalue_history - - @eigenvalue_history.setter - def eigenvalue_history(self, eigenvalue_history: list[float]) -> None: - """Sets the history of computed eigenvalues.""" - self._eigenvalue_history = eigenvalue_history diff --git a/qiskit/algorithms/minimum_eigensolvers/diagonal_estimator.py b/qiskit/algorithms/minimum_eigensolvers/diagonal_estimator.py deleted file mode 100644 index d695d7d05ab9..000000000000 --- a/qiskit/algorithms/minimum_eigensolvers/diagonal_estimator.py +++ /dev/null @@ -1,203 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2022. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. - -"""Expectation value for a diagonal observable using a sampler primitive.""" - -from __future__ import annotations - -from collections.abc import Callable, Sequence, Mapping -from typing import Any - -from dataclasses import dataclass - -import numpy as np -from qiskit.algorithms.algorithm_job import AlgorithmJob -from qiskit.circuit import QuantumCircuit -from qiskit.primitives import BaseSampler, BaseEstimator, EstimatorResult -from qiskit.primitives.utils import init_observable, _circuit_key -from qiskit.opflow import PauliSumOp -from qiskit.quantum_info import SparsePauliOp -from qiskit.quantum_info.operators.base_operator import BaseOperator - - -@dataclass(frozen=True) -class _DiagonalEstimatorResult(EstimatorResult): - """A result from an expectation of a diagonal observable.""" - - # TODO make each measurement a dataclass rather than a dict - best_measurements: Sequence[Mapping[str, Any]] | None = None - - -class _DiagonalEstimator(BaseEstimator): - """An estimator for diagonal observables.""" - - def __init__( - self, - sampler: BaseSampler, - aggregation: float | Callable[[Sequence[tuple[float, float]]], float] | None = None, - callback: Callable[[Sequence[Mapping[str, Any]]], None] | None = None, - **options, - ) -> None: - r"""Evaluate the expectation of quantum state with respect to a diagonal operator. - - Args: - sampler: The sampler used to evaluate the circuits. - aggregation: The aggregation function to aggregate the measurement outcomes. If a float - this specified the CVaR :math:`\alpha` parameter. - callback: A callback which is given the best measurements of all circuits in each - evaluation. - run_options: Options for the sampler. - - """ - super().__init__(options=options) - self._circuits = [] - self._parameters = [] - self._observables = [] - - self.sampler = sampler - if not callable(aggregation): - aggregation = _get_cvar_aggregation(aggregation) - - self.aggregation = aggregation - self.callback = callback - self._circuit_ids = {} - self._observable_ids = {} - - def _run( - self, - circuits: Sequence[QuantumCircuit], - observables: Sequence[BaseOperator | PauliSumOp], - parameter_values: Sequence[Sequence[float]], - **run_options, - ) -> AlgorithmJob: - circuit_indices = [] - for circuit in circuits: - key = _circuit_key(circuit) - index = self._circuit_ids.get(key) - if index is not None: - circuit_indices.append(index) - else: - circuit_indices.append(len(self._circuits)) - self._circuit_ids[key] = len(self._circuits) - self._circuits.append(circuit) - self._parameters.append(circuit.parameters) - observable_indices = [] - for observable in observables: - index = self._observable_ids.get(id(observable)) - if index is not None: - observable_indices.append(index) - else: - observable_indices.append(len(self._observables)) - self._observable_ids[id(observable)] = len(self._observables) - converted_observable = init_observable(observable) - _check_observable_is_diagonal(converted_observable) # check it's diagonal - self._observables.append(converted_observable) - job = AlgorithmJob( - self._call, circuit_indices, observable_indices, parameter_values, **run_options - ) - job.submit() - return job - - def _call( - self, - circuits: Sequence[int], - observables: Sequence[int], - parameter_values: Sequence[Sequence[float]], - **run_options, - ) -> _DiagonalEstimatorResult: - job = self.sampler.run( - [self._circuits[i] for i in circuits], - parameter_values, - **run_options, - ) - sampler_result = job.result() - samples = sampler_result.quasi_dists - - # a list of dictionaries containing: {state: (measurement probability, value)} - evaluations = [ - { - state: (probability, _evaluate_sparsepauli(state, self._observables[i])) - for state, probability in sampled.items() - } - for i, sampled in zip(observables, samples) - ] - - results = np.array([self.aggregation(evaluated.values()) for evaluated in evaluations]) - - # get the best measurements - best_measurements = [] - num_qubits = self._circuits[0].num_qubits - for evaluated in evaluations: - best_result = min(evaluated.items(), key=lambda x: x[1][1]) - best_measurements.append( - { - "state": best_result[0], - "bitstring": bin(best_result[0])[2:].zfill(num_qubits), - "value": best_result[1][1], - "probability": best_result[1][0], - } - ) - - if self.callback is not None: - self.callback(best_measurements) - - return _DiagonalEstimatorResult( - values=results, metadata=sampler_result.metadata, best_measurements=best_measurements - ) - - -def _get_cvar_aggregation(alpha): - """Get the aggregation function for CVaR with confidence level ``alpha``.""" - if alpha is None: - alpha = 1 - elif not 0 <= alpha <= 1: - raise ValueError(f"alpha must be in [0, 1] but was {alpha}") - - # if alpha is close to 1 we can avoid the sorting - if np.isclose(alpha, 1): - - def aggregate(measurements): - return sum(probability * value for probability, value in measurements) - - else: - - def aggregate(measurements): - # sort by values - sorted_measurements = sorted(measurements, key=lambda x: x[1]) - - accumulated_percent = 0 # once alpha is reached, stop - cvar = 0 - for probability, value in sorted_measurements: - cvar += value * min(probability, alpha - accumulated_percent) - accumulated_percent += probability - if accumulated_percent >= alpha: - break - - return cvar / alpha - - return aggregate - - -_PARITY = np.array([-1 if bin(i).count("1") % 2 else 1 for i in range(256)], dtype=np.complex128) - - -def _evaluate_sparsepauli(state: int, observable: SparsePauliOp) -> complex: - packed_uint8 = np.packbits(observable.paulis.z, axis=1, bitorder="little") - state_bytes = np.frombuffer(state.to_bytes(packed_uint8.shape[1], "little"), dtype=np.uint8) - reduced = np.bitwise_xor.reduce(packed_uint8 & state_bytes, axis=1) - return np.sum(observable.coeffs * _PARITY[reduced]) - - -def _check_observable_is_diagonal(observable: SparsePauliOp) -> None: - is_diagonal = not np.any(observable.paulis.x) - if not is_diagonal: - raise ValueError("The observable must be diagonal.") diff --git a/qiskit/algorithms/minimum_eigensolvers/minimum_eigensolver.py b/qiskit/algorithms/minimum_eigensolvers/minimum_eigensolver.py deleted file mode 100644 index 26087c053aef..000000000000 --- a/qiskit/algorithms/minimum_eigensolvers/minimum_eigensolver.py +++ /dev/null @@ -1,97 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2022. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. - -"""The minimum eigensolver interface and result.""" - -from __future__ import annotations - -from abc import ABC, abstractmethod -from typing import Any - -from qiskit.opflow import PauliSumOp -from qiskit.quantum_info.operators.base_operator import BaseOperator - -from ..algorithm_result import AlgorithmResult -from ..list_or_dict import ListOrDict - - -class MinimumEigensolver(ABC): - """The minimum eigensolver interface. - - Algorithms that can compute a minimum eigenvalue for an operator may implement this interface to - allow different algorithms to be used interchangeably. - """ - - @abstractmethod - def compute_minimum_eigenvalue( - self, - operator: BaseOperator | PauliSumOp, - aux_operators: ListOrDict[BaseOperator | PauliSumOp] | None = None, - ) -> "MinimumEigensolverResult": - """ - Computes the minimum eigenvalue. The ``operator`` and ``aux_operators`` are supplied here. - While an ``operator`` is required by algorithms, ``aux_operators`` are optional. - - Args: - operator: Qubit operator of the observable. - aux_operators: Optional list of auxiliary operators to be evaluated with the - parameters of the minimum eigenvalue main result and their expectation values - returned. For instance in chemistry these can be dipole operators and total particle - count operators, so we can get values for these at the ground state. - - Returns: - A minimum eigensolver result. - """ - return MinimumEigensolverResult() - - @classmethod - def supports_aux_operators(cls) -> bool: - """Whether computing the expectation value of auxiliary operators is supported. - - If the minimum eigensolver computes an eigenvalue of the main ``operator`` then it can - compute the expectation value of the ``aux_operators`` for that state. Otherwise they will - be ignored. - - Returns: - True if aux_operator expectations can be evaluated, False otherwise - """ - return False - - -class MinimumEigensolverResult(AlgorithmResult): - """Minimum eigensolver result.""" - - def __init__(self) -> None: - super().__init__() - self._eigenvalue: complex | None = None - self._aux_operators_evaluated: ListOrDict[tuple[complex, dict[str, Any]]] | None = None - - @property - def eigenvalue(self) -> complex | None: - """The computed minimum eigenvalue.""" - return self._eigenvalue - - @eigenvalue.setter - def eigenvalue(self, value: complex) -> None: - self._eigenvalue = value - - @property - def aux_operators_evaluated(self) -> ListOrDict[tuple[complex, dict[str, Any]]] | None: - """The aux operator expectation values. - - These values are in fact tuples formatted as (mean, (variance, shots)). - """ - return self._aux_operators_evaluated - - @aux_operators_evaluated.setter - def aux_operators_evaluated(self, value: ListOrDict[tuple[complex, dict[str, Any]]]) -> None: - self._aux_operators_evaluated = value diff --git a/qiskit/algorithms/minimum_eigensolvers/numpy_minimum_eigensolver.py b/qiskit/algorithms/minimum_eigensolvers/numpy_minimum_eigensolver.py deleted file mode 100644 index 93dc328b282c..000000000000 --- a/qiskit/algorithms/minimum_eigensolvers/numpy_minimum_eigensolver.py +++ /dev/null @@ -1,106 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2022. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. - -"""The NumPy minimum eigensolver algorithm and result.""" - -from __future__ import annotations - -from typing import Callable, List, Union, Optional -import logging -import numpy as np - -from qiskit.opflow import PauliSumOp -from qiskit.quantum_info import Statevector -from qiskit.quantum_info.operators.base_operator import BaseOperator - -from ..eigensolvers.numpy_eigensolver import NumPyEigensolver -from .minimum_eigensolver import MinimumEigensolver, MinimumEigensolverResult -from ..list_or_dict import ListOrDict - -logger = logging.getLogger(__name__) - -# future type annotations not supported in type aliases in 3.8 -FilterType = Callable[[Union[List, np.ndarray], float, Optional[ListOrDict[float]]], bool] - - -class NumPyMinimumEigensolver(MinimumEigensolver): - """ - The NumPy minimum eigensolver algorithm. - """ - - def __init__( - self, - filter_criterion: FilterType | None = None, - ) -> None: - """ - Args: - filter_criterion: Callable that allows to filter eigenvalues/eigenstates. The minimum - eigensolver is only searching over feasible states and returns an eigenstate that - has the smallest eigenvalue among feasible states. The callable has the signature - ``filter(eigenstate, eigenvalue, aux_values)`` and must return a boolean to indicate - whether to consider this value or not. If there is no feasible element, the result - can even be empty. - """ - self._eigensolver = NumPyEigensolver(filter_criterion=filter_criterion) - - @property - def filter_criterion( - self, - ) -> FilterType | None: - """Returns the criterion for filtering eigenstates/eigenvalues.""" - return self._eigensolver.filter_criterion - - @filter_criterion.setter - def filter_criterion( - self, - filter_criterion: FilterType, - ) -> None: - self._eigensolver.filter_criterion = filter_criterion - - @classmethod - def supports_aux_operators(cls) -> bool: - return NumPyEigensolver.supports_aux_operators() - - def compute_minimum_eigenvalue( - self, - operator: BaseOperator | PauliSumOp, - aux_operators: ListOrDict[BaseOperator | PauliSumOp] | None = None, - ) -> NumPyMinimumEigensolverResult: - super().compute_minimum_eigenvalue(operator, aux_operators) - eigensolver_result = self._eigensolver.compute_eigenvalues(operator, aux_operators) - result = NumPyMinimumEigensolverResult() - if eigensolver_result.eigenvalues is not None and len(eigensolver_result.eigenvalues) > 0: - result.eigenvalue = eigensolver_result.eigenvalues[0] - result.eigenstate = eigensolver_result.eigenstates[0] - if eigensolver_result.aux_operators_evaluated: - result.aux_operators_evaluated = eigensolver_result.aux_operators_evaluated[0] - - logger.debug("NumPy minimum eigensolver result: %s", result) - - return result - - -class NumPyMinimumEigensolverResult(MinimumEigensolverResult): - """NumPy minimum eigensolver result.""" - - def __init__(self) -> None: - super().__init__() - self._eigenstate: Statevector | None = None - - @property - def eigenstate(self) -> Statevector | None: - """Returns the eigenstate corresponding to the computed minimum eigenvalue.""" - return self._eigenstate - - @eigenstate.setter - def eigenstate(self, value: Statevector) -> None: - self._eigenstate = value diff --git a/qiskit/algorithms/minimum_eigensolvers/qaoa.py b/qiskit/algorithms/minimum_eigensolvers/qaoa.py deleted file mode 100644 index 825d4fa64cc7..000000000000 --- a/qiskit/algorithms/minimum_eigensolvers/qaoa.py +++ /dev/null @@ -1,144 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2022. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. - -"""The quantum approximate optimization algorithm.""" - -from __future__ import annotations - -import warnings -from typing import Callable, Any -import numpy as np - -from qiskit.algorithms.optimizers import Minimizer, Optimizer -from qiskit.circuit import QuantumCircuit -from qiskit.circuit.library.n_local.qaoa_ansatz import QAOAAnsatz -from qiskit.quantum_info.operators.base_operator import BaseOperator -from qiskit.opflow import PauliSumOp -from qiskit.primitives import BaseSampler -from qiskit.utils.validation import validate_min - -from .sampling_vqe import SamplingVQE - - -class QAOA(SamplingVQE): - r""" - The Quantum Approximate Optimization Algorithm (QAOA). - - QAOA is a well-known algorithm for finding approximate solutions to combinatorial-optimization - problems [1]. - - The QAOA implementation directly extends :class:`.SamplingVQE` and inherits its optimization - structure. However, unlike VQE, which can be configured with arbitrary ansatzes, QAOA uses its - own fine-tuned ansatz, which comprises :math:`p` parameterized global :math:`x` rotations and - :math:`p` different parameterizations of the problem hamiltonian. QAOA is thus principally - configured by the single integer parameter, ``reps``, which dictates the depth of the ansatz, - and thus affects the approximation quality. - - An optional array of :math:`2p` parameter values, as the :attr:`initial_point`, may be provided - as the starting :math:`\beta` and :math:`\gamma` parameters for the QAOA ansatz [1]. - - An operator or a parameterized quantum circuit may optionally also be provided as a custom - :attr:`mixer` Hamiltonian. This allows in the case of quantum annealing [2] and QAOA [3], to run - constrained optimization problems where the mixer constrains the evolution to a feasible - subspace of the full Hilbert space. - - The following attributes can be set via the initializer but can also be read and updated once - the QAOA object has been constructed. - - Attributes: - sampler (BaseSampler): The sampler primitive to sample the circuits. - 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. - reps (int): The integer parameter :math:`p`. Has a minimum valid value of 1. - initial_state: An optional initial state to prepend the QAOA circuit with. - mixer (QuantumCircuit | BaseOperator | PauliSumOp): The mixer Hamiltonian to evolve with or - a custom quantum circuit. Allows support of optimizations in constrained subspaces [2, - 3] as well as warm-starting the optimization [4]. - aggregation (float | Callable[[list[float]], float] | None): A float or callable to specify - how the objective function evaluated on the basis states should be aggregated. If a - float, this specifies the :math:`\alpha \in [0,1]` parameter for a CVaR expectation - value. - 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 value, the - the metadata dictionary, and the best measurement. - - References: - [1]: Farhi, E., Goldstone, J., Gutmann, S., "A Quantum Approximate Optimization Algorithm" - `arXiv:1411.4028 `__ - [2]: Hen, I., Spedalieri, F. M., "Quantum Annealing for Constrained Optimization" - `PhysRevApplied.5.034007 `__ - [3]: Hadfield, S. et al, "From the Quantum Approximate Optimization Algorithm to a Quantum - Alternating Operator Ansatz" `arXiv:1709.03489 `__ - [4]: Egger, D. J., Marecek, J., Woerner, S., "Warm-starting quantum optimization" - `arXiv: 2009.10095 `__ - """ - - def __init__( - self, - sampler: BaseSampler, - optimizer: Optimizer | Minimizer, - *, - reps: int = 1, - initial_state: QuantumCircuit | None = None, - mixer: QuantumCircuit | BaseOperator | PauliSumOp = None, - initial_point: np.ndarray | None = None, - aggregation: float | Callable[[list[float]], float] | None = None, - callback: Callable[[int, np.ndarray, float, dict[str, Any]], None] | None = None, - ) -> None: - r""" - Args: - sampler: The sampler primitive to sample the circuits. - 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. - reps: The integer parameter :math:`p`. Has a minimum valid value of 1. - initial_state: An optional initial state to prepend the QAOA circuit with. - mixer: The mixer Hamiltonian to evolve with or a custom quantum circuit. Allows support - of optimizations in constrained subspaces [2, 3] as well as warm-starting the - optimization [4]. - 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. ``QAOA`` 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. - aggregation: A float or callable to specify how the objective function evaluated on the - basis states should be aggregated. If a float, this specifies the :math:`\alpha \in - [0,1]` parameter for a CVaR expectation value. - callback: A callback that can access the intermediate data at each optimization step. - These data are: the evaluation count, the optimizer parameters for the ansatz, the - evaluated value, the metadata dictionary. - """ - with warnings.catch_warnings(): - warnings.filterwarnings("ignore", category=DeprecationWarning) - validate_min("reps", reps, 1) - - self.reps = reps - self.mixer = mixer - self.initial_state = initial_state - self._cost_operator = None - - super().__init__( - sampler=sampler, - ansatz=None, - optimizer=optimizer, - initial_point=initial_point, - aggregation=aggregation, - callback=callback, - ) - - def _check_operator_ansatz(self, operator: BaseOperator | PauliSumOp): - # Recreates a circuit based on operator parameter. - self.ansatz = QAOAAnsatz( - operator, self.reps, initial_state=self.initial_state, mixer_operator=self.mixer - ).decompose() # TODO remove decompose once #6674 is fixed diff --git a/qiskit/algorithms/minimum_eigensolvers/sampling_mes.py b/qiskit/algorithms/minimum_eigensolvers/sampling_mes.py deleted file mode 100644 index e193f53ce15c..000000000000 --- a/qiskit/algorithms/minimum_eigensolvers/sampling_mes.py +++ /dev/null @@ -1,138 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2022. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. - -"""The Sampling Minimum Eigensolver interface.""" - -from __future__ import annotations -from abc import ABC, abstractmethod -from collections.abc import Mapping -from typing import Any - -from qiskit.quantum_info.operators.base_operator import BaseOperator -from qiskit.opflow import PauliSumOp -from qiskit.result import QuasiDistribution -from ..algorithm_result import AlgorithmResult -from ..list_or_dict import ListOrDict - - -class SamplingMinimumEigensolver(ABC): - """The Sampling Minimum Eigensolver Interface.""" - - @abstractmethod - def compute_minimum_eigenvalue( - self, - operator: BaseOperator | PauliSumOp, - aux_operators: ListOrDict[BaseOperator | PauliSumOp] | None = None, - ) -> "SamplingMinimumEigensolverResult": - """Compute the minimum eigenvalue of a diagonal operator. - - Args: - operator: Diagonal qubit operator. - aux_operators: Optional list of auxiliary operators to be evaluated with the - final state. - - Returns: - A :class:`~.SamplingMinimumEigensolverResult` containing the optimization result. - """ - pass - - @classmethod - def supports_aux_operators(cls) -> bool: - """Whether computing the expectation value of auxiliary operators is supported. - - If the minimum eigensolver computes an eigenstate of the main operator then it - can compute the expectation value of the aux_operators for that state. Otherwise - they will be ignored. - - Returns: - True if aux_operator expectations can be evaluated, False otherwise - """ - return False - - -class SamplingMinimumEigensolverResult(AlgorithmResult): - """Sampling Minimum Eigensolver Result. - - In contrast to the result of a :class:`~.MinimumEigenSolver`, this result also contains - the best measurement of the overall optimization and the samples of the final state. - """ - - def __init__(self) -> None: - super().__init__() - self._eigenvalue: complex | None = None - self._eigenstate: QuasiDistribution | None = None - self._aux_operator_values: ListOrDict[tuple[complex, dict[str, Any]]] | None = None - self._best_measurement: Mapping[str, Any] | None = None - - @property - def eigenvalue(self) -> complex | None: - """Return the approximation to the eigenvalue.""" - return self._eigenvalue - - @eigenvalue.setter - def eigenvalue(self, value: complex | None) -> None: - """Set the approximation to the eigenvalue.""" - self._eigenvalue = value - - @property - def eigenstate(self) -> QuasiDistribution | None: - """Return the quasi-distribution sampled from the final state. - - The ansatz is sampled when parameterized with the optimal parameters that where obtained - computing the minimum eigenvalue. The keys represent a measured classical value and the - value is a float for the quasi-probability of that result. - """ - return self._eigenstate - - @eigenstate.setter - def eigenstate(self, value: QuasiDistribution | None) -> None: - """Set the quasi-distribution sampled from the final state.""" - self._eigenstate = value - - @property - def aux_operators_evaluated(self) -> ListOrDict[tuple[complex, dict[str, Any]]] | None: - """Return aux operator expectation values and metadata. - - These are formatted as (mean, metadata). - """ - return self._aux_operator_values - - @aux_operators_evaluated.setter - def aux_operators_evaluated( - self, value: ListOrDict[tuple[complex, dict[str, Any]]] | None - ) -> None: - self._aux_operator_values = value - - @property - def best_measurement(self) -> Mapping[str, Any] | None: - """Return the best measurement over the entire optimization. - - Possesses keys: ``state``, ``bitstring``, ``value``, ``probability``. - """ - return self._best_measurement - - @best_measurement.setter - def best_measurement(self, value: Mapping[str, Any]) -> None: - """Set the best measurement over the entire optimization.""" - self._best_measurement = value - - def __str__(self) -> str: - """Return a string representation of the result.""" - disp = ( - "SamplingMinimumEigensolverResult:\n" - + f"\tEigenvalue: {self.eigenvalue}\n" - + f"\tBest measurement\n: {self.best_measurement}\n" - ) - if self.aux_operators_evaluated is not None: - disp += f"\n\tAuxiliary operator values: {self.aux_operators_evaluated}\n" - - return disp diff --git a/qiskit/algorithms/minimum_eigensolvers/sampling_vqe.py b/qiskit/algorithms/minimum_eigensolvers/sampling_vqe.py deleted file mode 100644 index 2fb60355b2a5..000000000000 --- a/qiskit/algorithms/minimum_eigensolvers/sampling_vqe.py +++ /dev/null @@ -1,382 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2022. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. - -"""The Variational Quantum Eigensolver algorithm, optimized for diagonal Hamiltonians.""" - -from __future__ import annotations - -from collections.abc import Callable, Sequence -import logging -from time import time -from typing import Any - -import numpy as np - -from qiskit.circuit import QuantumCircuit -from qiskit.opflow import PauliSumOp -from qiskit.primitives import BaseSampler -from qiskit.result import QuasiDistribution -from qiskit.quantum_info.operators.base_operator import BaseOperator - -from ..exceptions import AlgorithmError -from ..list_or_dict import ListOrDict -from ..optimizers import Minimizer, Optimizer, OptimizerResult -from ..variational_algorithm import VariationalAlgorithm, VariationalResult -from .diagonal_estimator import _DiagonalEstimator -from .sampling_mes import ( - SamplingMinimumEigensolver, - SamplingMinimumEigensolverResult, -) -from ..observables_evaluator import estimate_observables -from ..utils import validate_initial_point, validate_bounds - -# private function as we expect this to be updated in the next released -from ..utils.set_batching import _set_default_batchsize - - -logger = logging.getLogger(__name__) - - -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 - ``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: - - .. code-block:: python - - from qiskit.algorithms.optimizers import OptimizerResult - - def my_minimizer(fun, x0, jac=None, bounds=None) -> OptimizerResult: - # Note that the callable *must* have these argument names! - # Args: - # fun (callable): the function to minimize - # x0 (np.ndarray): the initial point for the optimization - # jac (callable, optional): the gradient of the objective function - # bounds (list, optional): a list of tuples specifying the parameter bounds - - result = OptimizerResult() - result.x = # optimal parameters - result.fun = # optimal function value - return result - - The above signature also allows 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 ``SamplingVQE`` object has been constructed. - - Attributes: - sampler (BaseSampler): The sampler primitive to sample the circuits. - 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. - aggregation (float | Callable[[list[tuple[float, complex]], float] | None): - A float or callable to specify how the objective function evaluated on the basis states - should be aggregated. If a float, this specifies the :math:`\alpha \in [0,1]` parameter - for a CVaR expectation value [1]. If a callable, it takes a list of basis state - measurements specified as ``[(probability, objective_value)]`` and return an objective - value as float. If None, all an ordinary expectation value is calculated. - 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 value, and the - metadata dictionary. - - References: - [1]: Barkoutsos, P. K., Nannicini, G., Robert, A., Tavernelli, I., and Woerner, S., - "Improving Variational Quantum Optimization using CVaR" - `arXiv:1907.04769 `_ - """ - - def __init__( - self, - sampler: BaseSampler, - ansatz: QuantumCircuit, - optimizer: Optimizer | Minimizer, - *, - initial_point: Sequence[float] | None = None, - aggregation: float | Callable[[list[float]], float] | None = None, - callback: Callable[[int, np.ndarray, float, dict[str, Any]], None] | None = None, - ) -> None: - r""" - Args: - sampler: The sampler primitive to sample the circuits. - 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. - 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. ``SamplingVQE`` 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. - aggregation: A float or callable to specify how the objective function evaluated on the - basis states should be aggregated. - 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.sampler = sampler - self.ansatz = ansatz - self.optimizer = optimizer - self.aggregation = aggregation - self.callback = callback - - # this has to go via getters and setters due to the VariationalAlgorithm interface - self._initial_point = initial_point - - @property - def initial_point(self) -> Sequence[float] | None: - """Return the initial point.""" - return self._initial_point - - @initial_point.setter - def initial_point(self, value: Sequence[float] | None) -> None: - """Set the initial point.""" - self._initial_point = value - - def _check_operator_ansatz(self, operator: BaseOperator | PauliSumOp): - """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.") - - @classmethod - def supports_aux_operators(cls) -> bool: - return True - - def compute_minimum_eigenvalue( - self, - operator: BaseOperator | PauliSumOp, - aux_operators: ListOrDict[BaseOperator | PauliSumOp] | None = None, - ) -> SamplingMinimumEigensolverResult: - # check that the number of qubits of operator and ansatz match, and resize if possible - self._check_operator_ansatz(operator) - - if len(self.ansatz.clbits) > 0: - self.ansatz.remove_final_measurements() - self.ansatz.measure_all() - - initial_point = validate_initial_point(self.initial_point, self.ansatz) - - bounds = validate_bounds(self.ansatz) - - evaluate_energy, best_measurement = self._get_evaluate_energy( - operator, self.ansatz, return_best_measurement=True - ) - - start_time = time() - - if callable(self.optimizer): - optimizer_result = self.optimizer(fun=evaluate_energy, x0=initial_point, 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, x0=initial_point, 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 opt_params %s.", - optimizer_time, - optimizer_result.x, - ) - - final_state = self.sampler.run([self.ansatz], [optimizer_result.x]).result().quasi_dists[0] - - if aux_operators is not None: - aux_operators_evaluated = estimate_observables( - _DiagonalEstimator(sampler=self.sampler), - self.ansatz, - aux_operators, - optimizer_result.x, - ) - else: - aux_operators_evaluated = None - - return self._build_sampling_vqe_result( - self.ansatz.copy(), - optimizer_result, - aux_operators_evaluated, - best_measurement, - final_state, - optimizer_time, - ) - - def _get_evaluate_energy( - self, - operator: BaseOperator | PauliSumOp, - ansatz: QuantumCircuit, - return_best_measurement: bool = False, - ) -> Callable[[np.ndarray], np.ndarray | float] | tuple[ - Callable[[np.ndarray], np.ndarray | float], dict[str, Any] - ]: - """Returns a function handle to evaluate the energy at given parameters. - - This is the objective function to be passed to the optimizer that is used for evaluation. - - Args: - operator: The operator whose energy to evaluate. - ansatz: The ansatz preparing the quantum state. - return_best_measurement: If True, a handle to a dictionary containing the best - measurement evaluated with the cost function. - - Returns: - A tuple of a callable evaluating the energy and (optionally) a dictionary containing the - best measurement of the energy evaluation. - - Raises: - AlgorithmError: If the circuit is not parameterized (i.e. has 0 free parameters). - - """ - num_parameters = ansatz.num_parameters - if num_parameters == 0: - raise AlgorithmError("The ansatz must be parameterized, but has 0 free parameters.") - - # avoid creating an instance variable to remain stateless regarding results - eval_count = 0 - - best_measurement = {"best": None} - - def store_best_measurement(best): - for best_i in best: - if best_measurement["best"] is None or _compare_measurements( - best_i, best_measurement["best"] - ): - best_measurement["best"] = best_i - - estimator = _DiagonalEstimator( - sampler=self.sampler, callback=store_best_measurement, aggregation=self.aggregation - ) - - 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) - - estimator_result = estimator.run( - batch_size * [ansatz], batch_size * [operator], parameters - ).result() - 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) - - result = values if len(values) > 1 else values[0] - return np.real(result) - - if return_best_measurement: - return evaluate_energy, best_measurement - - return evaluate_energy - - def _build_sampling_vqe_result( - self, - ansatz: QuantumCircuit, - optimizer_result: OptimizerResult, - aux_operators_evaluated: ListOrDict[tuple[complex, tuple[complex, int]]], - best_measurement: dict[str, Any], - final_state: QuasiDistribution, - optimizer_time: float, - ) -> SamplingVQEResult: - result = SamplingVQEResult() - result.eigenvalue = optimizer_result.fun - result.cost_function_evals = optimizer_result.nfev - result.optimal_point = optimizer_result.x - result.optimal_parameters = dict(zip(self.ansatz.parameters, optimizer_result.x)) - result.optimal_value = optimizer_result.fun - result.optimizer_time = optimizer_time - result.aux_operators_evaluated = aux_operators_evaluated - result.optimizer_result = optimizer_result - result.best_measurement = best_measurement["best"] - result.eigenstate = final_state - result.optimal_circuit = ansatz - return result - - -class SamplingVQEResult(VariationalResult, SamplingMinimumEigensolverResult): - """VQE Result.""" - - def __init__(self) -> None: - super().__init__() - self._cost_function_evals: int | None = None - - @property - def cost_function_evals(self) -> int | None: - """Returns number of cost optimizer evaluations""" - return self._cost_function_evals - - @cost_function_evals.setter - def cost_function_evals(self, value: int) -> None: - """Sets number of cost function evaluations""" - self._cost_function_evals = value - - -def _compare_measurements(candidate, current_best): - """Compare two best measurements. Returns True if the candidate is better than current value. - - This compares the following two criteria, in this precedence: - - 1. The smaller objective value is better - 2. The higher probability for the objective value is better - - """ - if candidate["value"] < current_best["value"]: - return True - elif candidate["value"] == current_best["value"]: - return candidate["probability"] > current_best["probability"] - return False diff --git a/qiskit/algorithms/minimum_eigensolvers/vqe.py b/qiskit/algorithms/minimum_eigensolvers/vqe.py deleted file mode 100644 index f26d3687971c..000000000000 --- a/qiskit/algorithms/minimum_eigensolvers/vqe.py +++ /dev/null @@ -1,356 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2022. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. - -"""The variational quantum eigensolver algorithm.""" - -from __future__ import annotations - -import logging -from time import time -from collections.abc import Callable, Sequence -from typing import Any - -import numpy as np - -from qiskit.algorithms.gradients import BaseEstimatorGradient -from qiskit.circuit import QuantumCircuit -from qiskit.opflow import PauliSumOp -from qiskit.primitives import BaseEstimator -from qiskit.quantum_info.operators.base_operator import BaseOperator - -from ..exceptions import AlgorithmError -from ..list_or_dict import ListOrDict -from ..optimizers import Optimizer, Minimizer, OptimizerResult -from ..variational_algorithm import VariationalAlgorithm, VariationalResult -from .minimum_eigensolver import MinimumEigensolver, MinimumEigensolverResult -from ..observables_evaluator import estimate_observables -from ..utils import validate_initial_point, validate_bounds - -# private function as we expect this to be updated in the next released -from ..utils.set_batching import _set_default_batchsize - -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.algorithms.optimizers.SPSA` or a callable with the following signature: - - .. code-block:: python - - from qiskit.algorithms.optimizers import OptimizerResult - - def my_minimizer(fun, x0, jac=None, bounds=None) -> OptimizerResult: - # Note that the callable *must* have these argument names! - # Args: - # fun (callable): the function to minimize - # x0 (np.ndarray): the initial point for the optimization - # jac (callable, optional): the gradient of the objective function - # bounds (list, optional): a list of tuples specifying the parameter bounds - - result = OptimizerResult() - result.x = # optimal parameters - result.fun = # optimal function value - return result - - The above signature also allows 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: BaseEstimatorGradient | None = None, - initial_point: Sequence[float] | 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) -> Sequence[float] | None: - return self._initial_point - - @initial_point.setter - def initial_point(self, value: Sequence[float] | None) -> None: - self._initial_point = value - - def compute_minimum_eigenvalue( - self, - operator: BaseOperator | PauliSumOp, - aux_operators: ListOrDict[BaseOperator | PauliSumOp] | 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, 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, x0=initial_point, jac=evaluate_gradient, 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 - ) - else: - aux_operators_evaluated = None - - return self._build_vqe_result( - self.ansatz, optimizer_result, aux_operators_evaluated, optimizer_time - ) - - @classmethod - def supports_aux_operators(cls) -> bool: - return True - - def _get_evaluate_energy( - self, - ansatz: QuantumCircuit, - operator: BaseOperator | PauliSumOp, - ) -> 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 | PauliSumOp, - ) -> 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 | PauliSumOp): - """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 - result.optimal_parameters = dict(zip(self.ansatz.parameters, optimizer_result.x)) - result.optimal_value = optimizer_result.fun - result.optimizer_time = optimizer_time - result.aux_operators_evaluated = aux_operators_evaluated - result.optimizer_result = optimizer_result - return result - - -class VQEResult(VariationalResult, MinimumEigensolverResult): - """Variational quantum eigensolver 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/algorithms/observables_evaluator.py b/qiskit/algorithms/observables_evaluator.py deleted file mode 100644 index 6d40239e229e..000000000000 --- a/qiskit/algorithms/observables_evaluator.py +++ /dev/null @@ -1,126 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2021, 2022. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. - -"""Evaluator of observables for algorithms.""" - -from __future__ import annotations -from collections.abc import Sequence -from typing import Any - -import numpy as np - -from qiskit import QuantumCircuit -from qiskit.opflow import PauliSumOp -from qiskit.quantum_info import SparsePauliOp -from .exceptions import AlgorithmError -from .list_or_dict import ListOrDict -from ..primitives import BaseEstimator -from ..quantum_info.operators.base_operator import BaseOperator - - -def estimate_observables( - estimator: BaseEstimator, - quantum_state: QuantumCircuit, - observables: ListOrDict[BaseOperator | PauliSumOp], - parameter_values: Sequence[float] | None = None, - threshold: float = 1e-12, -) -> ListOrDict[tuple[complex, dict[str, Any]]]: - """ - Accepts a sequence of operators and calculates their expectation values - means - and metadata. They are calculated with respect to a quantum state provided. A user - can optionally provide a threshold value which filters mean values falling below the threshold. - - Args: - estimator: An estimator primitive used for calculations. - quantum_state: A (parameterized) quantum circuit preparing a quantum state that expectation - values are computed against. - observables: A list or a dictionary of operators whose expectation values are to be - calculated. - parameter_values: Optional list of parameters values to evaluate the quantum circuit on. - threshold: A threshold value that defines which mean values should be neglected (helpful for - ignoring numerical instabilities close to 0). - - Returns: - A list or a dictionary of tuples (mean, metadata). - - Raises: - AlgorithmError: If a primitive job is not successful. - """ - - if isinstance(observables, dict): - observables_list = list(observables.values()) - else: - observables_list = observables - - if len(observables_list) > 0: - observables_list = _handle_zero_ops(observables_list) - quantum_state = [quantum_state] * len(observables) - if parameter_values is not None: - parameter_values = [parameter_values] * len(observables) - try: - estimator_job = estimator.run(quantum_state, observables_list, parameter_values) - expectation_values = estimator_job.result().values - except Exception as exc: - raise AlgorithmError("The primitive job failed!") from exc - - metadata = estimator_job.result().metadata - # Discard values below threshold - observables_means = expectation_values * (np.abs(expectation_values) > threshold) - # zip means and metadata into tuples - observables_results = list(zip(observables_means, metadata)) - else: - observables_results = [] - - return _prepare_result(observables_results, observables) - - -def _handle_zero_ops( - observables_list: list[BaseOperator | PauliSumOp], -) -> list[BaseOperator | PauliSumOp]: - """Replaces all occurrence of operators equal to 0 in the list with an equivalent ``PauliSumOp`` - operator.""" - if observables_list: - zero_op = SparsePauliOp.from_list([("I" * observables_list[0].num_qubits, 0)]) - for ind, observable in enumerate(observables_list): - if observable == 0: - observables_list[ind] = zero_op - return observables_list - - -def _prepare_result( - observables_results: list[tuple[complex, dict]], - observables: ListOrDict[BaseOperator | PauliSumOp], -) -> ListOrDict[tuple[complex, dict[str, Any]]]: - """ - Prepares a list of tuples of eigenvalues and metadata tuples from - ``observables_results`` and ``observables``. - - Args: - observables_results: A list of tuples (mean, metadata). - observables: A list or a dictionary of operators whose expectation values are to be - calculated. - - Returns: - A list or a dictionary of tuples (mean, metadata). - """ - - if isinstance(observables, list): - # by construction, all None values will be overwritten - observables_eigenvalues: ListOrDict[tuple[complex, complex]] = [None] * len(observables) - key_value_iterator = enumerate(observables_results) - else: - observables_eigenvalues = {} - key_value_iterator = zip(observables.keys(), observables_results) - - for key, value in key_value_iterator: - observables_eigenvalues[key] = value - return observables_eigenvalues diff --git a/qiskit/algorithms/optimizers/__init__.py b/qiskit/algorithms/optimizers/__init__.py deleted file mode 100644 index 11bf73d1fcaf..000000000000 --- a/qiskit/algorithms/optimizers/__init__.py +++ /dev/null @@ -1,182 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2018, 2022. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. - -""" -Optimizers (:mod:`qiskit.algorithms.optimizers`) -===================================================== -It contains a variety of classical optimizers for use by quantum variational algorithms, -such as :class:`~qiskit.algorithms.VQE`. -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 - -Optimizer Base Class -==================== - -.. autosummary:: - :toctree: ../stubs/ - :nosignatures: - - OptimizerResult - OptimizerSupportLevel - Optimizer - Minimizer - -Steppable Optimizer Base Class -============================== - -.. autosummary:: - :toctree: ../stubs/ - - optimizer_utils - -.. autosummary:: - :toctree: ../stubs/ - :nosignatures: - - SteppableOptimizer - AskData - TellData - OptimizerState - - - -Local Optimizers -================ - -.. autosummary:: - :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 -the `scikit-quant` package. 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 so it has been made an optional install for the end user to choose whether to do so or -not. 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 their dependent NLopt package is manually installed. - -.. autosummary:: - :toctree: ../stubs/ - :nosignatures: - - CRS - DIRECT_L - DIRECT_L_RAND - ESCH - ISRES - -""" - -from .adam_amsgrad import ADAM -from .aqgd import AQGD -from .bobyqa import BOBYQA -from .cg import CG -from .cobyla import COBYLA -from .gsls import GSLS -from .gradient_descent import GradientDescent, GradientDescentState -from .imfil import IMFIL -from .l_bfgs_b import L_BFGS_B -from .nelder_mead import NELDER_MEAD -from .nft import NFT -from .nlopts.crs import CRS -from .nlopts.direct_l import DIRECT_L -from .nlopts.direct_l_rand import DIRECT_L_RAND -from .nlopts.esch import ESCH -from .nlopts.isres import ISRES -from .steppable_optimizer import SteppableOptimizer, AskData, TellData, OptimizerState -from .optimizer import Minimizer, Optimizer, OptimizerResult, OptimizerSupportLevel -from .p_bfgs import P_BFGS -from .powell import POWELL -from .qnspsa import QNSPSA -from .scipy_optimizer import SciPyOptimizer -from .slsqp import SLSQP -from .snobfit import SNOBFIT -from .spsa import SPSA -from .tnc import TNC -from .umda import UMDA - -__all__ = [ - "Optimizer", - "OptimizerSupportLevel", - "SteppableOptimizer", - "AskData", - "TellData", - "OptimizerState", - "OptimizerResult", - "Minimizer", - "ADAM", - "AQGD", - "CG", - "COBYLA", - "GSLS", - "GradientDescent", - "GradientDescentState", - "L_BFGS_B", - "NELDER_MEAD", - "NFT", - "P_BFGS", - "POWELL", - "SciPyOptimizer", - "SLSQP", - "SPSA", - "QNSPSA", - "TNC", - "CRS", - "DIRECT_L", - "DIRECT_L_RAND", - "ESCH", - "ISRES", - "SNOBFIT", - "BOBYQA", - "IMFIL", - "UMDA", -] diff --git a/qiskit/algorithms/optimizers/adam_amsgrad.py b/qiskit/algorithms/optimizers/adam_amsgrad.py deleted file mode 100644 index 4df4adf918ef..000000000000 --- a/qiskit/algorithms/optimizers/adam_amsgrad.py +++ /dev/null @@ -1,274 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2019, 2021. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. - -"""The Adam and AMSGRAD optimizers.""" -from __future__ import annotations - -from collections.abc import Callable -from typing import Any -import os - -import csv -import numpy as np -from qiskit.utils.deprecation import deprecate_arg -from .optimizer import Optimizer, OptimizerSupportLevel, OptimizerResult, POINT - -# pylint: disable=invalid-name - - -class ADAM(Optimizer): - """Adam and AMSGRAD optimizers. - - Adam [1] is a gradient-based optimization algorithm that is relies on adaptive estimates of - lower-order moments. The algorithm requires little memory and is invariant to diagonal - rescaling of the gradients. Furthermore, it is able to cope with non-stationary objective - functions and noisy and/or sparse gradients. - - AMSGRAD [2] (a variant of Adam) uses a 'long-term memory' of past gradients and, thereby, - improves convergence properties. - - References: - - [1]: Kingma, Diederik & Ba, Jimmy (2014), Adam: A Method for Stochastic Optimization. - `arXiv:1412.6980 `_ - - [2]: Sashank J. Reddi and Satyen Kale and Sanjiv Kumar (2018), - On the Convergence of Adam and Beyond. - `arXiv:1904.09237 `_ - - .. note:: - - 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.utils.algorithm_globals.random_seed = seed``). - - """ - - _OPTIONS = [ - "maxiter", - "tol", - "lr", - "beta_1", - "beta_2", - "noise_factor", - "eps", - "amsgrad", - "snapshot_dir", - ] - - def __init__( - self, - maxiter: int = 10000, - tol: float = 1e-6, - lr: float = 1e-3, - beta_1: float = 0.9, - beta_2: float = 0.99, - noise_factor: float = 1e-8, - eps: float = 1e-10, - amsgrad: bool = False, - snapshot_dir: str | None = None, - ) -> None: - """ - Args: - maxiter: Maximum number of iterations - tol: Tolerance for termination - lr: Value >= 0, Learning rate. - beta_1: Value in range 0 to 1, Generally close to 1. - beta_2: Value in range 0 to 1, Generally close to 1. - noise_factor: Value >= 0, Noise factor - eps : Value >=0, Epsilon to be used for finite differences if no analytic - gradient method is given. - amsgrad: True to use AMSGRAD, False if not - snapshot_dir: If not None save the optimizer's parameter - after every step to the given directory - """ - super().__init__() - for k, v in list(locals().items()): - if k in self._OPTIONS: - self._options[k] = v - self._maxiter = maxiter - self._snapshot_dir = snapshot_dir - self._tol = tol - self._lr = lr - self._beta_1 = beta_1 - self._beta_2 = beta_2 - self._noise_factor = noise_factor - self._eps = eps - self._amsgrad = amsgrad - - # runtime variables - self._t = 0 # time steps - self._m = np.zeros(1) - self._v = np.zeros(1) - if self._amsgrad: - self._v_eff = np.zeros(1) - - if self._snapshot_dir: - - with open(os.path.join(self._snapshot_dir, "adam_params.csv"), mode="w") as csv_file: - if self._amsgrad: - fieldnames = ["v", "v_eff", "m", "t"] - else: - fieldnames = ["v", "m", "t"] - writer = csv.DictWriter(csv_file, fieldnames=fieldnames) - writer.writeheader() - - @property - def settings(self) -> dict[str, Any]: - return { - "maxiter": self._maxiter, - "tol": self._tol, - "lr": self._lr, - "beta_1": self._beta_1, - "beta_2": self._beta_2, - "noise_factor": self._noise_factor, - "eps": self._eps, - "amsgrad": self._amsgrad, - "snapshot_dir": self._snapshot_dir, - } - - def get_support_level(self): - """Return support level dictionary""" - return { - "gradient": OptimizerSupportLevel.supported, - "bounds": OptimizerSupportLevel.ignored, - "initial_point": OptimizerSupportLevel.supported, - } - - def save_params(self, snapshot_dir: str) -> None: - """Save the current iteration parameters to a file called ``adam_params.csv``. - - Note: - - The current parameters are appended to the file, if it exists already. - The file is not overwritten. - - Args: - snapshot_dir: The directory to store the file in. - """ - if self._amsgrad: - with open(os.path.join(snapshot_dir, "adam_params.csv"), mode="a") as csv_file: - fieldnames = ["v", "v_eff", "m", "t"] - writer = csv.DictWriter(csv_file, fieldnames=fieldnames) - writer.writerow({"v": self._v, "v_eff": self._v_eff, "m": self._m, "t": self._t}) - else: - with open(os.path.join(snapshot_dir, "adam_params.csv"), mode="a") as csv_file: - fieldnames = ["v", "m", "t"] - writer = csv.DictWriter(csv_file, fieldnames=fieldnames) - writer.writerow({"v": self._v, "m": self._m, "t": self._t}) - - def load_params(self, load_dir: str) -> None: - """Load iteration parameters for a file called ``adam_params.csv``. - - Args: - load_dir: The directory containing ``adam_params.csv``. - """ - with open(os.path.join(load_dir, "adam_params.csv")) as csv_file: - if self._amsgrad: - fieldnames = ["v", "v_eff", "m", "t"] - else: - fieldnames = ["v", "m", "t"] - reader = csv.DictReader(csv_file, fieldnames=fieldnames) - for line in reader: - v = line["v"] - if self._amsgrad: - v_eff = line["v_eff"] - m = line["m"] - t = line["t"] - - v = v[1:-1] - self._v = np.fromstring(v, dtype=float, sep=" ") - if self._amsgrad: - v_eff = v_eff[1:-1] - self._v_eff = np.fromstring(v_eff, dtype=float, sep=" ") - m = m[1:-1] - self._m = np.fromstring(m, dtype=float, sep=" ") - t = t[1:-1] - self._t = np.fromstring(t, dtype=int, sep=" ") - - @deprecate_arg( - "objective_function", new_alias="fun", since="0.19.0", package_name="qiskit-terra" - ) - @deprecate_arg("initial_point", new_alias="fun", since="0.19.0", package_name="qiskit-terra") - @deprecate_arg( - "gradient_function", new_alias="jac", since="0.19.0", package_name="qiskit-terra" - ) - def minimize( - self, - fun: Callable[[POINT], float], - x0: POINT, - jac: Callable[[POINT], POINT] | None = None, - bounds: list[tuple[float, float]] | None = None, - # pylint:disable=unused-argument - objective_function: Callable[[np.ndarray], float] | None = None, - initial_point: np.ndarray | None = None, - gradient_function: Callable[[np.ndarray], float] | None = None, - # ) -> Tuple[np.ndarray, float, int]: - ) -> OptimizerResult: # TODO find proper way to deprecate return type - """Minimize the scalar function. - - Args: - fun: The scalar function to minimize. - x0: The initial point for the minimization. - jac: The gradient of the scalar function ``fun``. - bounds: Bounds for the variables of ``fun``. This argument might be ignored if the - optimizer does not support bounds. - objective_function: DEPRECATED. A function handle to the objective function. - initial_point: DEPRECATED. The initial iteration point. - gradient_function: DEPRECATED. A function handle to the gradient of the objective - function. - - Returns: - The result of the optimization, containing e.g. the result as attribute ``x``. - """ - if jac is None: - jac = Optimizer.wrap_function(Optimizer.gradient_num_diff, (fun, self._eps)) - - derivative = jac(x0) - self._t = 0 - self._m = np.zeros(np.shape(derivative)) - self._v = np.zeros(np.shape(derivative)) - if self._amsgrad: - self._v_eff = np.zeros(np.shape(derivative)) - - params = params_new = x0 - while self._t < self._maxiter: - if self._t > 0: - derivative = jac(params) - self._t += 1 - self._m = self._beta_1 * self._m + (1 - self._beta_1) * derivative - self._v = self._beta_2 * self._v + (1 - self._beta_2) * derivative * derivative - lr_eff = self._lr * np.sqrt(1 - self._beta_2**self._t) / (1 - self._beta_1**self._t) - if not self._amsgrad: - params_new = params - lr_eff * self._m.flatten() / ( - np.sqrt(self._v.flatten()) + self._noise_factor - ) - else: - self._v_eff = np.maximum(self._v_eff, self._v) - params_new = params - lr_eff * self._m.flatten() / ( - np.sqrt(self._v_eff.flatten()) + self._noise_factor - ) - - if self._snapshot_dir: - self.save_params(self._snapshot_dir) - - # check termination - if np.linalg.norm(params - params_new) < self._tol: - break - - params = params_new - - result = OptimizerResult() - result.x = params_new - result.fun = fun(params_new) - result.nfev = self._t - return result diff --git a/qiskit/algorithms/optimizers/aqgd.py b/qiskit/algorithms/optimizers/aqgd.py deleted file mode 100644 index ad8ad42ae9f1..000000000000 --- a/qiskit/algorithms/optimizers/aqgd.py +++ /dev/null @@ -1,367 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2019, 2021. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. - -"""Analytical Quantum Gradient Descent (AQGD) optimizer.""" - -from __future__ import annotations -import logging -from collections.abc import Callable -from typing import Any -import warnings - -import numpy as np -from qiskit.utils.validation import validate_range_exclusive_max -from .optimizer import Optimizer, OptimizerSupportLevel, OptimizerResult, POINT -from ..exceptions import AlgorithmError - -logger = logging.getLogger(__name__) - - -class AQGD(Optimizer): - """Analytic Quantum Gradient Descent (AQGD) with Epochs optimizer. - Performs gradient descent optimization with a momentum term, analytic gradients, - and customized step length schedule for parameterized quantum gates, i.e. - Pauli Rotations. See, for example: - - * K. Mitarai, M. Negoro, M. Kitagawa, and K. Fujii. (2018). - Quantum circuit learning. Phys. Rev. A 98, 032309. - https://arxiv.org/abs/1803.00745 - - * Maria Schuld, Ville Bergholm, Christian Gogolin, Josh Izaac, Nathan Killoran. (2019). - Evaluating analytic gradients on quantum hardware. Phys. Rev. A 99, 032331. - https://arxiv.org/abs/1811.11184 - - for further details on analytic gradients of parameterized quantum gates. - - Gradients are computed "analytically" using the quantum circuit when evaluating - the objective function. - - """ - - _OPTIONS = ["maxiter", "eta", "tol", "disp", "momentum", "param_tol", "averaging"] - - def __init__( - self, - maxiter: int | list[int] = 1000, - eta: float | list[float] = 1.0, - tol: float = 1e-6, # this is tol - momentum: float | list[float] = 0.25, - param_tol: float = 1e-6, - averaging: int = 10, - ) -> None: - """ - Performs Analytical Quantum Gradient Descent (AQGD) with Epochs. - - Args: - maxiter: Maximum number of iterations (full gradient steps) - eta: The coefficient of the gradient update. Increasing this value - results in larger step sizes: param = previous_param - eta * deriv - tol: Tolerance for change in windowed average of objective values. - Convergence occurs when either objective tolerance is met OR parameter - tolerance is met. - momentum: Bias towards the previous gradient momentum in current - update. Must be within the bounds: [0,1) - param_tol: Tolerance for change in norm of parameters. - averaging: Length of window over which to average objective values for objective - convergence criterion - - Raises: - AlgorithmError: If the length of ``maxiter``, `momentum``, and ``eta`` is not the same. - """ - super().__init__() - if isinstance(maxiter, int): - maxiter = [maxiter] - if isinstance(eta, (int, float)): - eta = [eta] - if isinstance(momentum, (int, float)): - momentum = [momentum] - if len(maxiter) != len(eta) or len(maxiter) != len(momentum): - raise AlgorithmError( - "AQGD input parameter length mismatch. Parameters `maxiter`, " - "`eta`, and `momentum` must have the same length." - ) - for m in momentum: - with warnings.catch_warnings(): - warnings.filterwarnings("ignore", category=DeprecationWarning) - validate_range_exclusive_max("momentum", m, 0, 1) - - self._eta = eta - self._maxiter = maxiter - self._momenta_coeff = momentum - self._param_tol = param_tol - self._tol = tol - self._averaging = averaging - - # state - self._avg_objval: float | None = None - self._prev_param: np.ndarray | None = None - self._eval_count = 0 # function evaluations - self._prev_loss: list[float] = [] - self._prev_grad: list[list[float]] = [] - - def get_support_level(self) -> dict[str, OptimizerSupportLevel]: - """Support level dictionary - - Returns: - Dict[str, int]: gradient, bounds and initial point - support information that is ignored/required. - """ - return { - "gradient": OptimizerSupportLevel.ignored, - "bounds": OptimizerSupportLevel.ignored, - "initial_point": OptimizerSupportLevel.required, - } - - @property - def settings(self) -> dict[str, Any]: - return { - "maxiter": self._maxiter, - "eta": self._eta, - "momentum": self._momenta_coeff, - "param_tol": self._param_tol, - "tol": self._tol, - "averaging": self._averaging, - } - - def _compute_objective_fn_and_gradient( - self, params: np.ndarray | list[float], obj: Callable - ) -> tuple[float, np.ndarray]: - """ - Obtains the objective function value for params and the analytical quantum derivatives of - the objective function with respect to each parameter. Requires - 2*(number parameters) + 1 objective evaluations - - Args: - params: Current value of the parameters to evaluate the objective function - obj: Objective function of interest - - Returns: - Tuple containing the objective value and array of gradients for the given parameter set. - """ - num_params = len(params) - param_sets_to_eval = params + np.concatenate( - ( - np.zeros((1, num_params)), # copy of the parameters as is - np.eye(num_params) * np.pi / 2, # copy of the parameters with the positive shift - -np.eye(num_params) * np.pi / 2, - ), # copy of the parameters with the negative shift - axis=0, - ) - # Evaluate, - # reshaping to flatten, as expected by objective function - values = np.array(obj(param_sets_to_eval.reshape(-1))) - - # Update number of objective function evaluations - self._eval_count += 2 * num_params + 1 - - # return the objective function value - obj_value = values[0] - - # return the gradient values - gradient = 0.5 * (values[1 : num_params + 1] - values[1 + num_params :]) - return obj_value, gradient - - def _update( - self, - params: np.ndarray, - gradient: np.ndarray, - mprev: np.ndarray, - step_size: float, - momentum_coeff: float, - ) -> tuple[np.ndarray, np.ndarray]: - """ - Updates full parameter array based on a step that is a convex - combination of the gradient and previous momentum - - Args: - params: Current value of the parameters to evaluate the objective function at - gradient: Gradient of objective wrt parameters - mprev: Momentum vector for each parameter - step_size: The scaling of step to take - momentum_coeff: Bias towards previous momentum vector when updating current - momentum/step vector - - Returns: - Tuple of the updated parameter and momentum vectors respectively. - """ - # Momentum update: - # Convex combination of previous momentum and current gradient estimate - mnew = (1 - momentum_coeff) * gradient + momentum_coeff * mprev - params -= step_size * mnew - return params, mnew - - def _converged_objective(self, objval: float, tol: float, window_size: int) -> bool: - """ - Tests convergence based on the change in a moving windowed average of past objective values - - Args: - objval: Current value of the objective function - tol: tolerance below which (average) objective function change must be - window_size: size of averaging window - - Returns: - Bool indicating whether or not the optimization has converged. - """ - # If we haven't reached the required window length, - # append the current value, but we haven't converged - if len(self._prev_loss) < window_size: - self._prev_loss.append(objval) - return False - - # Update last value in list with current value - self._prev_loss.append(objval) - # (length now = n+1) - - # Calculate previous windowed average - # and current windowed average of objective values - prev_avg = np.mean(self._prev_loss[:window_size]) - curr_avg = np.mean(self._prev_loss[1 : window_size + 1]) - self._avg_objval = curr_avg - - # Update window of objective values - # (Remove earliest value) - self._prev_loss.pop(0) - - if np.absolute(prev_avg - curr_avg) < tol: - # converged - logger.info("Previous obj avg: %f\nCurr obj avg: %f", prev_avg, curr_avg) - return True - return False - - def _converged_parameter(self, parameter: np.ndarray, tol: float) -> bool: - """ - Tests convergence based on change in parameter - - Args: - parameter: current parameter values - tol: tolerance for change in norm of parameters - - Returns: - Bool indicating whether or not the optimization has converged - """ - if self._prev_param is None: - self._prev_param = np.copy(parameter) - return False - - order = np.inf - p_change = np.linalg.norm(self._prev_param - parameter, ord=order) - if p_change < tol: - # converged - logger.info("Change in parameters (%f norm): %f", order, p_change) - return True - return False - - def _converged_alt(self, gradient: list[float], tol: float, window_size: int) -> bool: - """ - Tests convergence from norm of windowed average of gradients - - Args: - gradient: current gradient - tol: tolerance for average gradient norm - window_size: size of averaging window - - Returns: - Bool indicating whether or not the optimization has converged - """ - # If we haven't reached the required window length, - # append the current value, but we haven't converged - if len(self._prev_grad) < window_size - 1: - self._prev_grad.append(gradient) - return False - - # Update last value in list with current value - self._prev_grad.append(gradient) - # (length now = n) - - # Calculate previous windowed average - # and current windowed average of objective values - avg_grad = np.mean(self._prev_grad, axis=0) - - # Update window of values - # (Remove earliest value) - self._prev_grad.pop(0) - - if np.linalg.norm(avg_grad, ord=np.inf) < tol: - # converged - logger.info("Avg. grad. norm: %f", np.linalg.norm(avg_grad, ord=np.inf)) - return True - return False - - def minimize( - self, - fun: Callable[[POINT], float], - x0: POINT, - jac: Callable[[POINT], POINT] | None = None, - bounds: list[tuple[float, float]] | None = None, - ) -> OptimizerResult: - params = np.asarray(x0) - momentum = np.zeros(shape=(params.size,)) - # empty out history of previous objectives/gradients/parameters - # (in case this object is re-used) - self._prev_loss = [] - self._prev_grad = [] - self._prev_param = None - self._eval_count = 0 # function evaluations - - iter_count = 0 - logger.info("Initial Params: %s", params) - - epoch = 0 - converged = False - for (eta, mom_coeff) in zip(self._eta, self._momenta_coeff): - logger.info("Epoch: %4d | Stepsize: %6.4f | Momentum: %6.4f", epoch, eta, mom_coeff) - - sum_max_iters = sum(self._maxiter[0 : epoch + 1]) - while iter_count < sum_max_iters: - # update the iteration count - iter_count += 1 - - # Check for parameter convergence before potentially costly function evaluation - converged = self._converged_parameter(params, self._param_tol) - if converged: - break - - # Calculate objective function and estimate of analytical gradient - if jac is None: - objval, gradient = self._compute_objective_fn_and_gradient(params, fun) - else: - objval = fun(params) - gradient = jac(params) - - logger.info( - " Iter: %4d | Obj: %11.6f | Grad Norm: %f", - iter_count, - objval, - np.linalg.norm(gradient, ord=np.inf), - ) - - # Check for objective convergence - converged = self._converged_objective(objval, self._tol, self._averaging) - if converged: - break - - # Update parameters and momentum - params, momentum = self._update(params, gradient, momentum, eta, mom_coeff) - # end inner iteration - # if converged, end iterating over epochs - if converged: - break - epoch += 1 - # end epoch iteration - - result = OptimizerResult() - result.x = params - result.fun = objval - result.nfev = self._eval_count - result.nit = iter_count - - return result diff --git a/qiskit/algorithms/optimizers/bobyqa.py b/qiskit/algorithms/optimizers/bobyqa.py deleted file mode 100644 index 39250aef917a..000000000000 --- a/qiskit/algorithms/optimizers/bobyqa.py +++ /dev/null @@ -1,84 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2019, 2020. -# -# 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. - -"""Bound Optimization BY Quadratic Approximation (BOBYQA) optimizer.""" - -from __future__ import annotations - -from collections.abc import Callable -from typing import Any - -import numpy as np -from qiskit.utils import optionals as _optionals -from .optimizer import Optimizer, OptimizerSupportLevel, OptimizerResult, POINT - - -@_optionals.HAS_SKQUANT.require_in_instance -class BOBYQA(Optimizer): - """Bound Optimization BY Quadratic Approximation algorithm. - - BOBYQA finds local solutions to nonlinear, non-convex minimization problems with optional - bound constraints, without requirement of derivatives of the objective function. - - Uses skquant.opt installed with pip install scikit-quant. - For further detail, please refer to - https://github.com/scikit-quant/scikit-quant and https://qat4chem.lbl.gov/software. - """ - - def __init__( - self, - maxiter: int = 1000, - ) -> None: - """ - Args: - maxiter: Maximum number of function evaluations. - - Raises: - MissingOptionalLibraryError: scikit-quant not installed - """ - super().__init__() - self._maxiter = maxiter - - def get_support_level(self): - """Returns support level dictionary.""" - return { - "gradient": OptimizerSupportLevel.ignored, - "bounds": OptimizerSupportLevel.required, - "initial_point": OptimizerSupportLevel.required, - } - - @property - def settings(self) -> dict[str, Any]: - return {"maxiter": self._maxiter} - - def minimize( - self, - fun: Callable[[POINT], float], - x0: POINT, - jac: Callable[[POINT], POINT] | None = None, - bounds: list[tuple[float, float]] | None = None, - ) -> OptimizerResult: - from skquant import opt as skq - - res, history = skq.minimize( - func=fun, - x0=np.asarray(x0), - bounds=np.array(bounds), - budget=self._maxiter, - method="bobyqa", - ) - - optimizer_result = OptimizerResult() - optimizer_result.x = res.optpar - optimizer_result.fun = res.optval - optimizer_result.nfev = len(history) - return optimizer_result diff --git a/qiskit/algorithms/optimizers/cg.py b/qiskit/algorithms/optimizers/cg.py deleted file mode 100644 index 670b4ac33868..000000000000 --- a/qiskit/algorithms/optimizers/cg.py +++ /dev/null @@ -1,70 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2018, 2020. -# -# 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. - -"""Conjugate Gradient optimizer.""" - -from __future__ import annotations - -from .scipy_optimizer import SciPyOptimizer - - -class CG(SciPyOptimizer): - """Conjugate Gradient optimizer. - - CG is an algorithm for the numerical solution of systems of linear equations whose matrices are - symmetric and positive-definite. It is an *iterative algorithm* in that it uses an initial - guess to generate a sequence of improving approximate solutions for a problem, - in which each approximation is derived from the previous ones. It is often used to solve - unconstrained optimization problems, such as energy minimization. - - Uses scipy.optimize.minimize CG. - For further detail, please refer to - https://docs.scipy.org/doc/scipy/reference/generated/scipy.optimize.minimize.html - """ - - _OPTIONS = ["maxiter", "disp", "gtol", "eps"] - - # pylint: disable=unused-argument - def __init__( - self, - maxiter: int = 20, - disp: bool = False, - gtol: float = 1e-5, - tol: float | None = None, - eps: float = 1.4901161193847656e-08, - options: dict | None = None, - max_evals_grouped: int = 1, - **kwargs, - ) -> None: - """ - Args: - maxiter: Maximum number of iterations to perform. - disp: Set to True to print convergence messages. - gtol: Gradient norm must be less than gtol before successful termination. - tol: Tolerance for termination. - eps: If jac is approximated, use this value for the step size. - options: A dictionary of solver options. - max_evals_grouped: Max number of default gradient evaluations performed simultaneously. - kwargs: additional kwargs for scipy.optimize.minimize. - """ - if options is None: - options = {} - for k, v in list(locals().items()): - if k in self._OPTIONS: - options[k] = v - super().__init__( - method="CG", - options=options, - tol=tol, - max_evals_grouped=max_evals_grouped, - **kwargs, - ) diff --git a/qiskit/algorithms/optimizers/cobyla.py b/qiskit/algorithms/optimizers/cobyla.py deleted file mode 100644 index 72a0938379e7..000000000000 --- a/qiskit/algorithms/optimizers/cobyla.py +++ /dev/null @@ -1,59 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2018, 2020. -# -# 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. - -"""Constrained Optimization By Linear Approximation optimizer.""" - -from __future__ import annotations - -from .scipy_optimizer import SciPyOptimizer - - -class COBYLA(SciPyOptimizer): - """ - Constrained Optimization By Linear Approximation optimizer. - - COBYLA is a numerical optimization method for constrained problems - where the derivative of the objective function is not known. - - Uses scipy.optimize.minimize COBYLA. - For further detail, please refer to - https://docs.scipy.org/doc/scipy/reference/generated/scipy.optimize.minimize.html - """ - - _OPTIONS = ["maxiter", "disp", "rhobeg"] - - # pylint: disable=unused-argument - def __init__( - self, - maxiter: int = 1000, - disp: bool = False, - rhobeg: float = 1.0, - tol: float | None = None, - options: dict | None = None, - **kwargs, - ) -> None: - """ - Args: - maxiter: Maximum number of function evaluations. - disp: Set to True to print convergence messages. - rhobeg: Reasonable initial changes to the variables. - tol: Final accuracy in the optimization (not precisely guaranteed). - This is a lower bound on the size of the trust region. - options: A dictionary of solver options. - kwargs: additional kwargs for scipy.optimize.minimize. - """ - if options is None: - options = {} - for k, v in list(locals().items()): - if k in self._OPTIONS: - options[k] = v - super().__init__(method="COBYLA", options=options, tol=tol, **kwargs) diff --git a/qiskit/algorithms/optimizers/gradient_descent.py b/qiskit/algorithms/optimizers/gradient_descent.py deleted file mode 100644 index bd39ffa83d85..000000000000 --- a/qiskit/algorithms/optimizers/gradient_descent.py +++ /dev/null @@ -1,401 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2021, 2022. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. - -"""A standard gradient descent optimizer.""" -from __future__ import annotations - -from collections.abc import Generator -from dataclasses import dataclass, field -from typing import Any, Callable, SupportsFloat -import numpy as np -from .optimizer import Optimizer, OptimizerSupportLevel, OptimizerResult, POINT -from .steppable_optimizer import AskData, TellData, OptimizerState, SteppableOptimizer -from .optimizer_utils import LearningRate - -CALLBACK = Callable[[int, np.ndarray, float, SupportsFloat], None] - - -@dataclass -class GradientDescentState(OptimizerState): - """State of :class:`~.GradientDescent`. - - Dataclass with all the information of an optimizer plus the learning_rate and the stepsize. - """ - - stepsize: float | None - """Norm of the gradient on the last step.""" - - learning_rate: LearningRate = field(compare=False) - """Learning rate at the current step of the optimization process. - - It behaves like a generator, (use ``next(learning_rate)`` to get the learning rate for the - next step) but it can also return the current learning rate with ``learning_rate.current``. - - """ - - -class GradientDescent(SteppableOptimizer): - r"""The gradient descent minimization routine. - - For a function :math:`f` and an initial point :math:`\vec\theta_0`, the standard (or "vanilla") - gradient descent method is an iterative scheme to find the minimum :math:`\vec\theta^*` of - :math:`f` by updating the parameters in the direction of the negative gradient of :math:`f` - - .. math:: - - \vec\theta_{n+1} = \vec\theta_{n} - \eta_n \vec\nabla f(\vec\theta_{n}), - - for a small learning rate :math:`\eta_n > 0`. - - You can either provide the analytic gradient :math:`\vec\nabla f` as ``jac`` - in the :meth:`~.minimize` method, or, if you do not provide it, use a finite difference - approximation of the gradient. To adapt the size of the perturbation in the finite difference - gradients, set the ``perturbation`` property in the initializer. - - This optimizer supports a callback function. If provided in the initializer, the optimizer - will call the callback in each iteration with the following information in this order: - current number of function values, current parameters, current function value, norm of current - gradient. - - Examples: - - A minimum example that will use finite difference gradients with a default perturbation - of 0.01 and a default learning rate of 0.01. - - .. code-block:: python - - from qiskit.algorithms.optimizers import GradientDescent - - def f(x): - return (np.linalg.norm(x) - 1) ** 2 - - initial_point = np.array([1, 0.5, -0.2]) - - optimizer = GradientDescent(maxiter=100) - - result = optimizer.minimize(fun=fun, x0=initial_point) - - print(f"Found minimum {result.x} at a value" - "of {result.fun} using {result.nfev} evaluations.") - - An example where the learning rate is an iterator and we supply the analytic gradient. - Note how much faster this convergences (i.e. less ``nfev``) compared to the previous - example. - - .. code-block:: python - - from qiskit.algorithms.optimizers import GradientDescent - - def learning_rate(): - power = 0.6 - constant_coeff = 0.1 - def powerlaw(): - n = 0 - while True: - yield constant_coeff * (n ** power) - n += 1 - - return powerlaw() - - def f(x): - return (np.linalg.norm(x) - 1) ** 2 - - def grad_f(x): - return 2 * (np.linalg.norm(x) - 1) * x / np.linalg.norm(x) - - initial_point = np.array([1, 0.5, -0.2]) - - optimizer = GradientDescent(maxiter=100, learning_rate=learning_rate) - result = optimizer.minimize(fun=fun, jac=grad_f, x0=initial_point) - - print(f"Found minimum {result.x} at a value" - "of {result.fun} using {result.nfev} evaluations.") - - - An other example where the evaluation of the function has a chance of failing. The user, with - specific knowledge about his function can catch this errors and handle them before passing the - result to the optimizer. - - .. code-block:: python - - import random - import numpy as np - from qiskit.algorithms.optimizers import GradientDescent - - def objective(x): - if random.choice([True, False]): - return None - else: - return (np.linalg.norm(x) - 1) ** 2 - - def grad(x): - if random.choice([True, False]): - return None - else: - return 2 * (np.linalg.norm(x) - 1) * x / np.linalg.norm(x) - - - initial_point = np.random.normal(0, 1, size=(100,)) - - optimizer = GradientDescent(maxiter=20) - optimizer.start(x0=initial_point, fun=objective, jac=grad) - - while optimizer.continue_condition(): - ask_data = optimizer.ask() - evaluated_gradient = None - - while evaluated_gradient is None: - evaluated_gradient = grad(ask_data.x_center) - optimizer.state.njev += 1 - - optmizer.state.nit += 1 - - tell_data = TellData(eval_jac=evaluated_gradient) - optimizer.tell(ask_data=ask_data, tell_data=tell_data) - - result = optimizer.create_result() - - Users that aren't dealing with complicated functions and who are more familiar with step by step - optimization algorithms can use the :meth:`~.step` method which wraps the :meth:`~.ask` - and :meth:`~.tell` methods. In the same spirit the method :meth:`~.minimize` will optimize the - function and return the result. - - To see other libraries that use this interface one can visit: - https://optuna.readthedocs.io/en/stable/tutorial/20_recipes/009_ask_and_tell.html - - """ - - def __init__( - self, - maxiter: int = 100, - learning_rate: float - | list[float] - | np.ndarray - | Callable[[], Generator[float, None, None]] = 0.01, - tol: float = 1e-7, - callback: CALLBACK | None = None, - perturbation: float | None = None, - ) -> None: - """ - Args: - maxiter: The maximum number of iterations. - learning_rate: A constant, list, array or factory of generators yielding learning rates - for the parameter updates. See the docstring for an example. - tol: If the norm of the parameter update is smaller than this threshold, the - optimizer has converged. - perturbation: If no gradient is passed to :meth:`~.minimize` the gradient is - approximated with a forward finite difference scheme with ``perturbation`` - perturbation in both directions (defaults to 1e-2 if required). - Ignored when we have an explicit function for the gradient. - Raises: - ValueError: If ``learning_rate`` is an array and its length is less than ``maxiter``. - """ - super().__init__(maxiter=maxiter) - self.callback = callback - self._state: GradientDescentState | None = None - self._perturbation = perturbation - self._tol = tol - # if learning rate is an array, check it is sufficiently long. - if isinstance(learning_rate, (list, np.ndarray)): - if len(learning_rate) < maxiter: - raise ValueError( - f"Length of learning_rate ({len(learning_rate)}) " - f"is smaller than maxiter ({maxiter})." - ) - self.learning_rate = learning_rate - - @property - def state(self) -> GradientDescentState: - """Return the current state of the optimizer.""" - return self._state - - @state.setter - def state(self, state: GradientDescentState) -> None: - """Set the current state of the optimizer.""" - self._state = state - - @property - def tol(self) -> float: - """Returns the tolerance of the optimizer. - - Any step with smaller stepsize than this value will stop the optimization.""" - return self._tol - - @tol.setter - def tol(self, tol: float) -> None: - """Set the tolerance.""" - self._tol = tol - - @property - def perturbation(self) -> float | None: - """Returns the perturbation. - - This is the perturbation used in the finite difference gradient approximation. - """ - return self._perturbation - - @perturbation.setter - def perturbation(self, perturbation: float | None) -> None: - """Set the perturbation.""" - self._perturbation = perturbation - - def _callback_wrapper(self) -> None: - """ - Wraps the callback function to accommodate GradientDescent. - - Will call :attr:`~.callback` and pass the following arguments: - current number of function values, current parameters, current function value, - norm of current gradient. - """ - if self.callback is not None: - self.callback( - self.state.nfev, - self.state.x, - self.state.fun(self.state.x), - self.state.stepsize, - ) - - @property - def settings(self) -> dict[str, Any]: - # if learning rate or perturbation are custom iterators expand them - if callable(self.learning_rate): - iterator = self.learning_rate() - learning_rate: float | np.ndarray = np.array( - [next(iterator) for _ in range(self.maxiter)] - ) - else: - learning_rate = self.learning_rate - - return { - "maxiter": self.maxiter, - "tol": self.tol, - "learning_rate": learning_rate, - "perturbation": self.perturbation, - "callback": self.callback, - } - - def ask(self) -> AskData: - """Returns an object with the data needed to evaluate the gradient. - - If this object contains a gradient function the gradient can be evaluated directly. Otherwise - approximate it with a finite difference scheme. - """ - return AskData( - x_jac=self.state.x, - ) - - def tell(self, ask_data: AskData, tell_data: TellData) -> None: - """ - Updates :attr:`.~GradientDescentState.x` by an amount proportional to the learning - rate and value of the gradient at that point. - - Args: - ask_data: The data used to evaluate the function. - tell_data: The data from the function evaluation. - - Raises: - ValueError: If the gradient passed doesn't have the right dimension. - """ - if np.shape(self.state.x) != np.shape(tell_data.eval_jac): - raise ValueError("The gradient does not have the correct dimension") - self.state.x = self.state.x - next(self.state.learning_rate) * tell_data.eval_jac - self.state.stepsize = np.linalg.norm(tell_data.eval_jac) - self.state.nit += 1 - - def evaluate(self, ask_data: AskData) -> TellData: - """Evaluates the gradient. - - It does so either by evaluating an analytic gradient or by approximating it with a - finite difference scheme. It will either add ``1`` to the number of gradient evaluations or add - ``N+1`` to the number of function evaluations (Where N is the dimension of the gradient). - - Args: - ask_data: It contains the point where the gradient is to be evaluated and the gradient - function or, in its absence, the objective function to perform a finite difference - approximation. - - Returns: - The data containing the gradient evaluation. - """ - if self.state.jac is None: - eps = 0.01 if (self.perturbation is None) else self.perturbation - grad = Optimizer.gradient_num_diff( - x_center=ask_data.x_jac, - f=self.state.fun, - epsilon=eps, - max_evals_grouped=self._max_evals_grouped, - ) - self.state.nfev += 1 + len(ask_data.x_jac) - else: - grad = self.state.jac(ask_data.x_jac) - self.state.njev += 1 - - return TellData(eval_jac=grad) - - def create_result(self) -> OptimizerResult: - """Creates a result of the optimization process. - - This result contains the best point, the best function value, the number of function/gradient - evaluations and the number of iterations. - - Returns: - The result of the optimization process. - """ - result = OptimizerResult() - result.x = self.state.x - result.fun = self.state.fun(self.state.x) - result.nfev = self.state.nfev - result.njev = self.state.njev - result.nit = self.state.nit - return result - - def start( - self, - fun: Callable[[POINT], float], - x0: POINT, - jac: Callable[[POINT], POINT] | None = None, - bounds: list[tuple[float, float]] | None = None, - ) -> None: - - self.state = GradientDescentState( - fun=fun, - jac=jac, - x=np.asarray(x0), - nit=0, - nfev=0, - njev=0, - learning_rate=LearningRate(learning_rate=self.learning_rate), - stepsize=None, - ) - - def continue_condition(self) -> bool: - """ - Condition that indicates the optimization process should come to an end. - - When the stepsize is smaller than the tolerance, the optimization process is considered - finished. - - Returns: - ``True`` if the optimization process should continue, ``False`` otherwise. - """ - if self.state.stepsize is None: - return True - else: - return (self.state.stepsize > self.tol) and super().continue_condition() - - def get_support_level(self): - """Get the support level dictionary.""" - return { - "gradient": OptimizerSupportLevel.supported, - "bounds": OptimizerSupportLevel.ignored, - "initial_point": OptimizerSupportLevel.required, - } diff --git a/qiskit/algorithms/optimizers/gsls.py b/qiskit/algorithms/optimizers/gsls.py deleted file mode 100644 index 6ae423fb52c8..000000000000 --- a/qiskit/algorithms/optimizers/gsls.py +++ /dev/null @@ -1,378 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2018, 2021. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. - -"""Line search with Gaussian-smoothed samples on a sphere.""" - -from __future__ import annotations - -import warnings - -from collections.abc import Callable -from typing import Any, SupportsFloat -import numpy as np - -from qiskit.utils import algorithm_globals -from .optimizer import Optimizer, OptimizerSupportLevel, OptimizerResult, POINT - - -class GSLS(Optimizer): - """Gaussian-smoothed Line Search. - - An implementation of the line search algorithm described in - https://arxiv.org/pdf/1905.01332.pdf, using gradient approximation - based on Gaussian-smoothed samples on a sphere. - - .. note:: - - 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.utils.algorithm_globals.random_seed = seed``). - """ - - _OPTIONS = [ - "maxiter", - "max_eval", - "disp", - "sampling_radius", - "sample_size_factor", - "initial_step_size", - "min_step_size", - "step_size_multiplier", - "armijo_parameter", - "min_gradient_norm", - "max_failed_rejection_sampling", - ] - - # pylint: disable=unused-argument - def __init__( - self, - maxiter: int = 10000, - max_eval: int = 10000, - disp: bool = False, - sampling_radius: float = 1.0e-6, - sample_size_factor: int = 1, - initial_step_size: float = 1.0e-2, - min_step_size: float = 1.0e-10, - step_size_multiplier: float = 0.4, - armijo_parameter: float = 1.0e-1, - min_gradient_norm: float = 1e-8, - max_failed_rejection_sampling: int = 50, - ) -> None: - """ - Args: - maxiter: Maximum number of iterations. - max_eval: Maximum number of evaluations. - disp: Set to True to display convergence messages. - sampling_radius: Sampling radius to determine gradient estimate. - sample_size_factor: The size of the sample set at each iteration is this number - multiplied by the dimension of the problem, rounded to the nearest integer. - initial_step_size: Initial step size for the descent algorithm. - min_step_size: Minimum step size for the descent algorithm. - step_size_multiplier: Step size reduction after unsuccessful steps, in the - interval (0, 1). - armijo_parameter: Armijo parameter for sufficient decrease criterion, in the - interval (0, 1). - min_gradient_norm: If the gradient norm is below this threshold, the algorithm stops. - max_failed_rejection_sampling: Maximum number of attempts to sample points within - bounds. - """ - super().__init__() - for k, v in list(locals().items()): - if k in self._OPTIONS: - self._options[k] = v - - def get_support_level(self) -> dict[str, int]: - """Return support level dictionary. - - Returns: - A dictionary containing the support levels for different options. - """ - return { - "gradient": OptimizerSupportLevel.ignored, - "bounds": OptimizerSupportLevel.supported, - "initial_point": OptimizerSupportLevel.required, - } - - @property - def settings(self) -> dict[str, Any]: - return {key: self._options.get(key, None) for key in self._OPTIONS} - - def minimize( - self, - fun: Callable[[POINT], float], - x0: POINT, - jac: Callable[[POINT], POINT] | None = None, - bounds: list[tuple[float, float]] | None = None, - ) -> OptimizerResult: - if not isinstance(x0, np.ndarray): - x0 = np.asarray(x0) - - if bounds is None: - var_lb = np.array([-np.inf] * x0.size) - var_ub = np.array([np.inf] * x0.size) - else: - var_lb = np.array([l for (l, _) in bounds]) - var_ub = np.array([u for (_, u) in bounds]) - - x, fun, nfev, _ = self.ls_optimize(x0.size, fun, x0, var_lb, var_ub) - - result = OptimizerResult() - result.x = x - result.fun = fun - result.nfev = nfev - - return result - - def ls_optimize( - self, - n: int, - obj_fun: Callable[[POINT], float], - initial_point: np.ndarray, - var_lb: np.ndarray, - var_ub: np.ndarray, - ) -> tuple[np.ndarray, float, int, float]: - """Run the line search optimization. - - Args: - n: Dimension of the problem. - obj_fun: Objective function. - initial_point: Initial point. - var_lb: Vector of lower bounds on the decision variables. Vector elements can be -np.inf - if the corresponding variable is unbounded from below. - var_ub: Vector of upper bounds on the decision variables. Vector elements can be np.inf - if the corresponding variable is unbounded from below. - - Returns: - Final iterate as a vector, corresponding objective function value, - number of evaluations, and norm of the gradient estimate. - - Raises: - ValueError: If the number of dimensions mismatches the size of the initial point or - the length of the lower or upper bound. - """ - if len(initial_point) != n: - raise ValueError("Size of the initial point mismatches the number of dimensions.") - if len(var_lb) != n: - raise ValueError("Length of the lower bound mismatches the number of dimensions.") - if len(var_ub) != n: - raise ValueError("Length of the upper bound mismatches the number of dimensions.") - - # Initialize counters and data - iter_count = 0 - n_evals = 0 - prev_iter_successful = True - prev_directions, prev_sample_set_x, prev_sample_set_y = None, None, None - consecutive_fail_iter = 0 - alpha = self._options["initial_step_size"] - grad_norm: SupportsFloat = np.inf - sample_set_size = int(round(self._options["sample_size_factor"] * n)) - - # Initial point - x = initial_point - x_value = obj_fun(x) - n_evals += 1 - while iter_count < self._options["maxiter"] and n_evals < self._options["max_eval"]: - - # Determine set of sample points - directions, sample_set_x = self.sample_set(n, x, var_lb, var_ub, sample_set_size) - - if n_evals + len(sample_set_x) + 1 >= self._options["max_eval"]: - # The evaluation budget is too small to allow for - # another full iteration; we therefore exit now - break - - sample_set_y = np.array([obj_fun(point) for point in sample_set_x]) - n_evals += len(sample_set_x) - - # Expand sample set if we could not improve - if not prev_iter_successful: - directions = np.vstack((prev_directions, directions)) - sample_set_x = np.vstack((prev_sample_set_x, sample_set_x)) - sample_set_y = np.hstack((prev_sample_set_y, sample_set_y)) - - # Find gradient approximation and candidate point - grad = self.gradient_approximation( - n, x, x_value, directions, sample_set_x, sample_set_y - ) - grad_norm = np.linalg.norm(grad) - new_x = np.clip(x - alpha * grad, var_lb, var_ub) - new_x_value = obj_fun(new_x) - n_evals += 1 - - # Print information - if self._options["disp"]: - print(f"Iter {iter_count:d}") - print(f"Point {x} obj {x_value}") - print(f"Gradient {grad}") - print(f"Grad norm {grad_norm} new_x_value {new_x_value} step_size {alpha}") - print(f"Direction {directions}") - - # Test Armijo condition for sufficient decrease - if new_x_value <= x_value - self._options["armijo_parameter"] * alpha * grad_norm: - # Accept point - x, x_value = new_x, new_x_value - alpha /= 2 * self._options["step_size_multiplier"] - prev_iter_successful = True - consecutive_fail_iter = 0 - - # Reset sample set - prev_directions = None - prev_sample_set_x = None - prev_sample_set_y = None - else: - # Do not accept point - alpha *= self._options["step_size_multiplier"] - prev_iter_successful = False - consecutive_fail_iter += 1 - - # Store sample set to enlarge it - prev_directions = directions - prev_sample_set_x, prev_sample_set_y = sample_set_x, sample_set_y - - iter_count += 1 - - # Check termination criterion - if ( - grad_norm <= self._options["min_gradient_norm"] - or alpha <= self._options["min_step_size"] - ): - break - - return x, x_value, n_evals, grad_norm - - def sample_points( - self, n: int, x: np.ndarray, num_points: int - ) -> tuple[np.ndarray, np.ndarray]: - """Sample ``num_points`` points around ``x`` on the ``n``-sphere of specified radius. - - The radius of the sphere is ``self._options['sampling_radius']``. - - Args: - n: Dimension of the problem. - x: Point around which the sample set is constructed. - num_points: Number of points in the sample set. - - Returns: - A tuple containing the sampling points and the directions. - """ - with warnings.catch_warnings(): - warnings.filterwarnings("ignore", category=DeprecationWarning) - normal_samples = algorithm_globals.random.normal(size=(num_points, n)) - row_norms = np.linalg.norm(normal_samples, axis=1, keepdims=True) - directions = normal_samples / row_norms - points = x + self._options["sampling_radius"] * directions - - return points, directions - - def sample_set( - self, n: int, x: np.ndarray, var_lb: np.ndarray, var_ub: np.ndarray, num_points: int - ) -> tuple[np.ndarray, np.ndarray]: - """Construct sample set of given size. - - Args: - n: Dimension of the problem. - x: Point around which the sample set is constructed. - var_lb: Vector of lower bounds on the decision variables. Vector elements can be -np.inf - if the corresponding variable is unbounded from below. - var_ub: Vector of lower bounds on the decision variables. Vector elements can be np.inf - if the corresponding variable is unbounded from above. - num_points: Number of points in the sample set. - - Returns: - Matrices of (unit-norm) sample directions and sample points, one per row. - Both matrices are 2D arrays of floats. - - Raises: - RuntimeError: If not enough samples could be generated within the bounds. - """ - # Generate points uniformly on the sphere - points, directions = self.sample_points(n, x, num_points) - - # Check bounds - if (points >= var_lb).all() and (points <= var_ub).all(): - # If all points are within bounds, return them - return directions, (x + self._options["sampling_radius"] * directions) - else: - # Otherwise we perform rejection sampling until we have - # enough points that satisfy the bounds - indices = np.where((points >= var_lb).all(axis=1) & (points <= var_ub).all(axis=1))[0] - accepted = directions[indices] - num_trials = 0 - - while ( - len(accepted) < num_points - and num_trials < self._options["max_failed_rejection_sampling"] - ): - # Generate points uniformly on the sphere - points, directions = self.sample_points(n, x, num_points) - indices = np.where((points >= var_lb).all(axis=1) & (points <= var_ub).all(axis=1))[ - 0 - ] - accepted = np.vstack((accepted, directions[indices])) - num_trials += 1 - - # When we are at a corner point, the expected fraction of acceptable points may be - # exponential small in the dimension of the problem. Thus, if we keep failing and - # do not have enough points by now, we switch to a different method that guarantees - # finding enough points, but they may not be uniformly distributed. - if len(accepted) < num_points: - points, directions = self.sample_points(n, x, num_points) - to_be_flipped = (points < var_lb) | (points > var_ub) - directions *= np.where(to_be_flipped, -1, 1) - points = x + self._options["sampling_radius"] * directions - indices = np.where((points >= var_lb).all(axis=1) & (points <= var_ub).all(axis=1))[ - 0 - ] - accepted = np.vstack((accepted, directions[indices])) - - # If we still do not have enough sampling points, we have failed. - if len(accepted) < num_points: - raise RuntimeError( - "Could not generate enough samples within bounds; try smaller radius." - ) - - return ( - accepted[:num_points], - x + self._options["sampling_radius"] * accepted[:num_points], - ) - - def gradient_approximation( - self, - n: int, - x: np.ndarray, - x_value: float, - directions: np.ndarray, - sample_set_x: np.ndarray, - sample_set_y: np.ndarray, - ) -> np.ndarray: - """Construct gradient approximation from given sample. - - Args: - n: Dimension of the problem. - x: Point around which the sample set was constructed. - x_value: Objective function value at x. - directions: Directions of the sample points wrt the central point x, as a 2D array. - sample_set_x: x-coordinates of the sample set, one point per row, as a 2D array. - sample_set_y: Objective function values of the points in sample_set_x, as a 1D array. - - Returns: - Gradient approximation at x, as a 1D array. - """ - ffd = sample_set_y - x_value - gradient = ( - float(n) - / len(sample_set_y) - * np.sum( - ffd.reshape(len(sample_set_y), 1) / self._options["sampling_radius"] * directions, 0 - ) - ) - return gradient diff --git a/qiskit/algorithms/optimizers/imfil.py b/qiskit/algorithms/optimizers/imfil.py deleted file mode 100644 index 2fca4da2c139..000000000000 --- a/qiskit/algorithms/optimizers/imfil.py +++ /dev/null @@ -1,86 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2019, 2020. -# -# 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. - -"""IMplicit FILtering (IMFIL) optimizer.""" -from __future__ import annotations - -from collections.abc import Callable -from typing import Any - -from qiskit.utils import optionals as _optionals -from .optimizer import Optimizer, OptimizerSupportLevel, OptimizerResult, POINT - - -@_optionals.HAS_SKQUANT.require_in_instance -class IMFIL(Optimizer): - """IMplicit FILtering algorithm. - - Implicit filtering is a way to solve bound-constrained optimization problems for - which derivatives are not available. In comparison to methods that use interpolation to - reconstruct the function and its higher derivatives, implicit filtering builds upon - coordinate search followed by interpolation to get an approximate gradient. - - Uses skquant.opt installed with pip install scikit-quant. - For further detail, please refer to - https://github.com/scikit-quant/scikit-quant and https://qat4chem.lbl.gov/software. - """ - - def __init__( - self, - maxiter: int = 1000, - ) -> None: - """ - Args: - maxiter: Maximum number of function evaluations. - - Raises: - MissingOptionalLibraryError: scikit-quant not installed - """ - super().__init__() - self._maxiter = maxiter - - def get_support_level(self): - """Returns support level dictionary.""" - return { - "gradient": OptimizerSupportLevel.ignored, - "bounds": OptimizerSupportLevel.required, - "initial_point": OptimizerSupportLevel.required, - } - - @property - def settings(self) -> dict[str, Any]: - return { - "maxiter": self._maxiter, - } - - def minimize( - self, - fun: Callable[[POINT], float], - x0: POINT, - jac: Callable[[POINT], POINT] | None = None, - bounds: list[tuple[float, float]] | None = None, - ) -> OptimizerResult: - from skquant import opt as skq - - res, history = skq.minimize( - func=fun, - x0=x0, - bounds=bounds, - budget=self._maxiter, - method="imfil", - ) - - optimizer_result = OptimizerResult() - optimizer_result.x = res.optpar - optimizer_result.fun = res.optval - optimizer_result.nfev = len(history) - return optimizer_result diff --git a/qiskit/algorithms/optimizers/l_bfgs_b.py b/qiskit/algorithms/optimizers/l_bfgs_b.py deleted file mode 100644 index 3c2d7a619ef3..000000000000 --- a/qiskit/algorithms/optimizers/l_bfgs_b.py +++ /dev/null @@ -1,88 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2018, 2020. -# -# 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. - -"""Limited-memory BFGS Bound optimizer.""" - -from __future__ import annotations -from typing import SupportsFloat - -import numpy as np - -from .scipy_optimizer import SciPyOptimizer - - -class L_BFGS_B(SciPyOptimizer): # pylint: disable=invalid-name - """ - Limited-memory BFGS Bound optimizer. - - The target goal of Limited-memory Broyden-Fletcher-Goldfarb-Shanno Bound (L-BFGS-B) - is to minimize the value of a differentiable scalar function :math:`f`. - This optimizer is a quasi-Newton method, meaning that, in contrast to Newtons's method, - it does not require :math:`f`'s Hessian (the matrix of :math:`f`'s second derivatives) - when attempting to compute :math:`f`'s minimum value. - - Like BFGS, L-BFGS is an iterative method for solving unconstrained, non-linear optimization - problems, but approximates BFGS using a limited amount of computer memory. - L-BFGS starts with an initial estimate of the optimal value, and proceeds iteratively - to refine that estimate with a sequence of better estimates. - - The derivatives of :math:`f` are used to identify the direction of steepest descent, - and also to form an estimate of the Hessian matrix (second derivative) of :math:`f`. - L-BFGS-B extends L-BFGS to handle simple, per-variable bound constraints. - - Uses ``scipy.optimize.fmin_l_bfgs_b``. - For further detail, please refer to - https://docs.scipy.org/doc/scipy/reference/optimize.minimize-lbfgsb.html - """ - - _OPTIONS = ["maxfun", "maxiter", "ftol", "iprint", "eps"] - - # pylint: disable=unused-argument - def __init__( - self, - maxfun: int = 15000, - maxiter: int = 15000, - ftol: SupportsFloat = 10 * np.finfo(float).eps, - iprint: int = -1, - eps: float = 1e-08, - options: dict | None = None, - max_evals_grouped: int = 1, - **kwargs, - ): - r""" - Args: - maxfun: Maximum number of function evaluations. - maxiter: Maximum number of iterations. - ftol: The iteration stops when - :math:`(f^k - f^{k+1}) / \max\{|f^k|, |f^{k+1}|,1\} \leq \text{ftol}`. - iprint: Controls the frequency of output. ``iprint < 0`` means no output; - ``iprint = 0`` print only one line at the last iteration; ``0 < iprint < 99`` - print also :math:`f` and :math:`|\text{proj} g|` every iprint iterations; - ``iprint = 99`` print details of every iteration except n-vectors; ``iprint = 100`` - print also the changes of active set and final :math:`x`; ``iprint > 100`` print - details of every iteration including :math:`x` and :math:`g`. - eps: If jac is approximated, use this value for the step size. - options: A dictionary of solver options. - max_evals_grouped: Max number of default gradient evaluations performed simultaneously. - kwargs: additional kwargs for ``scipy.optimize.minimize``. - """ - if options is None: - options = {} - for k, v in list(locals().items()): - if k in self._OPTIONS: - options[k] = v - super().__init__( - method="L-BFGS-B", - options=options, - max_evals_grouped=max_evals_grouped, - **kwargs, - ) diff --git a/qiskit/algorithms/optimizers/nelder_mead.py b/qiskit/algorithms/optimizers/nelder_mead.py deleted file mode 100644 index ff9d8708763f..000000000000 --- a/qiskit/algorithms/optimizers/nelder_mead.py +++ /dev/null @@ -1,73 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2018, 2020. -# -# 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. - -"""Nelder-Mead optimizer.""" -from __future__ import annotations - - -from .scipy_optimizer import SciPyOptimizer - - -class NELDER_MEAD(SciPyOptimizer): # pylint: disable=invalid-name - """ - Nelder-Mead optimizer. - - The Nelder-Mead algorithm performs unconstrained optimization; it ignores bounds - or constraints. It is used to find the minimum or maximum of an objective function - in a multidimensional space. It is based on the Simplex algorithm. Nelder-Mead - is robust in many applications, especially when the first and second derivatives of the - objective function are not known. - - However, if the numerical computation of the derivatives can be trusted to be accurate, - other algorithms using the first and/or second derivatives information might be preferred to - Nelder-Mead for their better performance in the general case, especially in consideration of - the fact that the Nelder–Mead technique is a heuristic search method that can converge to - non-stationary points. - - Uses scipy.optimize.minimize Nelder-Mead. - For further detail, please refer to - See https://docs.scipy.org/doc/scipy/reference/generated/scipy.optimize.minimize.html - """ - - _OPTIONS = ["maxiter", "maxfev", "disp", "xatol", "adaptive"] - - # pylint: disable=unused-argument - def __init__( - self, - maxiter: int | None = None, - maxfev: int = 1000, - disp: bool = False, - xatol: float = 0.0001, - tol: float | None = None, - adaptive: bool = False, - options: dict | None = None, - **kwargs, - ) -> None: - """ - Args: - maxiter: Maximum allowed number of iterations. If both maxiter and maxfev are set, - minimization will stop at the first reached. - maxfev: Maximum allowed number of function evaluations. If both maxiter and - maxfev are set, minimization will stop at the first reached. - disp: Set to True to print convergence messages. - xatol: Absolute error in xopt between iterations that is acceptable for convergence. - tol: Tolerance for termination. - adaptive: Adapt algorithm parameters to dimensionality of problem. - options: A dictionary of solver options. - kwargs: additional kwargs for scipy.optimize.minimize. - """ - if options is None: - options = {} - for k, v in list(locals().items()): - if k in self._OPTIONS: - options[k] = v - super().__init__(method="Nelder-Mead", options=options, tol=tol, **kwargs) diff --git a/qiskit/algorithms/optimizers/nft.py b/qiskit/algorithms/optimizers/nft.py deleted file mode 100644 index 2a7503137daf..000000000000 --- a/qiskit/algorithms/optimizers/nft.py +++ /dev/null @@ -1,170 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2019, 2020. -# -# 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. - -"""Nakanishi-Fujii-Todo algorithm.""" -from __future__ import annotations - - -import numpy as np -from scipy.optimize import OptimizeResult - -from .scipy_optimizer import SciPyOptimizer - - -class NFT(SciPyOptimizer): - """ - Nakanishi-Fujii-Todo algorithm. - - See https://arxiv.org/abs/1903.12166 - """ - - _OPTIONS = ["maxiter", "maxfev", "disp", "reset_interval"] - - # pylint: disable=unused-argument - def __init__( - self, - maxiter: int | None = None, - maxfev: int = 1024, - disp: bool = False, - reset_interval: int = 32, - options: dict | None = None, - **kwargs, - ) -> None: - """ - Built out using scipy framework, for details, please refer to - https://docs.scipy.org/doc/scipy/reference/generated/scipy.optimize.minimize.html. - - Args: - maxiter: Maximum number of iterations to perform. - maxfev: Maximum number of function evaluations to perform. - disp: disp - reset_interval: The minimum estimates directly once - in ``reset_interval`` times. - options: A dictionary of solver options. - kwargs: additional kwargs for scipy.optimize.minimize. - - Notes: - In this optimization method, the optimization function have to satisfy - three conditions written in [1]_. - - References: - .. [1] K. M. Nakanishi, K. Fujii, and S. Todo. 2019. - Sequential minimal optimization for quantum-classical hybrid algorithms. - arXiv preprint arXiv:1903.12166. - """ - if options is None: - options = {} - for k, v in list(locals().items()): - if k in self._OPTIONS: - options[k] = v - super().__init__(method=nakanishi_fujii_todo, options=options, **kwargs) - - -# pylint: disable=invalid-name -def nakanishi_fujii_todo( - fun, x0, args=(), maxiter=None, maxfev=1024, reset_interval=32, eps=1e-32, callback=None, **_ -): - """ - Find the global minimum of a function using the nakanishi_fujii_todo - algorithm [1]. - Args: - fun (callable): ``f(x, *args)`` - Function to be optimized. ``args`` can be passed as an optional item - in the dict ``minimizer_kwargs``. - This function must satisfy the three condition written in Ref. [1]. - x0 (ndarray): shape (n,) - Initial guess. Array of real elements of size (n,), - where 'n' is the number of independent variables. - args (tuple, optional): - Extra arguments passed to the objective function. - maxiter (int): - Maximum number of iterations to perform. - Default: None. - maxfev (int): - Maximum number of function evaluations to perform. - Default: 1024. - reset_interval (int): - The minimum estimates directly once in ``reset_interval`` times. - Default: 32. - eps (float): eps - **_ : additional options - callback (callable, optional): - Called after each iteration. - Returns: - OptimizeResult: - The optimization result represented as a ``OptimizeResult`` object. - Important attributes are: ``x`` the solution array. See - `OptimizeResult` for a description of other attributes. - Notes: - In this optimization method, the optimization function have to satisfy - three conditions written in [2]_. - - References: - .. [2] K. M. Nakanishi, K. Fujii, and S. Todo. 2019. - Sequential minimal optimization for quantum-classical hybrid algorithms. - arXiv preprint arXiv:1903.12166. - """ - - x0 = np.asarray(x0) - recycle_z0 = None - niter = 0 - funcalls = 0 - - while True: - - idx = niter % x0.size - - if reset_interval > 0: - if niter % reset_interval == 0: - recycle_z0 = None - - if recycle_z0 is None: - z0 = fun(np.copy(x0), *args) - funcalls += 1 - else: - z0 = recycle_z0 - - p = np.copy(x0) - p[idx] = x0[idx] + np.pi / 2 - z1 = fun(p, *args) - funcalls += 1 - - p = np.copy(x0) - p[idx] = x0[idx] - np.pi / 2 - z3 = fun(p, *args) - funcalls += 1 - - z2 = z1 + z3 - z0 - c = (z1 + z3) / 2 - a = np.sqrt((z0 - z2) ** 2 + (z1 - z3) ** 2) / 2 - b = np.arctan((z1 - z3) / ((z0 - z2) + eps * (z0 == z2))) + x0[idx] - b += 0.5 * np.pi + 0.5 * np.pi * np.sign((z0 - z2) + eps * (z0 == z2)) - - x0[idx] = b - recycle_z0 = c - a - - niter += 1 - - if callback is not None: - callback(np.copy(x0)) - - if maxfev is not None: - if funcalls >= maxfev: - break - - if maxiter is not None: - if niter >= maxiter: - break - - return OptimizeResult( - fun=fun(np.copy(x0), *args), x=x0, nit=niter, nfev=funcalls, success=(niter > 1) - ) diff --git a/qiskit/algorithms/optimizers/nlopts/__init__.py b/qiskit/algorithms/optimizers/nlopts/__init__.py deleted file mode 100644 index e3165bc0b482..000000000000 --- a/qiskit/algorithms/optimizers/nlopts/__init__.py +++ /dev/null @@ -1,13 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2018, 2022. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. - -"""NLopt based global optimizers""" diff --git a/qiskit/algorithms/optimizers/nlopts/crs.py b/qiskit/algorithms/optimizers/nlopts/crs.py deleted file mode 100644 index 77eb67b298b6..000000000000 --- a/qiskit/algorithms/optimizers/nlopts/crs.py +++ /dev/null @@ -1,35 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2018, 2020. -# -# 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. - -"""Controlled Random Search (CRS) with local mutation optimizer.""" - -from .nloptimizer import NLoptOptimizer, NLoptOptimizerType - - -class CRS(NLoptOptimizer): - """ - Controlled Random Search (CRS) with local mutation optimizer. - - Controlled Random Search (CRS) with local mutation is part of the family of the CRS optimizers. - The CRS optimizers start with a random population of points, and randomly evolve these points - by heuristic rules. In the case of CRS with local mutation, the evolution is a randomized - version of the :class:`NELDER_MEAD` local optimizer. - - - NLopt global optimizer, derivative-free. - For further detail, please refer to - https://nlopt.readthedocs.io/en/latest/NLopt_Algorithms/#controlled-random-search-crs-with-local-mutation - """ - - def get_nlopt_optimizer(self) -> NLoptOptimizerType: - """Return NLopt optimizer type""" - return NLoptOptimizerType.GN_CRS2_LM diff --git a/qiskit/algorithms/optimizers/nlopts/direct_l.py b/qiskit/algorithms/optimizers/nlopts/direct_l.py deleted file mode 100644 index e7ed9be3e25f..000000000000 --- a/qiskit/algorithms/optimizers/nlopts/direct_l.py +++ /dev/null @@ -1,34 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2018, 2020. -# -# 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. - -"""DIviding RECTangles Locally-biased optimizer.""" - -from .nloptimizer import NLoptOptimizer, NLoptOptimizerType - - -class DIRECT_L(NLoptOptimizer): # pylint: disable=invalid-name - """ - DIviding RECTangles Locally-biased optimizer. - - DIviding RECTangles (DIRECT) is a deterministic-search algorithms based on systematic division - of the search domain into increasingly smaller hyper-rectangles. - The DIRECT-L version is a "locally biased" variant of DIRECT that makes the algorithm more - biased towards local search, so that it is more efficient for functions with few local minima. - - NLopt global optimizer, derivative-free. - For further detail, please refer to - http://nlopt.readthedocs.io/en/latest/NLopt_Algorithms/#direct-and-direct-l - """ - - def get_nlopt_optimizer(self) -> NLoptOptimizerType: - """Return NLopt optimizer type""" - return NLoptOptimizerType.GN_DIRECT_L diff --git a/qiskit/algorithms/optimizers/nlopts/direct_l_rand.py b/qiskit/algorithms/optimizers/nlopts/direct_l_rand.py deleted file mode 100644 index 15172ef00880..000000000000 --- a/qiskit/algorithms/optimizers/nlopts/direct_l_rand.py +++ /dev/null @@ -1,32 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2018, 2020. -# -# 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. - -"""DIviding RECTangles Locally-biased Randomized optimizer.""" - -from .nloptimizer import NLoptOptimizer, NLoptOptimizerType - - -class DIRECT_L_RAND(NLoptOptimizer): # pylint: disable=invalid-name - """ - DIviding RECTangles Locally-biased Randomized optimizer. - - DIRECT-L RAND is the "locally biased" variant with some randomization in near-tie decisions. - See also :class:`DIRECT_L` - - NLopt global optimizer, derivative-free. - For further detail, please refer to - http://nlopt.readthedocs.io/en/latest/NLopt_Algorithms/#direct-and-direct-l - """ - - def get_nlopt_optimizer(self) -> NLoptOptimizerType: - """Return NLopt optimizer type""" - return NLoptOptimizerType.GN_DIRECT_L_RAND diff --git a/qiskit/algorithms/optimizers/nlopts/esch.py b/qiskit/algorithms/optimizers/nlopts/esch.py deleted file mode 100644 index 7e754f9447fe..000000000000 --- a/qiskit/algorithms/optimizers/nlopts/esch.py +++ /dev/null @@ -1,33 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2018, 2020. -# -# 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. - -"""ESCH evolutionary optimizer.""" - -from .nloptimizer import NLoptOptimizer, NLoptOptimizerType - - -class ESCH(NLoptOptimizer): - """ - ESCH evolutionary optimizer. - - ESCH is an evolutionary algorithm for global optimization that supports bound constraints only. - Specifically, it does not support nonlinear constraints. - - NLopt global optimizer, derivative-free. - For further detail, please refer to - - http://nlopt.readthedocs.io/en/latest/NLopt_Algorithms/#esch-evolutionary-algorithm - """ - - def get_nlopt_optimizer(self) -> NLoptOptimizerType: - """Return NLopt optimizer type""" - return NLoptOptimizerType.GN_ESCH diff --git a/qiskit/algorithms/optimizers/nlopts/isres.py b/qiskit/algorithms/optimizers/nlopts/isres.py deleted file mode 100644 index 1c37a9401e3a..000000000000 --- a/qiskit/algorithms/optimizers/nlopts/isres.py +++ /dev/null @@ -1,39 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2018, 2020. -# -# 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. - -"""Improved Stochastic Ranking Evolution Strategy optimizer.""" - -from .nloptimizer import NLoptOptimizer, NLoptOptimizerType - - -class ISRES(NLoptOptimizer): - """ - Improved Stochastic Ranking Evolution Strategy optimizer. - - Improved Stochastic Ranking Evolution Strategy (ISRES) is an algorithm for - non-linearly constrained global optimization. It has heuristics to escape local optima, - even though convergence to a global optima is not guaranteed. The evolution strategy is based - on a combination of a mutation rule and differential variation. The fitness ranking is simply - via the objective function for problems without nonlinear constraints. When nonlinear - constraints are included, the `stochastic ranking proposed by Runarsson and Yao - `__ - is employed. This method supports arbitrary nonlinear inequality and equality constraints, in - addition to the bound constraints. - - NLopt global optimizer, derivative-free. - For further detail, please refer to - http://nlopt.readthedocs.io/en/latest/NLopt_Algorithms/#isres-improved-stochastic-ranking-evolution-strategy - """ - - def get_nlopt_optimizer(self) -> NLoptOptimizerType: - """Return NLopt optimizer type""" - return NLoptOptimizerType.GN_ISRES diff --git a/qiskit/algorithms/optimizers/nlopts/nloptimizer.py b/qiskit/algorithms/optimizers/nlopts/nloptimizer.py deleted file mode 100644 index 65f56b930482..000000000000 --- a/qiskit/algorithms/optimizers/nlopts/nloptimizer.py +++ /dev/null @@ -1,131 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2018, 2020. -# -# 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. - -"""Minimize using objective function""" -from __future__ import annotations - -from collections.abc import Callable -from enum import Enum -from abc import abstractmethod -import logging -import numpy as np - -from qiskit.utils import optionals as _optionals -from ..optimizer import Optimizer, OptimizerSupportLevel, OptimizerResult, POINT - -logger = logging.getLogger(__name__) - - -class NLoptOptimizerType(Enum): - """NLopt Valid Optimizer""" - - GN_CRS2_LM = 1 - GN_DIRECT_L_RAND = 2 - GN_DIRECT_L = 3 - GN_ESCH = 4 - GN_ISRES = 5 - - -@_optionals.HAS_NLOPT.require_in_instance -class NLoptOptimizer(Optimizer): - """ - NLopt global optimizer base class - """ - - _OPTIONS = ["max_evals"] - - def __init__(self, max_evals: int = 1000) -> None: # pylint: disable=unused-argument - """ - Args: - max_evals: Maximum allowed number of function evaluations. - - Raises: - MissingOptionalLibraryError: NLopt library not installed. - """ - import nlopt - - super().__init__() - for k, v in list(locals().items()): - if k in self._OPTIONS: - self._options[k] = v - - self._optimizer_names = { - NLoptOptimizerType.GN_CRS2_LM: nlopt.GN_CRS2_LM, - NLoptOptimizerType.GN_DIRECT_L_RAND: nlopt.GN_DIRECT_L_RAND, - NLoptOptimizerType.GN_DIRECT_L: nlopt.GN_DIRECT_L, - NLoptOptimizerType.GN_ESCH: nlopt.GN_ESCH, - NLoptOptimizerType.GN_ISRES: nlopt.GN_ISRES, - } - - @abstractmethod - def get_nlopt_optimizer(self) -> NLoptOptimizerType: - """return NLopt optimizer enum type""" - raise NotImplementedError - - def get_support_level(self): - """return support level dictionary""" - return { - "gradient": OptimizerSupportLevel.ignored, - "bounds": OptimizerSupportLevel.supported, - "initial_point": OptimizerSupportLevel.required, - } - - @property - def settings(self): - return {"max_evals": self._options.get("max_evals", 1000)} - - def minimize( - self, - fun: Callable[[POINT], float], - x0: POINT, - jac: Callable[[POINT], POINT] | None = None, - bounds: list[tuple[float, float]] | None = None, - ) -> OptimizerResult: - import nlopt - - x0 = np.asarray(x0) - - if bounds is None: - bounds = [(None, None)] * x0.size - - threshold = 3 * np.pi - low = [(l if l is not None else -threshold) for (l, u) in bounds] - high = [(u if u is not None else threshold) for (l, u) in bounds] - - name = self._optimizer_names[self.get_nlopt_optimizer()] - opt = nlopt.opt(name, len(low)) - logger.debug(opt.get_algorithm_name()) - - opt.set_lower_bounds(low) - opt.set_upper_bounds(high) - - eval_count = 0 - - def wrap_objfunc_global(x, _grad): - nonlocal eval_count - eval_count += 1 - return fun(x) - - opt.set_min_objective(wrap_objfunc_global) - opt.set_maxeval(self._options.get("max_evals", 1000)) - - xopt = opt.optimize(x0) - minf = opt.last_optimum_value() - - logger.debug("Global minimize found %s eval count %s", minf, eval_count) - - result = OptimizerResult() - result.x = xopt - result.fun = minf - result.nfev = eval_count - - return result diff --git a/qiskit/algorithms/optimizers/optimizer.py b/qiskit/algorithms/optimizers/optimizer.py deleted file mode 100644 index e253167b5d9d..000000000000 --- a/qiskit/algorithms/optimizers/optimizer.py +++ /dev/null @@ -1,389 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2018, 2020. -# -# 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. - -"""Optimizer interface""" - -from __future__ import annotations - -from abc import ABC, abstractmethod -from collections.abc import Callable -from enum import IntEnum -import logging -from typing import Any, Union, Protocol - -import numpy as np -import scipy - -from qiskit.algorithms.algorithm_result import AlgorithmResult - -logger = logging.getLogger(__name__) - -POINT = Union[float, np.ndarray] - - -class OptimizerResult(AlgorithmResult): - """The result of an optimization routine.""" - - def __init__(self) -> None: - super().__init__() - self._x: POINT | None = None - self._fun: float | None = None - self._jac: POINT | None = None - self._nfev: int | None = None - self._njev: int | None = None - self._nit: int | None = None - - @property - def x(self) -> POINT | None: - """The final point of the minimization.""" - return self._x - - @x.setter - def x(self, x: POINT | None) -> None: - """Set the final point of the minimization.""" - self._x = x - - @property - def fun(self) -> float | None: - """The final value of the minimization.""" - return self._fun - - @fun.setter - def fun(self, fun: float | None) -> None: - """Set the final value of the minimization.""" - self._fun = fun - - @property - def jac(self) -> POINT | None: - """The final gradient of the minimization.""" - return self._jac - - @jac.setter - def jac(self, jac: POINT | None) -> None: - """Set the final gradient of the minimization.""" - self._jac = jac - - @property - def nfev(self) -> int | None: - """The total number of function evaluations.""" - return self._nfev - - @nfev.setter - def nfev(self, nfev: int | None) -> None: - """Set the total number of function evaluations.""" - self._nfev = nfev - - @property - def njev(self) -> int | None: - """The total number of gradient evaluations.""" - return self._njev - - @njev.setter - def njev(self, njev: int | None) -> None: - """Set the total number of gradient evaluations.""" - self._njev = njev - - @property - def nit(self) -> int | None: - """The total number of iterations.""" - return self._nit - - @nit.setter - def nit(self, nit: int | None) -> None: - """Set the total number of iterations.""" - self._nit = nit - - -class Minimizer(Protocol): - """Callable Protocol for minimizer. - - This interface is based on `SciPy's optimize module - `__. - - This protocol defines a callable taking the following parameters: - - fun - The objective function to minimize (for example the energy in the case of the VQE). - x0 - The initial point for the optimization. - jac - The gradient of the objective function. - bounds - Parameters bounds for the optimization. Note that these might not be supported - by all optimizers. - - and which returns a minimization result object (either SciPy's or Qiskit's). - """ - - # pylint: disable=invalid-name - def __call__( - self, - fun: Callable[[np.ndarray], float], - x0: np.ndarray, - jac: Callable[[np.ndarray], np.ndarray] | None, - bounds: list[tuple[float, float]] | None, - ) -> scipy.optimize.OptimizeResult | OptimizerResult: - """Minimize the objective function. - - This interface is based on `SciPy's optimize module `__. - - Args: - fun: The objective function to minimize (for example the energy in the case of the VQE). - x0: The initial point for the optimization. - jac: The gradient of the objective function. - bounds: Parameters bounds for the optimization. Note that these might not be supported - by all optimizers. - - Returns: - The minimization result object (either SciPy's or Qiskit's). - """ - ... - - -class OptimizerSupportLevel(IntEnum): - """Support Level enum for features such as bounds, gradient and initial point""" - - # pylint: disable=invalid-name - not_supported = 0 # Does not support the corresponding parameter in optimize() - ignored = 1 # Feature can be passed as non None but will be ignored - supported = 2 # Feature is supported - required = 3 # Feature is required and must be given, None is invalid - - -class Optimizer(ABC): - """Base class for optimization algorithm.""" - - @abstractmethod - def __init__(self): - """ - Initialize the optimization algorithm, setting the support - level for _gradient_support_level, _bound_support_level, - _initial_point_support_level, and empty options. - """ - self._gradient_support_level = self.get_support_level()["gradient"] - self._bounds_support_level = self.get_support_level()["bounds"] - self._initial_point_support_level = self.get_support_level()["initial_point"] - self._options = {} - self._max_evals_grouped = None - - @abstractmethod - def get_support_level(self): - """Return support level dictionary""" - raise NotImplementedError - - def set_options(self, **kwargs): - """ - Sets or updates values in the options dictionary. - - The options dictionary may be used internally by a given optimizer to - pass additional optional values for the underlying optimizer/optimization - function used. The options dictionary may be initially populated with - a set of key/values when the given optimizer is constructed. - - Args: - kwargs (dict): options, given as name=value. - """ - for name, value in kwargs.items(): - self._options[name] = value - logger.debug("options: %s", self._options) - - # pylint: disable=invalid-name - @staticmethod - def gradient_num_diff(x_center, f, epsilon, max_evals_grouped=None): - """ - We compute the gradient with the numeric differentiation in the parallel way, - around the point x_center. - - Args: - x_center (ndarray): point around which we compute the gradient - f (func): the function of which the gradient is to be computed. - epsilon (float): the epsilon used in the numeric differentiation. - max_evals_grouped (int): max evals grouped, defaults to 1 (i.e. no batching). - Returns: - grad: the gradient computed - - """ - if max_evals_grouped is None: # no batching by default - max_evals_grouped = 1 - - forig = f(*((x_center,))) - grad = [] - ei = np.zeros((len(x_center),), float) - todos = [] - for k in range(len(x_center)): - ei[k] = 1.0 - d = epsilon * ei - todos.append(x_center + d) - ei[k] = 0.0 - - counter = 0 - chunk = [] - chunks = [] - length = len(todos) - # split all points to chunks, where each chunk has batch_size points - for i in range(length): - x = todos[i] - chunk.append(x) - counter += 1 - # the last one does not have to reach batch_size - if counter == max_evals_grouped or i == length - 1: - chunks.append(chunk) - chunk = [] - counter = 0 - - for chunk in chunks: # eval the chunks in order - parallel_parameters = np.concatenate(chunk) - todos_results = f(parallel_parameters) # eval the points in a chunk (order preserved) - if isinstance(todos_results, float): - grad.append((todos_results - forig) / epsilon) - else: - for todor in todos_results: - grad.append((todor - forig) / epsilon) - - return np.array(grad) - - @staticmethod - def wrap_function(function, args): - """ - Wrap the function to implicitly inject the args at the call of the function. - - Args: - function (func): the target function - args (tuple): the args to be injected - Returns: - function_wrapper: wrapper - """ - - def function_wrapper(*wrapper_args): - return function(*(wrapper_args + args)) - - return function_wrapper - - @property - def setting(self): - """Return setting""" - ret = f"Optimizer: {self.__class__.__name__}\n" - params = "" - for key, value in self.__dict__.items(): - if key[0] == "_": - params += f"-- {key[1:]}: {value}\n" - ret += f"{params}" - return ret - - @property - def settings(self) -> dict[str, Any]: - """The optimizer settings in a dictionary format. - - The settings can for instance be used for JSON-serialization (if all settings are - serializable, which e.g. doesn't hold per default for callables), such that the - optimizer object can be reconstructed as - - .. code-block:: - - settings = optimizer.settings - # JSON serialize and send to another server - optimizer = OptimizerClass(**settings) - - """ - raise NotImplementedError("The settings method is not implemented per default.") - - @abstractmethod - def minimize( - self, - fun: Callable[[POINT], float], - x0: POINT, - jac: Callable[[POINT], POINT] | None = None, - bounds: list[tuple[float, float]] | None = None, - ) -> OptimizerResult: - """Minimize the scalar function. - - Args: - fun: The scalar function to minimize. - x0: The initial point for the minimization. - jac: The gradient of the scalar function ``fun``. - bounds: Bounds for the variables of ``fun``. This argument might be ignored if the - optimizer does not support bounds. - - Returns: - The result of the optimization, containing e.g. the result as attribute ``x``. - """ - raise NotImplementedError() - - @property - def gradient_support_level(self): - """Returns gradient support level""" - return self._gradient_support_level - - @property - def is_gradient_ignored(self): - """Returns is gradient ignored""" - return self._gradient_support_level == OptimizerSupportLevel.ignored - - @property - def is_gradient_supported(self): - """Returns is gradient supported""" - return self._gradient_support_level != OptimizerSupportLevel.not_supported - - @property - def is_gradient_required(self): - """Returns is gradient required""" - return self._gradient_support_level == OptimizerSupportLevel.required - - @property - def bounds_support_level(self): - """Returns bounds support level""" - return self._bounds_support_level - - @property - def is_bounds_ignored(self): - """Returns is bounds ignored""" - return self._bounds_support_level == OptimizerSupportLevel.ignored - - @property - def is_bounds_supported(self): - """Returns is bounds supported""" - return self._bounds_support_level != OptimizerSupportLevel.not_supported - - @property - def is_bounds_required(self): - """Returns is bounds required""" - return self._bounds_support_level == OptimizerSupportLevel.required - - @property - def initial_point_support_level(self): - """Returns initial point support level""" - return self._initial_point_support_level - - @property - def is_initial_point_ignored(self): - """Returns is initial point ignored""" - return self._initial_point_support_level == OptimizerSupportLevel.ignored - - @property - def is_initial_point_supported(self): - """Returns is initial point supported""" - return self._initial_point_support_level != OptimizerSupportLevel.not_supported - - @property - def is_initial_point_required(self): - """Returns is initial point required""" - return self._initial_point_support_level == OptimizerSupportLevel.required - - def print_options(self): - """Print algorithm-specific options.""" - for name in sorted(self._options): - logger.debug("%s = %s", name, str(self._options[name])) - - def set_max_evals_grouped(self, limit): - """Set max evals grouped""" - self._max_evals_grouped = limit diff --git a/qiskit/algorithms/optimizers/optimizer_utils/__init__.py b/qiskit/algorithms/optimizers/optimizer_utils/__init__.py deleted file mode 100644 index 33c5bc90b087..000000000000 --- a/qiskit/algorithms/optimizers/optimizer_utils/__init__.py +++ /dev/null @@ -1,27 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2022. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. -"""Utils for optimizers - -Optimizer Utils (:mod:`qiskit.algorithms.optimizers.optimizer_utils`) -===================================================================== - -.. autosummary:: - :toctree: ../stubs/ - :nosignatures: - - LearningRate - -""" - -from .learning_rate import LearningRate - -__all__ = ["LearningRate"] diff --git a/qiskit/algorithms/optimizers/optimizer_utils/learning_rate.py b/qiskit/algorithms/optimizers/optimizer_utils/learning_rate.py deleted file mode 100644 index 7bfea636ce2c..000000000000 --- a/qiskit/algorithms/optimizers/optimizer_utils/learning_rate.py +++ /dev/null @@ -1,88 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2021, 2022. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. - -"""A class to represent the Learning Rate.""" -from __future__ import annotations - -from collections.abc import Generator, Callable -from itertools import tee -import numpy as np - - -class LearningRate(Generator): - """Represents a Learning Rate. - Will be an attribute of :class:`~.GradientDescentState`. Note that :class:`~.GradientDescent` also - has a learning rate. That learning rate can be a float, a list, an array, a function returning - a generator and will be used to create a generator to be used during the - optimization process. - This class wraps ``Generator`` so that we can also access the last yielded value. - """ - - def __init__( - self, - learning_rate: float - | list[float] - | np.ndarray - | Callable[[], Generator[float, None, None]], - ): - """ - Args: - learning_rate: Used to create a generator to iterate on. - """ - if isinstance(learning_rate, (float, int)): - self._gen = constant(learning_rate) - elif isinstance(learning_rate, Generator): - learning_rate, self._gen = tee(learning_rate) - elif isinstance(learning_rate, (list, np.ndarray)): - self._gen = (eta for eta in learning_rate) - else: - self._gen = learning_rate() - - self._current: float | None = None - - def send(self, value): - """Send a value into the generator. - Return next yielded value or raise StopIteration. - """ - self._current = next(self._gen) - return self.current - - def throw(self, typ, val=None, tb=None): - """Raise an exception in the generator. - Return next yielded value or raise StopIteration. - """ - if val is None: - if tb is None: - raise typ - val = typ() - if tb is not None: - val = val.with_traceback(tb) - raise val - - @property - def current(self): - """Returns the current value of the learning rate.""" - return self._current - - -def constant(learning_rate: float = 0.01) -> Generator[float, None, None]: - """Returns a python generator that always yields the same value. - - Args: - learning_rate: The value to yield. - - Yields: - The learning rate for the next iteration. - """ - - while True: - yield learning_rate diff --git a/qiskit/algorithms/optimizers/p_bfgs.py b/qiskit/algorithms/optimizers/p_bfgs.py deleted file mode 100644 index f166160d98de..000000000000 --- a/qiskit/algorithms/optimizers/p_bfgs.py +++ /dev/null @@ -1,182 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2018, 2021. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. - -"""Parallelized Limited-memory BFGS optimizer""" -from __future__ import annotations - -import logging -import multiprocessing -import platform -import warnings -from collections.abc import Callable -from typing import SupportsFloat - -import numpy as np - -from qiskit.utils import algorithm_globals -from qiskit.utils.validation import validate_min - -from .optimizer import OptimizerResult, POINT -from .scipy_optimizer import SciPyOptimizer - -logger = logging.getLogger(__name__) - - -class P_BFGS(SciPyOptimizer): # pylint: disable=invalid-name - """ - Parallelized Limited-memory BFGS optimizer. - - P-BFGS is a parallelized version of :class:`L_BFGS_B` with which it shares the same parameters. - P-BFGS can be useful when the target hardware is a quantum simulator running on a classical - machine. This allows the multiple processes to use simulation to potentially reach a minimum - faster. The parallelization may also help the optimizer avoid getting stuck at local optima. - - Uses scipy.optimize.fmin_l_bfgs_b. - For further detail, please refer to - https://docs.scipy.org/doc/scipy/reference/generated/scipy.optimize.fmin_l_bfgs_b.html - """ - - _OPTIONS = ["maxfun", "ftol", "iprint"] - - # pylint: disable=unused-argument - def __init__( - self, - maxfun: int = 1000, - ftol: SupportsFloat = 10 * np.finfo(float).eps, - iprint: int = -1, - max_processes: int | None = None, - options: dict | None = None, - max_evals_grouped: int = 1, - **kwargs, - ) -> None: - r""" - Args: - maxfun: Maximum number of function evaluations. - ftol: The iteration stops when (f\^k - f\^{k+1})/max{\|f\^k\|,\|f\^{k+1}\|,1} <= ftol. - iprint: Controls the frequency of output. iprint < 0 means no output; - iprint = 0 print only one line at the last iteration; 0 < iprint < 99 - print also f and \|proj g\| every iprint iterations; iprint = 99 print - details of every iteration except n-vectors; iprint = 100 print also the - changes of active set and final x; iprint > 100 print details of - every iteration including x and g. - max_processes: maximum number of processes allowed, has a min. value of 1 if not None. - options: A dictionary of solver options. - max_evals_grouped: Max number of default gradient evaluations performed simultaneously. - kwargs: additional kwargs for scipy.optimize.minimize. - """ - if max_processes: - with warnings.catch_warnings(): - warnings.filterwarnings("ignore", category=DeprecationWarning) - validate_min("max_processes", max_processes, 1) - - if options is None: - options = {} - for k, v in list(locals().items()): - if k in self._OPTIONS: - options[k] = v - super().__init__( - method="L-BFGS-B", - options=options, - max_evals_grouped=max_evals_grouped, - **kwargs, - ) - self._max_processes = max_processes - - def minimize( - self, - fun: Callable[[POINT], float], - x0: POINT, - jac: Callable[[POINT], POINT] | None = None, - bounds: list[tuple[float, float]] | None = None, - ) -> OptimizerResult: - x0 = np.asarray(x0) - - num_procs = multiprocessing.cpu_count() - 1 - num_procs = ( - num_procs if self._max_processes is None else min(num_procs, self._max_processes) - ) - num_procs = num_procs if num_procs >= 0 else 0 - - if platform.system() == "Darwin": - # Changed in version 3.8: On macOS, the spawn start method is now the - # default. The fork start method should be considered unsafe as it can - # lead to crashes. - # However P_BFGS doesn't support spawn, so we revert to single process. - num_procs = 0 - logger.warning( - "For MacOS, python >= 3.8, using only current process. " - "Multiple core use not supported." - ) - elif platform.system() == "Windows": - num_procs = 0 - logger.warning( - "For Windows, using only current process. Multiple core use not supported." - ) - - queue: multiprocessing.queues.Queue[tuple[POINT, float, int]] = multiprocessing.Queue() - - # TODO: are automatic bounds a good idea? What if the circuit parameters are not - # just from plain Pauli rotations but have a coefficient? - - # bounds for additional initial points in case bounds has any None values - threshold = 2 * np.pi - if bounds is None: - bounds = [(-threshold, threshold)] * x0.size - low = [(l if l is not None else -threshold) for (l, u) in bounds] - high = [(u if u is not None else threshold) for (l, u) in bounds] - - def optimize_runner(_queue, _i_pt): # Multi-process sampling - _sol, _opt, _nfev = self._optimize(fun, _i_pt, jac, bounds) - _queue.put((_sol, _opt, _nfev)) - - # Start off as many other processes running the optimize (can be 0) - processes = [] - for _ in range(num_procs): - with warnings.catch_warnings(): - warnings.filterwarnings("ignore", category=DeprecationWarning) - i_pt = algorithm_globals.random.uniform(low, high) # Another random point in bounds - proc = multiprocessing.Process(target=optimize_runner, args=(queue, i_pt)) - processes.append(proc) - proc.start() - - # While the one optimize in this process below runs the other processes will - # be running too. This one runs - # with the supplied initial point. The process ones have their own random one - sol, opt, nfev = self._optimize(fun, x0, jac, bounds) - - for proc in processes: - # For each other process we wait now for it to finish and see if it has - # a better result than above - proc.join() - p_sol, p_opt, p_nfev = queue.get() - if p_opt < opt: - sol, opt = p_sol, p_opt - nfev += p_nfev - - result = OptimizerResult() - result.x = sol - result.fun = opt - result.nfev = nfev - - return result - - def _optimize( - self, - objective_function, - initial_point, - gradient_function=None, - variable_bounds=None, - ) -> tuple[POINT, float, int]: - result = super().minimize( - objective_function, initial_point, gradient_function, variable_bounds - ) - return result.x, result.fun, result.nfev diff --git a/qiskit/algorithms/optimizers/powell.py b/qiskit/algorithms/optimizers/powell.py deleted file mode 100644 index de8cbb1b9d18..000000000000 --- a/qiskit/algorithms/optimizers/powell.py +++ /dev/null @@ -1,64 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2018, 2020. -# -# 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. - -"""Powell optimizer.""" -from __future__ import annotations - -from .scipy_optimizer import SciPyOptimizer - - -class POWELL(SciPyOptimizer): - """ - Powell optimizer. - - The Powell algorithm performs unconstrained optimization; it ignores bounds or - constraints. Powell is a *conjugate direction method*: it performs sequential one-dimensional - minimization along each directional vector, which is updated at - each iteration of the main minimization loop. The function being minimized need not be - differentiable, and no derivatives are taken. - - Uses scipy.optimize.minimize Powell. - For further detail, please refer to - See https://docs.scipy.org/doc/scipy/reference/generated/scipy.optimize.minimize.html - """ - - _OPTIONS = ["maxiter", "maxfev", "disp", "xtol"] - - # pylint: disable=unused-argument - def __init__( - self, - maxiter: int | None = None, - maxfev: int = 1000, - disp: bool = False, - xtol: float = 0.0001, - tol: float | None = None, - options: dict | None = None, - **kwargs, - ) -> None: - """ - Args: - maxiter: Maximum allowed number of iterations. If both maxiter and maxfev - are set, minimization will stop at the first reached. - maxfev: Maximum allowed number of function evaluations. If both maxiter and - maxfev are set, minimization will stop at the first reached. - disp: Set to True to print convergence messages. - xtol: Relative error in solution xopt acceptable for convergence. - tol: Tolerance for termination. - options: A dictionary of solver options. - kwargs: additional kwargs for scipy.optimize.minimize. - """ - if options is None: - options = {} - for k, v in list(locals().items()): - if k in self._OPTIONS: - options[k] = v - super().__init__("Powell", options=options, tol=tol, **kwargs) diff --git a/qiskit/algorithms/optimizers/qnspsa.py b/qiskit/algorithms/optimizers/qnspsa.py deleted file mode 100644 index 44fa33d8a9a0..000000000000 --- a/qiskit/algorithms/optimizers/qnspsa.py +++ /dev/null @@ -1,423 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2021. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. - -"""The QN-SPSA optimizer.""" - -from __future__ import annotations - -from collections.abc import Iterator -from typing import Any, Callable - -import numpy as np -from qiskit.providers import Backend -from qiskit.circuit import ParameterVector, QuantumCircuit -from qiskit.opflow import StateFn, CircuitSampler, ExpectationBase -from qiskit.utils import QuantumInstance -from qiskit.utils.deprecation import deprecate_arg - -from qiskit.primitives import BaseSampler, Sampler -from qiskit.algorithms.state_fidelities import ComputeUncompute - -from .spsa import SPSA, CALLBACK, TERMINATIONCHECKER, _batch_evaluate - -# the function to compute the fidelity -FIDELITY = Callable[[np.ndarray, np.ndarray], float] - - -class QNSPSA(SPSA): - r"""The Quantum Natural SPSA (QN-SPSA) optimizer. - - The QN-SPSA optimizer [1] is a stochastic optimizer that belongs to the family of gradient - descent methods. This optimizer is based on SPSA but attempts to improve the convergence by - sampling the **natural gradient** instead of the vanilla, first-order gradient. It achieves - this by approximating Hessian of the ``fidelity`` of the ansatz circuit. - - Compared to natural gradients, which require :math:`\mathcal{O}(d^2)` expectation value - evaluations for a circuit with :math:`d` parameters, QN-SPSA only requires - :math:`\mathcal{O}(1)` and can therefore significantly speed up the natural gradient calculation - by sacrificing some accuracy. Compared to SPSA, QN-SPSA requires 4 additional function - evaluations of the fidelity. - - The stochastic approximation of the natural gradient can be systematically improved by - increasing the number of ``resamplings``. This leads to a Monte Carlo-style convergence to - the exact, analytic value. - - .. note:: - - 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.utils.algorithm_globals.random_seed = seed``). - - Examples: - - This short example runs QN-SPSA for the ground state calculation of the ``Z ^ Z`` - observable where the ansatz is a ``PauliTwoDesign`` circuit. - - .. code-block:: python - - import numpy as np - from qiskit.algorithms.optimizers import QNSPSA - from qiskit.circuit.library import PauliTwoDesign - from qiskit.primitives import Estimator, Sampler - from qiskit.quantum_info import Pauli - - # problem setup - ansatz = PauliTwoDesign(2, reps=1, seed=2) - observable = Pauli("ZZ") - initial_point = np.random.random(ansatz.num_parameters) - - # loss function - estimator = Estimator() - - def loss(x): - result = estimator.run([ansatz], [observable], [x]).result() - return np.real(result.values[0]) - - # fidelity for estimation of the geometric tensor - sampler = Sampler() - fidelity = QNSPSA.get_fidelity(ansatz, sampler) - - # run QN-SPSA - qnspsa = QNSPSA(fidelity, maxiter=300) - result = qnspsa.optimize(ansatz.num_parameters, loss, initial_point=initial_point) - - This is a legacy version solving the same problem but using Qiskit Opflow instead - of the Qiskit Primitives. Note however, that this usage is deprecated. - - .. code-block:: python - - import numpy as np - from qiskit.algorithms.optimizers import QNSPSA - from qiskit.circuit.library import PauliTwoDesign - from qiskit.opflow import Z, StateFn - - ansatz = PauliTwoDesign(2, reps=1, seed=2) - observable = Z ^ Z - initial_point = np.random.random(ansatz.num_parameters) - - def loss(x): - bound = ansatz.assign_parameters(x) - return np.real((StateFn(observable, is_measurement=True) @ StateFn(bound)).eval()) - - fidelity = QNSPSA.get_fidelity(ansatz) - qnspsa = QNSPSA(fidelity, maxiter=300) - result = qnspsa.optimize(ansatz.num_parameters, loss, initial_point=initial_point) - - - References: - - [1] J. Gacon et al, "Simultaneous Perturbation Stochastic Approximation of the Quantum - Fisher Information", `arXiv:2103.09232 `_ - - """ - - def __init__( - self, - fidelity: FIDELITY, - maxiter: int = 100, - blocking: bool = True, - allowed_increase: float | None = None, - learning_rate: float | Callable[[], Iterator] | None = None, - perturbation: float | Callable[[], Iterator] | None = None, - resamplings: int | dict[int, int] = 1, - perturbation_dims: int | None = None, - regularization: float | None = None, - hessian_delay: int = 0, - lse_solver: Callable[[np.ndarray, np.ndarray], np.ndarray] | None = None, - initial_hessian: np.ndarray | None = None, - callback: CALLBACK | None = None, - termination_checker: TERMINATIONCHECKER | None = None, - ) -> None: - r""" - Args: - fidelity: A function to compute the fidelity of the ansatz state with itself for - two different sets of parameters. - maxiter: The maximum number of iterations. Note that this is not the maximal number - of function evaluations. - blocking: If True, only accepts updates that improve the loss (up to some allowed - increase, see next argument). - allowed_increase: If ``blocking`` is ``True``, this argument determines by how much - the loss can increase with the proposed parameters and still be accepted. - If ``None``, the allowed increases is calibrated automatically to be twice the - approximated standard deviation of the loss function. - learning_rate: The update step is the learning rate is multiplied with the gradient. - If the learning rate is a float, it remains constant over the course of the - optimization. It can also be a callable returning an iterator which yields the - learning rates for each optimization step. - If ``learning_rate`` is set ``perturbation`` must also be provided. - perturbation: Specifies the magnitude of the perturbation for the finite difference - approximation of the gradients. Can be either a float or a generator yielding - the perturbation magnitudes per step. - If ``perturbation`` is set ``learning_rate`` must also be provided. - resamplings: The number of times the gradient (and Hessian) is sampled using a random - direction to construct a gradient estimate. Per default the gradient is estimated - using only one random direction. If an integer, all iterations use the same number - of resamplings. If a dictionary, this is interpreted as - ``{iteration: number of resamplings per iteration}``. - perturbation_dims: The number of perturbed dimensions. Per default, all dimensions - are perturbed, but a smaller, fixed number can be perturbed. If set, the perturbed - dimensions are chosen uniformly at random. - regularization: To ensure the preconditioner is symmetric and positive definite, the - identity times a small coefficient is added to it. This generator yields that - coefficient. - hessian_delay: Start multiplying the gradient with the inverse Hessian only after a - certain number of iterations. The Hessian is still evaluated and therefore this - argument can be useful to first get a stable average over the last iterations before - using it as preconditioner. - lse_solver: The method to solve for the inverse of the Hessian. Per default an - exact LSE solver is used, but can e.g. be overwritten by a minimization routine. - initial_hessian: The initial guess for the Hessian. By default the identity matrix - is used. - callback: A callback function passed information in each iteration step. The - information is, in this order: the parameters, the function value, the number - of function evaluations, the stepsize, whether the step was accepted. - termination_checker: A callback function executed at the end of each iteration step. The - arguments are, in this order: the parameters, the function value, the number - of function evaluations, the stepsize, whether the step was accepted. If the callback - returns True, the optimization is terminated. - To prevent additional evaluations of the objective method, if the objective has not yet - been evaluated, the objective is estimated by taking the mean of the objective - evaluations used in the estimate of the gradient. - - - """ - super().__init__( - maxiter, - blocking, - allowed_increase, - # trust region *must* be false for natural gradients to work - trust_region=False, - learning_rate=learning_rate, - perturbation=perturbation, - resamplings=resamplings, - callback=callback, - second_order=True, - hessian_delay=hessian_delay, - lse_solver=lse_solver, - regularization=regularization, - perturbation_dims=perturbation_dims, - initial_hessian=initial_hessian, - termination_checker=termination_checker, - ) - - self.fidelity = fidelity - - def _point_sample(self, loss, x, eps, delta1, delta2): - loss_points = [x + eps * delta1, x - eps * delta1] - fidelity_points = [ - (x, x + eps * delta1), - (x, x - eps * delta1), - (x, x + eps * (delta1 + delta2)), - (x, x + eps * (-delta1 + delta2)), - ] - self._nfev += 6 - - loss_values = _batch_evaluate(loss, loss_points, self._max_evals_grouped) - fidelity_values = _batch_evaluate( - self.fidelity, fidelity_points, self._max_evals_grouped, unpack_points=True - ) - - # compute the gradient approximation and additionally return the loss function evaluations - gradient_estimate = (loss_values[0] - loss_values[1]) / (2 * eps) * delta1 - - # compute the preconditioner point estimate - fidelity_values = np.asarray(fidelity_values, dtype=float) - diff = fidelity_values[2] - fidelity_values[0] - diff = diff - (fidelity_values[3] - fidelity_values[1]) - diff = diff / (2 * eps**2) - - rank_one = np.outer(delta1, delta2) - # -0.5 factor comes from the fact that we need -0.5 * fidelity - hessian_estimate = -0.5 * diff * (rank_one + rank_one.T) / 2 - - return np.mean(loss_values), gradient_estimate, hessian_estimate - - @property - def settings(self) -> dict[str, Any]: - """The optimizer settings in a dictionary format.""" - # re-use serialization from SPSA - settings = super().settings - settings.update({"fidelity": self.fidelity}) - - # remove SPSA-specific arguments not in QNSPSA - settings.pop("trust_region") - settings.pop("second_order") - - return settings - - @staticmethod - @deprecate_arg( - "backend", - since="0.24.0", - package_name="qiskit-terra", - additional_msg="See https://qisk.it/algo_migration for a migration guide.", - # We allow passing a sampler as the second argument because that will become a positional - # argument for `sampler` after removing `backend` and `expectation`. - predicate=lambda backend: not isinstance(backend, BaseSampler), - ) - @deprecate_arg( - "expectation", - since="0.24.0", - package_name="qiskit-terra", - additional_msg="See https://qisk.it/algo_migration for a migration guide.", - ) - def get_fidelity( - circuit: QuantumCircuit, - backend: Backend | QuantumInstance | None = None, - expectation: ExpectationBase | None = None, - *, - sampler: BaseSampler | None = None, - ) -> Callable[[np.ndarray, np.ndarray], float]: - r"""Get a function to compute the fidelity of ``circuit`` with itself. - - .. note:: - - Using this function with a backend and expectation converter is pending deprecation, - instead pass a Qiskit Primitive sampler, such as :class:`~.Sampler`. - The sampler can be passed as keyword argument or, positionally, as second argument. - - Let ``circuit`` be a parameterized quantum circuit performing the operation - :math:`U(\theta)` given a set of parameters :math:`\theta`. Then this method returns - a function to evaluate - - .. math:: - - F(\theta, \phi) = \big|\langle 0 | U^\dagger(\theta) U(\phi) |0\rangle \big|^2. - - The output of this function can be used as input for the ``fidelity`` to the - :class:`~.QNSPSA` optimizer. - - Args: - circuit: The circuit preparing the parameterized ansatz. - backend: Deprecated. A backend of quantum instance to evaluate the circuits. - If None, plain matrix multiplication will be used. - expectation: Deprecated. An expectation converter to specify how the expected - value is computed. If a shot-based readout is used this should be set to - ``PauliExpectation``. - sampler: A sampler primitive to sample from a quantum state. - - Returns: - A handle to the function :math:`F`. - - """ - # allow passing sampler by position - if isinstance(backend, BaseSampler): - sampler = backend - backend = None - - if expectation is None and backend is None and sampler is None: - sampler = Sampler() - - if expectation is not None or backend is not None: - return QNSPSA._legacy_get_fidelity(circuit, backend, expectation) - - fid = ComputeUncompute(sampler) - - num_parameters = circuit.num_parameters - - def fidelity(values_x, values_y): - values_x = np.reshape(values_x, (-1, num_parameters)).tolist() - batch_size_x = len(values_x) - - values_y = np.reshape(values_y, (-1, num_parameters)).tolist() - batch_size_y = len(values_y) - - result = fid.run( - batch_size_x * [circuit], batch_size_y * [circuit], values_x, values_y - ).result() - return np.asarray(result.fidelities) - - return fidelity - - @staticmethod - def _legacy_get_fidelity( - circuit: QuantumCircuit, - backend: Backend | QuantumInstance | None = None, - expectation: ExpectationBase | None = None, - ) -> Callable[[np.ndarray, np.ndarray], float]: - r"""Deprecated. Get a function to compute the fidelity of ``circuit`` with itself. - - .. note:: - - This method is deprecated. Instead use the :class:`~.ComputeUncompute` - class which implements the fidelity calculation in the same fashion as this method. - - Let ``circuit`` be a parameterized quantum circuit performing the operation - :math:`U(\theta)` given a set of parameters :math:`\theta`. Then this method returns - a function to evaluate - - .. math:: - - F(\theta, \phi) = \big|\langle 0 | U^\dagger(\theta) U(\phi) |0\rangle \big|^2. - - The output of this function can be used as input for the ``fidelity`` to the - :class:~`qiskit.algorithms.optimizers.QNSPSA` optimizer. - - Args: - circuit: The circuit preparing the parameterized ansatz. - backend: A backend of quantum instance to evaluate the circuits. If None, plain - matrix multiplication will be used. - expectation: An expectation converter to specify how the expected value is computed. - If a shot-based readout is used this should be set to ``PauliExpectation``. - - Returns: - A handle to the function :math:`F`. - - """ - params_x = ParameterVector("x", circuit.num_parameters) - params_y = ParameterVector("y", circuit.num_parameters) - - expression = ~StateFn(circuit.assign_parameters(params_x)) @ StateFn( - circuit.assign_parameters(params_y) - ) - - if expectation is not None: - expression = expectation.convert(expression) - - if backend is None: - - def fidelity(values_x, values_y): - value_dict = dict( - zip(params_x[:] + params_y[:], values_x.tolist() + values_y.tolist()) - ) - return np.abs(expression.assign_parameters(value_dict).eval()) ** 2 - - else: - sampler = CircuitSampler(backend) - - def fidelity(values_x, values_y=None): - # no batches - if isinstance(values_x, np.ndarray) and isinstance(values_y, np.ndarray): - value_dict = dict( - zip(params_x[:] + params_y[:], values_x.tolist() + values_y.tolist()) - ) - # legacy batching -- remove once QNSPSA.get_fidelity is only supported with sampler - elif values_y is None: - value_dict = {p: [] for p in params_x[:] + params_y[:]} - for values_xy in values_x: - for value_x, param_x in zip(values_xy[0, :], params_x): - value_dict[param_x].append(value_x) - - for value_y, param_y in zip(values_xy[1, :], params_y): - value_dict[param_y].append(value_y) - else: - value_dict = {p: [] for p in params_x[:] + params_y[:]} - for values_i_x, values_i_y in zip(values_x, values_y): - for value_x, param_x in zip(values_i_x, params_x): - value_dict[param_x].append(value_x) - - for value_y, param_y in zip(values_i_y, params_y): - value_dict[param_y].append(value_y) - - return np.abs(sampler.convert(expression, params=value_dict).eval()) ** 2 - - return fidelity diff --git a/qiskit/algorithms/optimizers/scipy_optimizer.py b/qiskit/algorithms/optimizers/scipy_optimizer.py deleted file mode 100644 index 3a22b41bcfbd..000000000000 --- a/qiskit/algorithms/optimizers/scipy_optimizer.py +++ /dev/null @@ -1,183 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2018, 2020. -# -# 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. - -"""Wrapper class of scipy.optimize.minimize.""" -from __future__ import annotations - -import warnings -from collections.abc import Callable -from typing import Any - -import numpy as np -from scipy.optimize import minimize - -from qiskit.utils.validation import validate_min - -from .optimizer import Optimizer, OptimizerSupportLevel, OptimizerResult, POINT - - -class SciPyOptimizer(Optimizer): - """A general Qiskit Optimizer wrapping scipy.optimize.minimize. - - For further detail, please refer to - https://docs.scipy.org/doc/scipy/reference/generated/scipy.optimize.minimize.html - """ - - _bounds_support_methods = {"l-bfgs-b", "tnc", "slsqp", "powell", "trust-constr"} - _gradient_support_methods = { - "cg", - "bfgs", - "newton-cg", - "l-bfgs-b", - "tnc", - "slsqp", - "dogleg", - "trust-ncg", - "trust-krylov", - "trust-exact", - "trust-constr", - } - - def __init__( - self, - method: str | Callable, - options: dict[str, Any] | None = None, - max_evals_grouped: int = 1, - **kwargs, - ): - """ - Args: - method: Type of solver. - options: A dictionary of solver options. - kwargs: additional kwargs for scipy.optimize.minimize. - max_evals_grouped: Max number of default gradient evaluations performed simultaneously. - """ - self._method = method.lower() if isinstance(method, str) else method - # Set support level - if self._method in self._bounds_support_methods: - self._bounds_support_level = OptimizerSupportLevel.supported - else: - self._bounds_support_level = OptimizerSupportLevel.ignored - if self._method in self._gradient_support_methods: - self._gradient_support_level = OptimizerSupportLevel.supported - else: - self._gradient_support_level = OptimizerSupportLevel.ignored - self._initial_point_support_level = OptimizerSupportLevel.required - - self._options = options if options is not None else {} - - with warnings.catch_warnings(): - warnings.filterwarnings("ignore", category=DeprecationWarning) - validate_min("max_evals_grouped", max_evals_grouped, 1) - - self._max_evals_grouped = max_evals_grouped - self._kwargs = kwargs - - def get_support_level(self): - """Return support level dictionary""" - return { - "gradient": self._gradient_support_level, - "bounds": self._bounds_support_level, - "initial_point": self._initial_point_support_level, - } - - @property - def settings(self) -> dict[str, Any]: - options = self._options.copy() - if hasattr(self, "_OPTIONS"): - # all _OPTIONS should be keys in self._options, but add a failsafe here - attributes = [ - option - for option in self._OPTIONS # pylint: disable=no-member - if option in options.keys() - ] - - settings = {attr: options.pop(attr) for attr in attributes} - else: - settings = {} - - settings["max_evals_grouped"] = self._max_evals_grouped - settings["options"] = options - settings.update(self._kwargs) - - # the subclasses don't need the "method" key as the class type specifies the method - if self.__class__ == SciPyOptimizer: - settings["method"] = self._method - - return settings - - def minimize( - self, - fun: Callable[[POINT], float], - x0: POINT, - jac: Callable[[POINT], POINT] | None = None, - bounds: list[tuple[float, float]] | None = None, - ) -> OptimizerResult: - # Remove ignored parameters to suppress the warning of scipy.optimize.minimize - if self.is_bounds_ignored: - bounds = None - if self.is_gradient_ignored: - jac = None - - if self.is_gradient_supported and jac is None and self._max_evals_grouped > 1: - if "eps" in self._options: - epsilon = self._options["eps"] - else: - epsilon = ( - 1e-8 if self._method in {"l-bfgs-b", "tnc"} else np.sqrt(np.finfo(float).eps) - ) - jac = Optimizer.wrap_function( - Optimizer.gradient_num_diff, (fun, epsilon, self._max_evals_grouped) - ) - - # Workaround for L_BFGS_B because it does not accept np.ndarray. - # See https://github.com/Qiskit/qiskit-terra/pull/6373. - if jac is not None and self._method == "l-bfgs-b": - jac = self._wrap_gradient(jac) - - # Starting in scipy 1.9.0 maxiter is deprecated and maxfun (added in 1.5.0) - # should be used instead - swapped_deprecated_args = False - if self._method == "tnc" and "maxiter" in self._options: - swapped_deprecated_args = True - self._options["maxfun"] = self._options.pop("maxiter") - - raw_result = minimize( - fun=fun, - x0=x0, - method=self._method, - jac=jac, - bounds=bounds, - options=self._options, - **self._kwargs, - ) - if swapped_deprecated_args: - self._options["maxiter"] = self._options.pop("maxfun") - - result = OptimizerResult() - result.x = raw_result.x - result.fun = raw_result.fun - result.nfev = raw_result.nfev - result.njev = raw_result.get("njev", None) - result.nit = raw_result.get("nit", None) - - return result - - @staticmethod - def _wrap_gradient(gradient_function): - def wrapped_gradient(x): - gradient = gradient_function(x) - if isinstance(gradient, np.ndarray): - return gradient.tolist() - return gradient - - return wrapped_gradient diff --git a/qiskit/algorithms/optimizers/slsqp.py b/qiskit/algorithms/optimizers/slsqp.py deleted file mode 100644 index d02eb790afa9..000000000000 --- a/qiskit/algorithms/optimizers/slsqp.py +++ /dev/null @@ -1,73 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2018, 2020. -# -# 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. - -"""Sequential Least SQuares Programming optimizer""" -from __future__ import annotations - - -from .scipy_optimizer import SciPyOptimizer - - -class SLSQP(SciPyOptimizer): - """ - Sequential Least SQuares Programming optimizer. - - SLSQP minimizes a function of several variables with any combination of bounds, equality - and inequality constraints. The method wraps the SLSQP Optimization subroutine originally - implemented by Dieter Kraft. - - SLSQP is ideal for mathematical problems for which the objective function and the constraints - are twice continuously differentiable. Note that the wrapper handles infinite values in bounds - by converting them into large floating values. - - Uses scipy.optimize.minimize SLSQP. - For further detail, please refer to - See https://docs.scipy.org/doc/scipy/reference/generated/scipy.optimize.minimize.html - """ - - _OPTIONS = ["maxiter", "disp", "ftol", "eps"] - - # pylint: disable=unused-argument - def __init__( - self, - maxiter: int = 100, - disp: bool = False, - ftol: float = 1e-06, - tol: float | None = None, - eps: float = 1.4901161193847656e-08, - options: dict | None = None, - max_evals_grouped: int = 1, - **kwargs, - ) -> None: - """ - Args: - maxiter: Maximum number of iterations. - disp: Set to True to print convergence messages. - ftol: Precision goal for the value of f in the stopping criterion. - tol: Tolerance for termination. - eps: Step size used for numerical approximation of the Jacobian. - options: A dictionary of solver options. - max_evals_grouped: Max number of default gradient evaluations performed simultaneously. - kwargs: additional kwargs for scipy.optimize.minimize. - """ - if options is None: - options = {} - for k, v in list(locals().items()): - if k in self._OPTIONS: - options[k] = v - super().__init__( - "SLSQP", - options=options, - tol=tol, - max_evals_grouped=max_evals_grouped, - **kwargs, - ) diff --git a/qiskit/algorithms/optimizers/snobfit.py b/qiskit/algorithms/optimizers/snobfit.py deleted file mode 100644 index 8d6a3bde1d07..000000000000 --- a/qiskit/algorithms/optimizers/snobfit.py +++ /dev/null @@ -1,130 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2019, 2020. -# -# 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. - -"""Stable Noisy Optimization by Branch and FIT algorithm (SNOBFIT) optimizer.""" -from __future__ import annotations - -from collections.abc import Callable -from typing import Any - -import numpy as np -from qiskit.exceptions import QiskitError -from qiskit.utils import optionals as _optionals -from .optimizer import Optimizer, OptimizerSupportLevel, OptimizerResult, POINT - - -@_optionals.HAS_SKQUANT.require_in_instance -@_optionals.HAS_SQSNOBFIT.require_in_instance -class SNOBFIT(Optimizer): - """Stable Noisy Optimization by Branch and FIT algorithm. - - SnobFit is used for the optimization of derivative-free, noisy objective functions providing - robust and fast solutions of problems with continuous variables varying within bound. - - Uses skquant.opt installed with pip install scikit-quant. - For further detail, please refer to - https://github.com/scikit-quant/scikit-quant and https://qat4chem.lbl.gov/software. - """ - - def __init__( - self, - maxiter: int = 1000, - maxfail: int = 10, - maxmp: int = None, - verbose: bool = False, - ) -> None: - """ - Args: - maxiter: Maximum number of function evaluations. - maxmp: Maximum number of model points requested for the local fit. - Default = 2 * number of parameters + 6 set to this value when None. - maxfail: Maximum number of failures to improve the solution. Stops the algorithm - after maxfail is reached. - verbose: Provide verbose (debugging) output. - - Raises: - MissingOptionalLibraryError: scikit-quant or SQSnobFit not installed - QiskitError: If NumPy 1.24.0 or above is installed. - See https://github.com/scikit-quant/scikit-quant/issues/24 for more details. - """ - # check version - version = tuple(map(int, np.__version__.split("."))) - if version >= (1, 24, 0): - raise QiskitError( - "SnobFit is incompatible with NumPy 1.24.0 or above, please " - "install a previous version. See also scikit-quant/scikit-quant#24." - ) - - super().__init__() - self._maxiter = maxiter - self._maxfail = maxfail - self._maxmp = maxmp - self._verbose = verbose - - def get_support_level(self): - """Returns support level dictionary.""" - return { - "gradient": OptimizerSupportLevel.ignored, - "bounds": OptimizerSupportLevel.required, - "initial_point": OptimizerSupportLevel.required, - } - - @property - def settings(self) -> dict[str, Any]: - return { - "maxiter": self._maxiter, - "maxfail": self._maxfail, - "maxmp": self._maxmp, - "verbose": self._verbose, - } - - def minimize( - self, - fun: Callable[[POINT], float], - x0: POINT, - jac: Callable[[POINT], POINT] | None = None, - bounds: list[tuple[float, float]] | None = None, - ) -> OptimizerResult: - import skquant.opt as skq - from SQSnobFit import optset - - if bounds is None or any(None in bound_tuple for bound_tuple in bounds): - raise ValueError("Optimizer SNOBFIT requires bounds for all parameters.") - - snobfit_settings = { - "maxmp": self._maxmp, - "maxfail": self._maxfail, - "verbose": self._verbose, - } - options = optset(optin=snobfit_settings) - # counters the error when initial point is outside the acceptable bounds - x0 = np.asarray(x0) - for idx, theta in enumerate(x0): - if abs(theta) > bounds[idx][0]: - x0[idx] = x0[idx] % bounds[idx][0] - elif abs(theta) > bounds[idx][1]: - x0[idx] = x0[idx] % bounds[idx][1] - - res, history = skq.minimize( - fun, - x0, - bounds=bounds, - budget=self._maxiter, - method="snobfit", - options=options, - ) - - optimizer_result = OptimizerResult() - optimizer_result.x = res.optpar - optimizer_result.fun = res.optval - optimizer_result.nfev = len(history) - return optimizer_result diff --git a/qiskit/algorithms/optimizers/spsa.py b/qiskit/algorithms/optimizers/spsa.py deleted file mode 100644 index f02a760e596c..000000000000 --- a/qiskit/algorithms/optimizers/spsa.py +++ /dev/null @@ -1,811 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2018, 2021. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. - -"""Simultaneous Perturbation Stochastic Approximation (SPSA) optimizer. - -This implementation allows both, standard first-order as well as second-order SPSA. -""" -from __future__ import annotations - -from collections import deque -from collections.abc import Iterator -from typing import Callable, Any, SupportsFloat -import logging -import warnings -from time import time - -import scipy -import numpy as np - -from qiskit.utils import algorithm_globals -from qiskit.utils.deprecation import deprecate_func - -from .optimizer import Optimizer, OptimizerSupportLevel, OptimizerResult, POINT - -# number of function evaluations, parameters, loss, stepsize, accepted -CALLBACK = Callable[[int, np.ndarray, float, SupportsFloat, bool], None] -TERMINATIONCHECKER = Callable[[int, np.ndarray, float, SupportsFloat, bool], bool] - -logger = logging.getLogger(__name__) - - -class SPSA(Optimizer): - """Simultaneous Perturbation Stochastic Approximation (SPSA) optimizer. - - SPSA [1] is an gradient descent method for optimizing systems with multiple unknown parameters. - As an optimization method, it is appropriately suited to large-scale population models, - adaptive modeling, and simulation optimization. - - .. seealso:: - - Many examples are presented at the `SPSA Web site `__. - - The main feature of SPSA is the stochastic gradient approximation, which requires only two - measurements of the objective function, regardless of the dimension of the optimization - problem. - - Additionally to standard, first-order SPSA, where only gradient information is used, this - implementation also allows second-order SPSA (2-SPSA) [2]. In 2-SPSA we also estimate the - Hessian of the loss with a stochastic approximation and multiply the gradient with the - inverse Hessian to take local curvature into account and improve convergence. - Notably this Hessian estimate requires only a constant number of function evaluations - unlike an exact evaluation of the Hessian, which scales quadratically in the number of - function evaluations. - - .. note:: - - SPSA can be used in the presence of noise, and it is therefore indicated in situations - involving measurement uncertainty on a quantum computation when finding a minimum. - If you are executing a variational algorithm using an OpenQASM - simulator or a real device, SPSA would be the most recommended choice among the optimizers - provided here. - - The optimization process can includes a calibration phase if neither the ``learning_rate`` nor - ``perturbation`` is provided, which requires additional functional evaluations. - (Note that either both or none must be set.) For further details on the automatic calibration, - please refer to the supplementary information section IV. of [3]. - - .. note:: - - 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.utils.algorithm_globals.random_seed = seed``). - - - Examples: - - This short example runs SPSA for the ground state calculation of the ``Z ^ Z`` - observable where the ansatz is a ``PauliTwoDesign`` circuit. - - .. code-block:: python - - import numpy as np - from qiskit.algorithms.optimizers import SPSA - from qiskit.circuit.library import PauliTwoDesign - from qiskit.opflow import Z, StateFn - - ansatz = PauliTwoDesign(2, reps=1, seed=2) - observable = Z ^ Z - initial_point = np.random.random(ansatz.num_parameters) - - def loss(x): - bound = ansatz.assign_parameters(x) - return np.real((StateFn(observable, is_measurement=True) @ StateFn(bound)).eval()) - - spsa = SPSA(maxiter=300) - result = spsa.optimize(ansatz.num_parameters, loss, initial_point=initial_point) - - To use the Hessian information, i.e. 2-SPSA, you can add `second_order=True` to the - initializer of the `SPSA` class, the rest of the code remains the same. - - .. code-block:: python - - two_spsa = SPSA(maxiter=300, second_order=True) - result = two_spsa.optimize(ansatz.num_parameters, loss, initial_point=initial_point) - - The `termination_checker` can be used to implement a custom termination criterion. - - .. code-block:: python - - import numpy as np - from qiskit.algorithms.optimizers import SPSA - - def objective(x): - return np.linalg.norm(x) + .04*np.random.rand(1) - - class TerminationChecker: - - def __init__(self, N : int): - self.N = N - self.values = [] - - def __call__(self, nfev, parameters, value, stepsize, accepted) -> bool: - self.values.append(value) - - if len(self.values) > self.N: - last_values = self.values[-self.N:] - pp = np.polyfit(range(self.N), last_values, 1) - slope = pp[0] / self.N - - if slope > 0: - return True - return False - - spsa = SPSA(maxiter=200, termination_checker=TerminationChecker(10)) - parameters, value, niter = spsa.optimize(2, objective, initial_point=[0.5, 0.5]) - print(f'SPSA completed after {niter} iterations') - - - References: - - [1]: J. C. Spall (1998). An Overview of the Simultaneous Perturbation Method for Efficient - Optimization, Johns Hopkins APL Technical Digest, 19(4), 482–492. - `Online at jhuapl.edu. `_ - - [2]: J. C. Spall (1997). Accelerated second-order stochastic optimization using only - function measurements, Proceedings of the 36th IEEE Conference on Decision and Control, - 1417-1424 vol.2. `Online at IEEE.org. `_ - - [3]: A. Kandala et al. (2017). Hardware-efficient Variational Quantum Eigensolver for - Small Molecules and Quantum Magnets. Nature 549, pages242–246(2017). - `arXiv:1704.05018v2 `_ - - """ - - def __init__( - self, - maxiter: int = 100, - blocking: bool = False, - allowed_increase: float | None = None, - trust_region: bool = False, - learning_rate: float | np.ndarray | Callable[[], Iterator] | None = None, - perturbation: float | np.ndarray | Callable[[], Iterator] | None = None, - last_avg: int = 1, - resamplings: int | dict[int, int] = 1, - perturbation_dims: int | None = None, - second_order: bool = False, - regularization: float | None = None, - hessian_delay: int = 0, - lse_solver: Callable[[np.ndarray, np.ndarray], np.ndarray] | None = None, - initial_hessian: np.ndarray | None = None, - callback: CALLBACK | None = None, - termination_checker: TERMINATIONCHECKER | None = None, - ) -> None: - r""" - Args: - maxiter: The maximum number of iterations. Note that this is not the maximal number - of function evaluations. - blocking: If True, only accepts updates that improve the loss (up to some allowed - increase, see next argument). - allowed_increase: If ``blocking`` is ``True``, this argument determines by how much - the loss can increase with the proposed parameters and still be accepted. - If ``None``, the allowed increases is calibrated automatically to be twice the - approximated standard deviation of the loss function. - trust_region: If ``True``, restricts the norm of the update step to be :math:`\leq 1`. - learning_rate: The update step is the learning rate is multiplied with the gradient. - If the learning rate is a float, it remains constant over the course of the - optimization. If a NumPy array, the :math:`i`-th element is the learning rate for - the :math:`i`-th iteration. It can also be a callable returning an iterator which - yields the learning rates for each optimization step. - If ``learning_rate`` is set ``perturbation`` must also be provided. - perturbation: Specifies the magnitude of the perturbation for the finite difference - approximation of the gradients. See ``learning_rate`` for the supported types. - If ``perturbation`` is set ``learning_rate`` must also be provided. - last_avg: Return the average of the ``last_avg`` parameters instead of just the - last parameter values. - resamplings: The number of times the gradient (and Hessian) is sampled using a random - direction to construct a gradient estimate. Per default the gradient is estimated - using only one random direction. If an integer, all iterations use the same number - of resamplings. If a dictionary, this is interpreted as - ``{iteration: number of resamplings per iteration}``. - perturbation_dims: The number of perturbed dimensions. Per default, all dimensions - are perturbed, but a smaller, fixed number can be perturbed. If set, the perturbed - dimensions are chosen uniformly at random. - second_order: If True, use 2-SPSA instead of SPSA. In 2-SPSA, the Hessian is estimated - additionally to the gradient, and the gradient is preconditioned with the inverse - of the Hessian to improve convergence. - regularization: To ensure the preconditioner is symmetric and positive definite, the - identity times a small coefficient is added to it. This generator yields that - coefficient. - hessian_delay: Start multiplying the gradient with the inverse Hessian only after a - certain number of iterations. The Hessian is still evaluated and therefore this - argument can be useful to first get a stable average over the last iterations before - using it as preconditioner. - lse_solver: The method to solve for the inverse of the Hessian. Per default an - exact LSE solver is used, but can e.g. be overwritten by a minimization routine. - initial_hessian: The initial guess for the Hessian. By default the identity matrix - is used. - callback: A callback function passed information in each iteration step. The - information is, in this order: the number of function evaluations, the parameters, - the function value, the stepsize, whether the step was accepted. - termination_checker: A callback function executed at the end of each iteration step. The - arguments are, in this order: the parameters, the function value, the number - of function evaluations, the stepsize, whether the step was accepted. If the callback - returns True, the optimization is terminated. - To prevent additional evaluations of the objective method, if the objective has not yet - been evaluated, the objective is estimated by taking the mean of the objective - evaluations used in the estimate of the gradient. - - - Raises: - ValueError: If ``learning_rate`` or ``perturbation`` is an array with less elements - than the number of iterations. - - - """ - super().__init__() - - # general optimizer arguments - self.maxiter = maxiter - self.trust_region = trust_region - self.callback = callback - self.termination_checker = termination_checker - - # if learning rate and perturbation are arrays, check they are sufficiently long - for attr, name in zip([learning_rate, perturbation], ["learning_rate", "perturbation"]): - if isinstance(attr, (list, np.ndarray)): - if len(attr) < maxiter: - raise ValueError(f"Length of {name} is smaller than maxiter ({maxiter}).") - - self.learning_rate = learning_rate - self.perturbation = perturbation - - # SPSA specific arguments - self.blocking = blocking - self.allowed_increase = allowed_increase - self.last_avg = last_avg - self.resamplings = resamplings - self.perturbation_dims = perturbation_dims - - # 2-SPSA specific arguments - if regularization is None: - regularization = 0.01 - - self.second_order = second_order - self.hessian_delay = hessian_delay - self.lse_solver = lse_solver - self.regularization = regularization - self.initial_hessian = initial_hessian - - # runtime arguments - self._nfev: int | None = None # the number of function evaluations - self._smoothed_hessian: np.ndarray | None = None # smoothed average of the Hessians - - @staticmethod - def calibrate( - loss: Callable[[np.ndarray], float], - initial_point: np.ndarray, - c: float = 0.2, - stability_constant: float = 0, - target_magnitude: float | None = None, # 2 pi / 10 - alpha: float = 0.602, - gamma: float = 0.101, - modelspace: bool = False, - max_evals_grouped: int = 1, - ) -> tuple[Callable, Callable]: - r"""Calibrate SPSA parameters with a powerseries as learning rate and perturbation coeffs. - - The powerseries are: - - .. math:: - - a_k = \frac{a}{(A + k + 1)^\alpha}, c_k = \frac{c}{(k + 1)^\gamma} - - Args: - loss: The loss function. - initial_point: The initial guess of the iteration. - c: The initial perturbation magnitude. - stability_constant: The value of `A`. - target_magnitude: The target magnitude for the first update step, defaults to - :math:`2\pi / 10`. - alpha: The exponent of the learning rate powerseries. - gamma: The exponent of the perturbation powerseries. - modelspace: Whether the target magnitude is the difference of parameter values - or function values (= model space). - max_evals_grouped: The number of grouped evaluations supported by the loss function. - Defaults to 1, i.e. no grouping. - - Returns: - tuple(generator, generator): A tuple of powerseries generators, the first one for the - learning rate and the second one for the perturbation. - """ - logger.info("SPSA: Starting calibration of learning rate and perturbation.") - if target_magnitude is None: - target_magnitude = 2 * np.pi / 10 - - dim = len(initial_point) - - # compute the average magnitude of the first step - steps = 25 - points = [] - for _ in range(steps): - # compute the random direction - pert = bernoulli_perturbation(dim) - points += [initial_point + c * pert, initial_point - c * pert] - - losses = _batch_evaluate(loss, points, max_evals_grouped) - - avg_magnitudes = 0.0 - for i in range(steps): - delta = losses[2 * i] - losses[2 * i + 1] - avg_magnitudes += np.abs(delta / (2 * c)) - - avg_magnitudes /= steps - - if modelspace: - a = target_magnitude / (avg_magnitudes**2) - else: - a = target_magnitude / avg_magnitudes - - # compute the rescaling factor for correct first learning rate - if a < 1e-10: - warnings.warn(f"Calibration failed, using {target_magnitude} for `a`") - a = target_magnitude - - logger.info("Finished calibration:") - logger.info( - " -- Learning rate: a / ((A + n) ^ alpha) with a = %s, A = %s, alpha = %s", - a, - stability_constant, - alpha, - ) - logger.info(" -- Perturbation: c / (n ^ gamma) with c = %s, gamma = %s", c, gamma) - - # set up the powerseries - def learning_rate(): - return powerseries(a, alpha, stability_constant) - - def perturbation(): - return powerseries(c, gamma) - - return learning_rate, perturbation - - @staticmethod - def estimate_stddev( - loss: Callable[[np.ndarray], float], - initial_point: np.ndarray, - avg: int = 25, - max_evals_grouped: int = 1, - ) -> float: - """Estimate the standard deviation of the loss function.""" - losses = _batch_evaluate(loss, avg * [initial_point], max_evals_grouped) - return np.std(losses) - - @property - def settings(self) -> dict[str, Any]: - # if learning rate or perturbation are custom iterators expand them - if callable(self.learning_rate): - iterator = self.learning_rate() - learning_rate = np.array([next(iterator) for _ in range(self.maxiter)]) - else: - learning_rate = self.learning_rate - - if callable(self.perturbation): - iterator = self.perturbation() - perturbation = np.array([next(iterator) for _ in range(self.maxiter)]) - else: - perturbation = self.perturbation - - return { - "maxiter": self.maxiter, - "learning_rate": learning_rate, - "perturbation": perturbation, - "trust_region": self.trust_region, - "blocking": self.blocking, - "allowed_increase": self.allowed_increase, - "resamplings": self.resamplings, - "perturbation_dims": self.perturbation_dims, - "second_order": self.second_order, - "hessian_delay": self.hessian_delay, - "regularization": self.regularization, - "lse_solver": self.lse_solver, - "initial_hessian": self.initial_hessian, - "callback": self.callback, - "termination_checker": self.termination_checker, - } - - def _point_sample(self, loss, x, eps, delta1, delta2): - """A single sample of the gradient at position ``x`` in direction ``delta``.""" - # points to evaluate - points = [x + eps * delta1, x - eps * delta1] - self._nfev += 2 - - if self.second_order: - points += [x + eps * (delta1 + delta2), x + eps * (-delta1 + delta2)] - self._nfev += 2 - - # batch evaluate the points (if possible) - values = _batch_evaluate(loss, points, self._max_evals_grouped) - - plus = values[0] - minus = values[1] - gradient_sample = (plus - minus) / (2 * eps) * delta1 - - hessian_sample = None - if self.second_order: - diff = (values[2] - plus) - (values[3] - minus) - diff /= 2 * eps**2 - - rank_one = np.outer(delta1, delta2) - hessian_sample = diff * (rank_one + rank_one.T) / 2 - - return np.mean(values), gradient_sample, hessian_sample - - def _point_estimate(self, loss, x, eps, num_samples): - """The gradient estimate at point x.""" - # set up variables to store averages - value_estimate = 0 - gradient_estimate = np.zeros(x.size) - hessian_estimate = np.zeros((x.size, x.size)) - - # iterate over the directions - deltas1 = [ - bernoulli_perturbation(x.size, self.perturbation_dims) for _ in range(num_samples) - ] - - if self.second_order: - deltas2 = [ - bernoulli_perturbation(x.size, self.perturbation_dims) for _ in range(num_samples) - ] - else: - deltas2 = None - - for i in range(num_samples): - delta1 = deltas1[i] - delta2 = deltas2[i] if self.second_order else None - - value_sample, gradient_sample, hessian_sample = self._point_sample( - loss, x, eps, delta1, delta2 - ) - value_estimate += value_sample - gradient_estimate += gradient_sample - - if self.second_order: - hessian_estimate += hessian_sample - - return ( - value_estimate / num_samples, - gradient_estimate / num_samples, - hessian_estimate / num_samples, - ) - - def _compute_update(self, loss, x, k, eps, lse_solver): - # compute the perturbations - if isinstance(self.resamplings, dict): - num_samples = self.resamplings.get(k, 1) - else: - num_samples = self.resamplings - - # accumulate the number of samples - value, gradient, hessian = self._point_estimate(loss, x, eps, num_samples) - - # precondition gradient with inverse Hessian, if specified - if self.second_order: - smoothed = k / (k + 1) * self._smoothed_hessian + 1 / (k + 1) * hessian - self._smoothed_hessian = smoothed - - if k > self.hessian_delay: - spd_hessian = _make_spd(smoothed, self.regularization) - - # solve for the gradient update - gradient = np.real(lse_solver(spd_hessian, gradient)) - - return value, gradient - - def minimize( - self, - fun: Callable[[POINT], float], - x0: POINT, - jac: Callable[[POINT], POINT] | None = None, - bounds: list[tuple[float, float]] | None = None, - ) -> OptimizerResult: - # ensure learning rate and perturbation are correctly set: either none or both - # this happens only here because for the calibration the loss function is required - if self.learning_rate is None and self.perturbation is None: - get_eta, get_eps = self.calibrate(fun, x0, max_evals_grouped=self._max_evals_grouped) - else: - get_eta, get_eps = _validate_pert_and_learningrate( - self.perturbation, self.learning_rate - ) - eta, eps = get_eta(), get_eps() - - if self.lse_solver is None: - lse_solver = np.linalg.solve - else: - lse_solver = self.lse_solver - - # prepare some initials - x = np.asarray(x0) - if self.initial_hessian is None: - self._smoothed_hessian = np.identity(x.size) - else: - self._smoothed_hessian = self.initial_hessian - - self._nfev = 0 - - # if blocking is enabled we need to keep track of the function values - if self.blocking: - fx = fun(x) - - self._nfev += 1 - if self.allowed_increase is None: - self.allowed_increase = 2 * self.estimate_stddev( - fun, x, max_evals_grouped=self._max_evals_grouped - ) - - logger.info("SPSA: Starting optimization.") - start = time() - - # keep track of the last few steps to return their average - last_steps = deque([x]) - - # use a local variable and while loop to keep track of the number of iterations - # if the termination checker terminates early - k = 0 - while k < self.maxiter: - k += 1 - iteration_start = time() - # compute update - fx_estimate, update = self._compute_update(fun, x, k, next(eps), lse_solver) - - # trust region - if self.trust_region: - norm = np.linalg.norm(update) - if norm > 1: # stop from dividing by 0 - update = update / norm - - # compute next parameter value - update = update * next(eta) - x_next = x - update - fx_next = None - - # blocking - if self.blocking: - self._nfev += 1 - fx_next = fun(x_next) - - if fx + self.allowed_increase <= fx_next: # accept only if loss improved - if self.callback is not None: - self.callback( - self._nfev, # number of function evals - x_next, # next parameters - fx_next, # loss at next parameters - np.linalg.norm(update), # size of the update step - False, - ) # not accepted - - logger.info( - "Iteration %s/%s rejected in %s.", - k, - self.maxiter + 1, - time() - iteration_start, - ) - continue - fx = fx_next - - logger.info( - "Iteration %s/%s done in %s.", k, self.maxiter + 1, time() - iteration_start - ) - - if self.callback is not None: - # if we didn't evaluate the function yet, do it now - if not self.blocking: - self._nfev += 1 - fx_next = fun(x_next) - - self.callback( - self._nfev, # number of function evals - x_next, # next parameters - fx_next, # loss at next parameters - np.linalg.norm(update), # size of the update step - True, - ) # accepted - - # update parameters - x = x_next - - # update the list of the last ``last_avg`` parameters - if self.last_avg > 1: - last_steps.append(x_next) - if len(last_steps) > self.last_avg: - last_steps.popleft() - - if self.termination_checker is not None: - fx_check = fx_estimate if fx_next is None else fx_next - if self.termination_checker( - self._nfev, x_next, fx_check, np.linalg.norm(update), True - ): - logger.info("terminated optimization at {k}/{self.maxiter} iterations") - break - - logger.info("SPSA: Finished in %s", time() - start) - - if self.last_avg > 1: - x = np.mean(last_steps, axis=0) - - result = OptimizerResult() - result.x = x - result.fun = fun(x) - result.nfev = self._nfev - result.nit = k - - return result - - def get_support_level(self): - """Get the support level dictionary.""" - return { - "gradient": OptimizerSupportLevel.ignored, - "bounds": OptimizerSupportLevel.ignored, - "initial_point": OptimizerSupportLevel.required, - } - - # pylint: disable=bad-docstring-quotes - @deprecate_func( - additional_msg=( - "Instead, use ``SPSA.minimize`` as a replacement, which supports the same arguments " - "but follows the interface of scipy.optimize and returns a complete result object " - "containing additional information." - ), - since="0.21.0", - package_name="qiskit-terra", - ) - def optimize( - self, - num_vars, # pylint: disable=unused-argument - objective_function, - gradient_function=None, # pylint: disable=unused-argument - variable_bounds=None, # pylint: disable=unused-argument - initial_point=None, - ): - """Perform optimization. - - Args: - num_vars (int): Number of parameters to be optimized. - objective_function (callable): A function that computes the objective function. - gradient_function (callable): Not supported for SPSA. - variable_bounds (list[(float, float)]): Not supported for SPSA. - initial_point (numpy.ndarray[float]): Initial point. - - Returns: - tuple: point, value, nfev - point: is a 1D numpy.ndarray[float] containing the solution - value: is a float with the objective function value - nfev: number of objective function calls made if available or None - """ - result = self.minimize(objective_function, initial_point) - return result.x, result.fun, result.nfev - - -def bernoulli_perturbation(dim, perturbation_dims=None): - """Get a Bernoulli random perturbation.""" - if perturbation_dims is None: - with warnings.catch_warnings(): - warnings.filterwarnings("ignore", category=DeprecationWarning) - return 1 - 2 * algorithm_globals.random.binomial(1, 0.5, size=dim) - - with warnings.catch_warnings(): - warnings.filterwarnings("ignore", category=DeprecationWarning) - pert = 1 - 2 * algorithm_globals.random.binomial(1, 0.5, size=perturbation_dims) - indices = algorithm_globals.random.choice( - list(range(dim)), size=perturbation_dims, replace=False - ) - result = np.zeros(dim) - result[indices] = pert - - return result - - -def powerseries(eta=0.01, power=2, offset=0): - """Yield a series decreasing by a powerlaw.""" - - n = 1 - while True: - yield eta / ((n + offset) ** power) - n += 1 - - -def constant(eta=0.01): - """Yield a constant series.""" - - while True: - yield eta - - -def _batch_evaluate(function, points, max_evals_grouped, unpack_points=False): - """Evaluate a function on all points with batches of max_evals_grouped. - - The points are a list of inputs, as ``[in1, in2, in3, ...]``. If the individual - inputs are tuples (because the function takes multiple inputs), set ``unpack_points`` to ``True``. - """ - - # if the function cannot handle lists of points as input, cover this case immediately - if max_evals_grouped is None or max_evals_grouped == 1: - # support functions with multiple arguments where the points are given in a tuple - return [ - function(*point) if isinstance(point, tuple) else function(point) for point in points - ] - - num_points = len(points) - - # get the number of batches - num_batches = num_points // max_evals_grouped - if num_points % max_evals_grouped != 0: - num_batches += 1 - - # split the points - batched_points = np.array_split(np.asarray(points), num_batches) - - results = [] - for batch in batched_points: - if unpack_points: - batch = _repack_points(batch) - results += _as_list(function(*batch)) - else: - results += _as_list(function(batch)) - - return results - - -def _as_list(obj): - """Convert a list or numpy array into a list.""" - return obj.tolist() if isinstance(obj, np.ndarray) else obj - - -def _repack_points(points): - """Turn a list of tuples of points into a tuple of lists of points. - E.g. turns - [(a1, a2, a3), (b1, b2, b3)] - into - ([a1, b1], [a2, b2], [a3, b3]) - where all elements are np.ndarray. - """ - num_sets = len(points[0]) # length of (a1, a2, a3) - return ([x[i] for x in points] for i in range(num_sets)) - - -def _make_spd(matrix, bias=0.01): - identity = np.identity(matrix.shape[0]) - psd = scipy.linalg.sqrtm(matrix.dot(matrix)) - return psd + bias * identity - - -def _validate_pert_and_learningrate(perturbation, learning_rate): - if learning_rate is None or perturbation is None: - raise ValueError("If one of learning rate or perturbation is set, both must be set.") - - if isinstance(perturbation, float): - - def get_eps(): - return constant(perturbation) - - elif isinstance(perturbation, (list, np.ndarray)): - - def get_eps(): - return iter(perturbation) - - else: - get_eps = perturbation - - if isinstance(learning_rate, float): - - def get_eta(): - return constant(learning_rate) - - elif isinstance(learning_rate, (list, np.ndarray)): - - def get_eta(): - return iter(learning_rate) - - else: - get_eta = learning_rate - - return get_eta, get_eps diff --git a/qiskit/algorithms/optimizers/steppable_optimizer.py b/qiskit/algorithms/optimizers/steppable_optimizer.py deleted file mode 100644 index ed9c75d86b04..000000000000 --- a/qiskit/algorithms/optimizers/steppable_optimizer.py +++ /dev/null @@ -1,303 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2022. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. - -"""SteppableOptimizer interface""" -from __future__ import annotations - -from abc import abstractmethod, ABC -from collections.abc import Callable -from dataclasses import dataclass -from .optimizer import Optimizer, POINT, OptimizerResult - - -@dataclass -class AskData(ABC): - """Base class for return type of :meth:`~.SteppableOptimizer.ask`. - - Args: - x_fun: Point or list of points where the function needs to be evaluated to compute the next - state of the optimizer. - x_jac: Point or list of points where the gradient/jacobian needs to be evaluated to compute - the next state of the optimizer. - - """ - - x_fun: POINT | list[POINT] | None = None - x_jac: POINT | list[POINT] | None = None - - -@dataclass -class TellData(ABC): - """Base class for argument type of :meth:`~.SteppableOptimizer.tell`. - - Args: - eval_fun: Image of the function at :attr:`~.ask_data.x_fun`. - eval_jac: Image of the gradient-jacobian at :attr:`~.ask_data.x_jac`. - - """ - - eval_fun: float | list[float] | None = None - eval_jac: POINT | list[POINT] | None = None - - -@dataclass -class OptimizerState: - """Base class representing the state of the optimizer. - - This class stores the current state of the optimizer, given by the current point and - (optionally) information like the function value, the gradient or the number of - function evaluations. This dataclass can also store any other individual variables that - change during the optimization. - - """ - - x: POINT - """Current optimization parameters.""" - fun: Callable[[POINT], float] | None - """Function being optimized.""" - jac: Callable[[POINT], POINT] | None - """Jacobian of the function being optimized.""" - nfev: int | None - """Number of function evaluations so far in the optimization.""" - njev: int | None - """Number of jacobian evaluations so far in the opimization.""" - nit: int | None - """Number of optimization steps performed so far in the optimization.""" - - -class SteppableOptimizer(Optimizer): - """ - Base class for a steppable optimizer. - - This family of optimizers uses the `ask and tell interface - `_. - When using this interface the user has to call :meth:`~.ask` to get information about - how to evaluate the function (we are asking the optimizer about how to do the evaluation). - This information is typically the next points at which the function is evaluated, but depending - on the optimizer it can also determine whether to evaluate the function or its gradient. - Once the function has been evaluated, the user calls the method :meth:`~..tell` - to tell the optimizer what the result of the function evaluation(s) is. The optimizer then - updates its state accordingly and the user can decide whether to stop the optimization process - or to repeat a step. - - This interface is more customizable, and allows the user to have full control over the evaluation - of the function. - - Examples: - - An example where the evaluation of the function has a chance of failing. The user, with - specific knowledge about his function can catch this errors and handle them before passing - the result to the optimizer. - - .. code-block:: python - - import random - import numpy as np - from qiskit.algorithms.optimizers import GradientDescent - - def objective(x): - if random.choice([True, False]): - return None - else: - return (np.linalg.norm(x) - 1) ** 2 - - def grad(x): - if random.choice([True, False]): - return None - else: - return 2 * (np.linalg.norm(x) - 1) * x / np.linalg.norm(x) - - - initial_point = np.random.normal(0, 1, size=(100,)) - - optimizer = GradientDescent(maxiter=20) - optimizer.start(x0=initial_point, fun=objective, jac=grad) - - while optimizer.continue_condition(): - ask_data = optimizer.ask() - evaluated_gradient = None - - while evaluated_gradient is None: - evaluated_gradient = grad(ask_data.x_center) - optimizer.state.njev += 1 - - optmizer.state.nit += 1 - - cf = TellData(eval_jac=evaluated_gradient) - optimizer.tell(ask_data=ask_data, tell_data=tell_data) - - result = optimizer.create_result() - - - Users that aren't dealing with complicated functions and who are more familiar with step by step - optimization algorithms can use the :meth:`~.step` method which wraps the :meth:`~.ask` - and :meth:`~.tell` methods. In the same spirit the method :meth:`~.minimize` will optimize the - function and return the result. - - To see other libraries that use this interface one can visit: - https://optuna.readthedocs.io/en/stable/tutorial/20_recipes/009_ask_and_tell.html - - - """ - - def __init__( - self, - maxiter: int = 100, - ): - """ - Args: - maxiter: Number of steps in the optimization process before ending the loop. - """ - super().__init__() - self._state: OptimizerState | None = None - self.maxiter = maxiter - - @property - def state(self) -> OptimizerState: - """Return the current state of the optimizer.""" - return self._state - - @state.setter - def state(self, state: OptimizerState) -> None: - """Set the current state of the optimizer.""" - self._state = state - - def ask(self) -> AskData: - """Ask the optimizer for a set of points to evaluate. - - This method asks the optimizer which are the next points to evaluate. - These points can, e.g., correspond to function values and/or its derivative. - It may also correspond to variables that let the user infer which points to evaluate. - It is the first method inside of a :meth:`~.step` in the optimization process. - - Returns: - An object containing the data needed to make the function evaluation to advance the - optimization process. - - """ - raise NotImplementedError - - def tell(self, ask_data: AskData, tell_data: TellData) -> None: - """Updates the optimization state using the results of the function evaluation. - - A canonical optimization example using :meth:`~.ask` and :meth:`~.tell` can be seen - in :meth:`~.step`. - - Args: - ask_data: Contains the information on how the evaluation was done. - tell_data: Contains all relevant information about the evaluation of the objective - function. - """ - raise NotImplementedError - - @abstractmethod - def evaluate(self, ask_data: AskData) -> TellData: - """Evaluates the function according to the instructions contained in :attr:`~.ask_data`. - - If the user decides to use :meth:`~.step` instead of :meth:`~.ask` and :meth:`~.tell` - this function will contain the logic on how to evaluate the function. - - Args: - ask_data: Contains the information on how to do the evaluation. - - Returns: - Data of all relevant information about the function evaluation. - - """ - raise NotImplementedError - - def _callback_wrapper(self) -> None: - """ - Wraps the callback function to accommodate each optimizer. - """ - pass - - def step(self) -> None: - """Performs one step in the optimization process. - - This method composes :meth:`~.ask`, :meth:`~.evaluate`, and :meth:`~.tell` to make a "step" - in the optimization process. - """ - ask_data = self.ask() - tell_data = self.evaluate(ask_data=ask_data) - self.tell(ask_data=ask_data, tell_data=tell_data) - - # pylint: disable=invalid-name - @abstractmethod - def start( - self, - fun: Callable[[POINT], float], - x0: POINT, - jac: Callable[[POINT], POINT] | None = None, - bounds: list[tuple[float, float]] | None = None, - ) -> None: - """Populates the state of the optimizer with the data provided and sets all the counters to 0. - - Args: - fun: Function to minimize. - x0: Initial point. - jac: Function to compute the gradient. - bounds: Bounds of the search space. - - """ - raise NotImplementedError - - def minimize( - self, - fun: Callable[[POINT], float], - x0: POINT, - jac: Callable[[POINT], POINT] | None = None, - bounds: list[tuple[float, float]] | None = None, - ) -> OptimizerResult: - """Minimizes the function. - - For well behaved functions the user can call this method to minimize a function. - If the user wants more control on how to evaluate the function a custom loop can be - created using :meth:`~.ask` and :meth:`~.tell` and evaluating the function manually. - - Args: - fun: Function to minimize. - x0: Initial point. - jac: Function to compute the gradient. - bounds: Bounds of the search space. - - Returns: - Object containing the result of the optimization. - - """ - self.start(x0=x0, fun=fun, jac=jac, bounds=bounds) - while self.continue_condition(): - self.step() - self._callback_wrapper() - return self.create_result() - - @abstractmethod - def create_result(self) -> OptimizerResult: - """Returns the result of the optimization. - - All the information needed to create such a result should be stored in the optimizer state - and will typically contain the best point found, the function value and gradient at that point, - the number of function and gradient evaluation and the number of iterations in the optimization. - - Returns: - The result of the optimization process. - - """ - raise NotImplementedError - - def continue_condition(self) -> bool: - """Condition that indicates the optimization process should continue. - - Returns: - ``True`` if the optimization process should continue, ``False`` otherwise. - """ - return self.state.nit < self.maxiter diff --git a/qiskit/algorithms/optimizers/tnc.py b/qiskit/algorithms/optimizers/tnc.py deleted file mode 100644 index 06174e51ace9..000000000000 --- a/qiskit/algorithms/optimizers/tnc.py +++ /dev/null @@ -1,83 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2018, 2020. -# -# 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. - -"""Truncated Newton (TNC) optimizer.""" -from __future__ import annotations - - -from .scipy_optimizer import SciPyOptimizer - - -class TNC(SciPyOptimizer): - """ - Truncated Newton (TNC) optimizer. - - TNC uses a truncated Newton algorithm to minimize a function with variables subject to bounds. - This algorithm uses gradient information; it is also called Newton Conjugate-Gradient. - It differs from the :class:`CG` method as it wraps a C implementation and allows each variable - to be given upper and lower bounds. - - Uses scipy.optimize.minimize TNC - For further detail, please refer to - See https://docs.scipy.org/doc/scipy/reference/generated/scipy.optimize.minimize.html - """ - - _OPTIONS = ["maxiter", "disp", "accuracy", "ftol", "xtol", "gtol", "eps"] - - # pylint: disable=unused-argument - def __init__( - self, - maxiter: int = 100, - disp: bool = False, - accuracy: float = 0, - ftol: float = -1, - xtol: float = -1, - gtol: float = -1, - tol: float | None = None, - eps: float = 1e-08, - options: dict | None = None, - max_evals_grouped: int = 1, - **kwargs, - ) -> None: - """ - Args: - maxiter: Maximum number of function evaluation. - disp: Set to True to print convergence messages. - accuracy: Relative precision for finite difference calculations. - If <= machine_precision, set to sqrt(machine_precision). Defaults to 0. - ftol: Precision goal for the value of f in the stopping criterion. - If ftol < 0.0, ftol is set to 0.0 defaults to -1. - xtol: Precision goal for the value of x in the stopping criterion - (after applying x scaling factors). - If xtol < 0.0, xtol is set to sqrt(machine_precision). Defaults to -1. - gtol: Precision goal for the value of the projected gradient in - the stopping criterion (after applying x scaling factors). - If gtol < 0.0, gtol is set to 1e-2 * sqrt(accuracy). - Setting it to 0.0 is not recommended. Defaults to -1. - tol: Tolerance for termination. - eps: Step size used for numerical approximation of the Jacobian. - options: A dictionary of solver options. - max_evals_grouped: Max number of default gradient evaluations performed simultaneously. - kwargs: additional kwargs for scipy.optimize.minimize. - """ - if options is None: - options = {} - for k, v in list(locals().items()): - if k in self._OPTIONS: - options[k] = v - super().__init__( - "TNC", - options=options, - tol=tol, - max_evals_grouped=max_evals_grouped, - **kwargs, - ) diff --git a/qiskit/algorithms/optimizers/umda.py b/qiskit/algorithms/optimizers/umda.py deleted file mode 100644 index edea27939ade..000000000000 --- a/qiskit/algorithms/optimizers/umda.py +++ /dev/null @@ -1,355 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2018, 2020. -# -# 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. - -"""Univariate Marginal Distribution Algorithm (Estimation-of-Distribution-Algorithm).""" - -from __future__ import annotations - -import warnings - -from collections.abc import Callable -from typing import Any -import numpy as np -from scipy.stats import norm -from qiskit.utils import algorithm_globals - -from .optimizer import OptimizerResult, POINT -from .scipy_optimizer import Optimizer, OptimizerSupportLevel - - -class UMDA(Optimizer): - """Continuous Univariate Marginal Distribution Algorithm (UMDA). - - UMDA [1] is a specific type of Estimation of Distribution Algorithm (EDA) where new individuals - are sampled from univariate normal distributions and are updated in each iteration of the - algorithm by the best individuals found in the previous iteration. - - .. seealso:: - - This original implementation of the UDMA optimizer for Qiskit was inspired by my - (Vicente P. Soloviev) work on the EDAspy Python package [2]. - - EDAs are stochastic search algorithms and belong to the family of the evolutionary algorithms. - The main difference is that EDAs have a probabilistic model which is updated in each iteration - from the best individuals of previous generations (elite selection). Depending on the complexity - of the probabilistic model, EDAs can be classified in different ways. In this case, UMDA is a - univariate EDA as the embedded probabilistic model is univariate. - - UMDA has been compared to some of the already implemented algorithms in Qiskit library to - optimize the parameters of variational algorithms such as QAOA or VQE and competitive results - have been obtained [1]. UMDA seems to provide very good solutions for those circuits in which - the number of layers is not big. - - The optimization process can be personalized depending on the parameters chosen in the - initialization. The main parameter is the population size. The bigger it is, the final result - will be better. However, this increases the complexity of the algorithm and the runtime will - be much heavier. In the work [1] different experiments have been performed where population - size has been set to 20 - 30. - - .. note:: - - The UMDA implementation has more parameters but these have default values for the - initialization for better understanding of the user. For example, ``\alpha`` parameter has - been set to 0.5 and is the percentage of the population which is selected in each iteration - to update the probabilistic model. - - - Example: - - This short example runs UMDA to optimize the parameters of a variational algorithm. Here we - will use the same operator as used in the algorithms introduction, which was originally - computed by Qiskit Nature for an H2 molecule. The minimum energy of the H2 Hamiltonian can - be found quite easily so we are able to set maxiters to a small value. - - .. code-block:: python - - from qiskit.opflow import X, Z, I - from qiskit import Aer - from qiskit.algorithms.optimizers import UMDA - from qiskit.algorithms import QAOA - from qiskit.utils import QuantumInstance - - - H2_op = (-1.052373245772859 * I ^ I) + \ - (0.39793742484318045 * I ^ Z) + \ - (-0.39793742484318045 * Z ^ I) + \ - (-0.01128010425623538 * Z ^ Z) + \ - (0.18093119978423156 * X ^ X) - - p = 2 # Toy example: 2 layers with 2 parameters in each layer: 4 variables - - opt = UMDA(maxiter=100, size_gen=20) - - backend = Aer.get_backend('statevector_simulator') - vqe = QAOA(opt, - quantum_instance=QuantumInstance(backend=backend), - reps=p) - - result = vqe.compute_minimum_eigenvalue(operator=H2_op) - - If it is desired to modify the percentage of individuals considered to update the - probabilistic model, then this code can be used. Here for example we set the 60% instead - of the 50% predefined. - - .. code-block:: python - - opt = UMDA(maxiter=100, size_gen=20, alpha = 0.6) - - backend = Aer.get_backend('statevector_simulator') - vqe = QAOA(opt, - quantum_instance=QuantumInstance(backend=backend), - reps=p) - - result = vqe.compute_minimum_eigenvalue(operator=qubit_op) - - - References: - - [1]: Vicente P. Soloviev, Pedro Larrañaga and Concha Bielza (2022, July). Quantum Parametric - Circuit Optimization with Estimation of Distribution Algorithms. In 2022 The Genetic and - Evolutionary Computation Conference (GECCO). DOI: https://doi.org/10.1145/3520304.3533963 - - [2]: Vicente P. Soloviev. Python package EDAspy. - https://github.com/VicentePerezSoloviev/EDAspy. - """ - - ELITE_FACTOR = 0.4 - STD_BOUND = 0.3 - - def __init__( - self, - maxiter: int = 100, - size_gen: int = 20, - alpha: float = 0.5, - callback: Callable[[int, np.array, float], None] | None = None, - ) -> None: - r""" - Args: - maxiter: Maximum number of iterations. - size_gen: Population size of each generation. - alpha: Percentage (0, 1] of the population to be selected as elite selection. - callback: A callback function passed information in each iteration step. The - information is, in this order: the number of function evaluations, the parameters, - the best function value in this iteration. - """ - - self.size_gen = size_gen - self.maxiter = maxiter - self.alpha = alpha - self._vector: np.ndarray | None = None - # initialization of generation - self._generation: np.ndarray | None = None - self._dead_iter = int(self._maxiter / 5) - - self._truncation_length = int(size_gen * alpha) - - super().__init__() - - self._best_cost_global: float | None = None - self._best_ind_global: int | None = None - self._evaluations: np.ndarray | None = None - - self._n_variables: int | None = None - - self.callback = callback - - def _initialization(self) -> np.ndarray: - vector = np.zeros((4, self._n_variables)) - - vector[0, :] = np.pi # mu - vector[1, :] = 0.5 # std - - return vector - - # build a generation of size SIZE_GEN from prob vector - def _new_generation(self): - """Build a new generation sampled from the vector of probabilities. - Updates the generation pandas dataframe - """ - with warnings.catch_warnings(): - warnings.filterwarnings("ignore", category=DeprecationWarning) - gen = algorithm_globals.random.normal( - self._vector[0, :], self._vector[1, :], [self._size_gen, self._n_variables] - ) - - self._generation = self._generation[: int(self.ELITE_FACTOR * len(self._generation))] - self._generation = np.vstack((self._generation, gen)) - - # truncate the generation at alpha percent - def _truncation(self): - """Selection of the best individuals of the actual generation. - Updates the generation by selecting the best individuals. - """ - best_indices = self._evaluations.argsort()[: self._truncation_length] - self._generation = self._generation[best_indices, :] - self._evaluations = np.take(self._evaluations, best_indices) - - # check each individual of the generation - def _check_generation(self, objective_function): - """Check the cost of each individual in the cost function implemented by the user.""" - self._evaluations = np.apply_along_axis(objective_function, 1, self._generation) - - # update the probability vector - def _update_vector(self): - """From the best individuals update the vector of normal distributions in order to the next - generation can sample from it. Update the vector of normal distributions - """ - - for i in range(self._n_variables): - self._vector[0, i], self._vector[1, i] = norm.fit(self._generation[:, i]) - if self._vector[1, i] < self.STD_BOUND: - self._vector[1, i] = self.STD_BOUND - - def minimize( - self, - fun: Callable[[POINT], float], - x0: POINT, - jac: Callable[[POINT], POINT] | None = None, - bounds: list[tuple[float, float]] | None = None, - ) -> OptimizerResult: - - not_better_count = 0 - result = OptimizerResult() - - if isinstance(x0, float): - x0 = [x0] - self._n_variables = len(x0) - - self._best_cost_global = 999999999999 - self._best_ind_global = 9999999 - history = [] - self._evaluations = np.array(0) - - self._vector = self._initialization() - - with warnings.catch_warnings(): - warnings.filterwarnings("ignore", category=DeprecationWarning) - # initialization of generation - self._generation = algorithm_globals.random.normal( - self._vector[0, :], self._vector[1, :], [self._size_gen, self._n_variables] - ) - - for _ in range(self._maxiter): - self._check_generation(fun) - self._truncation() - self._update_vector() - - best_mae_local: float = min(self._evaluations) - - history.append(best_mae_local) - best_ind_local = np.where(self._evaluations == best_mae_local)[0][0] - best_ind_local = self._generation[best_ind_local] - - # update the best values ever - if best_mae_local < self._best_cost_global: - self._best_cost_global = best_mae_local - self._best_ind_global = best_ind_local - not_better_count = 0 - - else: - not_better_count += 1 - if not_better_count >= self._dead_iter: - break - - if self.callback is not None: - self.callback( - len(history) * self._size_gen, self._best_ind_global, self._best_cost_global - ) - - self._new_generation() - - result.x = self._best_ind_global - result.fun = self._best_cost_global - result.nfev = len(history) * self._size_gen - - return result - - @property - def size_gen(self) -> int: - """Returns the size of the generations (number of individuals per generation)""" - return self._size_gen - - @size_gen.setter - def size_gen(self, value: int): - """ - Sets the size of the generations of the algorithm. - - Args: - value: Size of the generations (number of individuals per generation). - - Raises: - ValueError: If `value` is lower than 1. - """ - if value <= 0: - raise ValueError("The size of the generation should be greater than 0.") - self._size_gen = value - - @property - def maxiter(self) -> int: - """Returns the maximum number of iterations""" - return self._maxiter - - @maxiter.setter - def maxiter(self, value: int): - """ - Sets the maximum number of iterations of the algorithm. - - Args: - value: Maximum number of iterations of the algorithm. - - Raises: - ValueError: If `value` is lower than 1. - """ - if value <= 0: - raise ValueError("The maximum number of iterations should be greater than 0.") - - self._maxiter = value - - @property - def alpha(self) -> float: - """Returns the alpha parameter value (percentage of population selected to update - probabilistic model)""" - return self._alpha - - @alpha.setter - def alpha(self, value: float): - """ - Sets the alpha parameter (percentage of individuals selected to update the probabilistic - model) - - Args: - value: Percentage (0,1] of generation selected to update the probabilistic model. - - Raises: - ValueError: If `value` is lower than 0 or greater than 1. - """ - if (value <= 0) or (value > 1): - raise ValueError(f"alpha must be in the range (0, 1], value given was {value}") - - self._alpha = value - - @property - def settings(self) -> dict[str, Any]: - return { - "maxiter": self.maxiter, - "alpha": self.alpha, - "size_gen": self.size_gen, - "callback": self.callback, - } - - def get_support_level(self): - """Get the support level dictionary.""" - return { - "gradient": OptimizerSupportLevel.ignored, - "bounds": OptimizerSupportLevel.ignored, - "initial_point": OptimizerSupportLevel.required, - } diff --git a/qiskit/algorithms/phase_estimators/__init__.py b/qiskit/algorithms/phase_estimators/__init__.py deleted file mode 100644 index 2ef5b089aaed..000000000000 --- a/qiskit/algorithms/phase_estimators/__init__.py +++ /dev/null @@ -1,31 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2020. -# -# 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. - -"""Phase Estimators.""" - -from .phase_estimator import PhaseEstimator -from .phase_estimation import PhaseEstimation -from .phase_estimation_result import PhaseEstimationResult -from .phase_estimation_scale import PhaseEstimationScale -from .hamiltonian_phase_estimation import HamiltonianPhaseEstimation -from .hamiltonian_phase_estimation_result import HamiltonianPhaseEstimationResult -from .ipe import IterativePhaseEstimation - -__all__ = [ - "PhaseEstimator", - "PhaseEstimation", - "PhaseEstimationResult", - "PhaseEstimationScale", - "HamiltonianPhaseEstimation", - "HamiltonianPhaseEstimationResult", - "IterativePhaseEstimation", -] diff --git a/qiskit/algorithms/phase_estimators/hamiltonian_phase_estimation.py b/qiskit/algorithms/phase_estimators/hamiltonian_phase_estimation.py deleted file mode 100644 index 5301502b48cc..000000000000 --- a/qiskit/algorithms/phase_estimators/hamiltonian_phase_estimation.py +++ /dev/null @@ -1,310 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2020, 2022. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. - -"""Phase estimation for the spectrum of a Hamiltonian""" - -from __future__ import annotations - -import warnings - -from qiskit import QuantumCircuit -from qiskit.utils import QuantumInstance -from qiskit.utils.deprecation import deprecate_arg -from qiskit.opflow import ( - SummedOp, - PauliOp, - MatrixOp, - PauliSumOp, - StateFn, - EvolutionBase, - PauliTrotterEvolution, - I, -) -from qiskit.providers import Backend -from .phase_estimation import PhaseEstimation -from .hamiltonian_phase_estimation_result import HamiltonianPhaseEstimationResult -from .phase_estimation_scale import PhaseEstimationScale -from ...circuit.library import PauliEvolutionGate -from ...primitives import BaseSampler -from ...quantum_info import SparsePauliOp, Statevector, Pauli -from ...synthesis import EvolutionSynthesis - - -class HamiltonianPhaseEstimation: - r"""Run the Quantum Phase Estimation algorithm to find the eigenvalues of a Hermitian operator. - - This class is nearly the same as :class:`~qiskit.algorithms.PhaseEstimation`, differing only - in that the input in that class is a unitary operator, whereas here the input is a Hermitian - operator from which a unitary will be obtained by scaling and exponentiating. The scaling is - performed in order to prevent the phases from wrapping around :math:`2\pi`. - The problem of estimating eigenvalues :math:`\lambda_j` of the Hermitian operator - :math:`H` is solved by running a circuit representing - - .. math:: - - \exp(i b H) |\psi\rangle = \sum_j \exp(i b \lambda_j) c_j |\lambda_j\rangle, - - where the input state is - - .. math:: - - |\psi\rangle = \sum_j c_j |\lambda_j\rangle, - - and :math:`\lambda_j` are the eigenvalues of :math:`H`. - - Here, :math:`b` is a scaling factor sufficiently large to map positive :math:`\lambda` to - :math:`[0,\pi)` and negative :math:`\lambda` to :math:`[\pi,2\pi)`. Each time the circuit is - run, one measures a phase corresponding to :math:`lambda_j` with probability :math:`|c_j|^2`. - - If :math:`H` is a Pauli sum, the bound :math:`b` is computed from the sum of the absolute - values of the coefficients of the terms. There is no way to reliably recover eigenvalues - from phases very near the endpoints of these intervals. Because of this you should be aware - that for degenerate cases, such as :math:`H=Z`, the eigenvalues :math:`\pm 1` will be - mapped to the same phase, :math:`\pi`, and so cannot be distinguished. In this case, you need - to specify a larger bound as an argument to the method ``estimate``. - - This class uses and works together with :class:`~qiskit.algorithms.PhaseEstimationScale` to - manage scaling the Hamiltonian and the phases that are obtained by the QPE algorithm. This - includes setting, or computing, a bound on the eigenvalues of the operator, using this - bound to obtain a scale factor, scaling the operator, and shifting and scaling the measured - phases to recover the eigenvalues. - - Note that, although we speak of "evolving" the state according the Hamiltonian, in the - present algorithm, we are not actually considering time evolution. Rather, the role of time is - played by the scaling factor, which is chosen to best extract the eigenvalues of the - Hamiltonian. - - A few of the ideas in the algorithm may be found in Ref. [1]. - - **Reference:** - - [1]: Quantum phase estimation of multiple eigenvalues for small-scale (noisy) experiments - T.E. O'Brien, B. Tarasinski, B.M. Terhal - `arXiv:1809.09697 `_ - - """ - - @deprecate_arg( - "quantum_instance", - additional_msg=( - "Instead, use the ``sampler`` argument. See https://qisk.it/algo_migration for a " - "migration guide." - ), - since="0.24.0", - package_name="qiskit-terra", - ) - def __init__( - self, - num_evaluation_qubits: int, - quantum_instance: QuantumInstance | Backend | None = None, - sampler: BaseSampler | None = None, - ) -> None: - r""" - Args: - num_evaluation_qubits: The number of qubits used in estimating the phase. The phase will - be estimated as a binary string with this many bits. - quantum_instance: Deprecated: The quantum instance on which - the circuit will be run. - sampler: The sampler primitive on which the circuit will be sampled. - """ - # Avoid double warning on deprecated used of `quantum_instance`. - with warnings.catch_warnings(): - warnings.simplefilter("ignore", DeprecationWarning) - self._phase_estimation = PhaseEstimation( - num_evaluation_qubits=num_evaluation_qubits, - quantum_instance=quantum_instance, - sampler=sampler, - ) - - def _get_scale(self, hamiltonian, bound=None) -> PhaseEstimationScale: - if bound is None: - return PhaseEstimationScale.from_pauli_sum(hamiltonian) - - return PhaseEstimationScale(bound) - - def _get_unitary( - self, hamiltonian, pe_scale, evolution: EvolutionSynthesis | EvolutionBase - ) -> QuantumCircuit: - """Evolve the Hamiltonian to obtain a unitary. - - Apply the scaling to the Hamiltonian that has been computed from an eigenvalue bound - and compute the unitary by applying the evolution object. - """ - - if self._phase_estimation._sampler is not None: - - evo = PauliEvolutionGate(hamiltonian, -pe_scale.scale, synthesis=evolution) - unitary = QuantumCircuit(evo.num_qubits) - unitary.append(evo, unitary.qubits) - - return unitary.decompose().decompose() - else: - # scale so that phase does not wrap. - scaled_hamiltonian = -pe_scale.scale * hamiltonian - unitary = evolution.convert(scaled_hamiltonian.exp_i()) - if not isinstance(unitary, QuantumCircuit): - unitary = unitary.to_circuit() - - return unitary.decompose().decompose() - - # Decomposing twice allows some 1Q Hamiltonians to give correct results - # when using MatrixEvolution(), that otherwise would give incorrect results. - # It does not break any others that we tested. - - def estimate( - self, - hamiltonian: PauliOp | MatrixOp | SummedOp | Pauli | SparsePauliOp | PauliSumOp, - state_preparation: StateFn | QuantumCircuit | Statevector | None = None, - evolution: EvolutionSynthesis | EvolutionBase | None = None, - bound: float | None = None, - ) -> HamiltonianPhaseEstimationResult: - """Run the Hamiltonian phase estimation algorithm. - - Args: - hamiltonian: A Hermitian operator. If the algorithm is used with a ``Sampler`` - primitive, the allowed types are ``Pauli``, ``SparsePauliOp``, and ``PauliSumOp``. - If the algorithm is used with a ``QuantumInstance``, ``PauliOp, ``MatrixOp``, - ``PauliSumOp``, and ``SummedOp`` types are allowed. - state_preparation: The ``StateFn`` to be prepared, whose eigenphase will be - measured. If this parameter is omitted, no preparation circuit will be run and - input state will be the all-zero state in the computational basis. - evolution: An evolution converter that generates a unitary from ``hamiltonian``. If - ``None``, then the default ``PauliTrotterEvolution`` is used. - bound: An upper bound on the absolute value of the eigenvalues of - ``hamiltonian``. If omitted, then ``hamiltonian`` must be a Pauli sum, or a - ``PauliOp``, in which case a bound will be computed. If ``hamiltonian`` - is a ``MatrixOp``, then ``bound`` may not be ``None``. The tighter the bound, - the higher the resolution of computed phases. - - Returns: - ``HamiltonianPhaseEstimationResult`` instance containing the result of the estimation - and diagnostic information. - - Raises: - TypeError: If ``evolution`` is not of type ``EvolutionSynthesis`` when a ``Sampler`` is - provided. - TypeError: If ``hamiltonian`` type is not ``Pauli`` or ``SparsePauliOp`` or - ``PauliSumOp`` when a ``Sampler`` is provided. - ValueError: If ``bound`` is ``None`` and ``hamiltonian`` is not a Pauli sum, i.e. a - ``PauliSumOp`` or a ``SummedOp`` whose terms are of type ``PauliOp``. - TypeError: If ``evolution`` is not of type ``EvolutionBase`` when no ``Sampler`` is - provided. - """ - if self._phase_estimation._sampler is not None: - if evolution is not None and not isinstance(evolution, EvolutionSynthesis): - raise TypeError(f"Expecting type EvolutionSynthesis, got {type(evolution)}") - if not isinstance(hamiltonian, (Pauli, SparsePauliOp, PauliSumOp)): - raise TypeError( - f"Expecting Hamiltonian type Pauli, SparsePauliOp or PauliSumOp, " - f"got {type(hamiltonian)}." - ) - - if isinstance(state_preparation, Statevector): - circuit = QuantumCircuit(state_preparation.num_qubits) - circuit.prepare_state(state_preparation.data) - state_preparation = circuit - if isinstance(hamiltonian, PauliSumOp): - id_coefficient, hamiltonian_no_id = _remove_identity_pauli_sum_op(hamiltonian) - else: - id_coefficient = 0.0 - hamiltonian_no_id = hamiltonian - pe_scale = self._get_scale(hamiltonian_no_id, bound) - unitary = self._get_unitary(hamiltonian_no_id, pe_scale, evolution) - else: - if evolution is None: - evolution = PauliTrotterEvolution() - elif not isinstance(evolution, EvolutionBase): - raise TypeError(f"Expecting type EvolutionBase, got {type(evolution)}") - - if isinstance(hamiltonian, PauliSumOp): - hamiltonian = hamiltonian.to_pauli_op() - elif isinstance(hamiltonian, PauliOp): - hamiltonian = SummedOp([hamiltonian]) - - if isinstance(hamiltonian, SummedOp): - # remove identitiy terms - # The term prop to the identity is removed from hamiltonian. - # This is done for three reasons: - # 1. Work around an unknown bug that otherwise causes the energies to be wrong in some - # cases. - # 2. Allow working with a simpler Hamiltonian, one with fewer terms. - # 3. Tighten the bound on the eigenvalues so that the spectrum is better resolved, i.e. - # occupies more of the range of values representable by the qubit register. - # The coefficient of this term will be added to the eigenvalues. - id_coefficient, hamiltonian_no_id = _remove_identity(hamiltonian) - # get the rescaling object - pe_scale = self._get_scale(hamiltonian_no_id, bound) - - # get the unitary - unitary = self._get_unitary(hamiltonian_no_id, pe_scale, evolution) - - elif isinstance(hamiltonian, MatrixOp): - if bound is None: - raise ValueError("bound must be specified if Hermitian operator is MatrixOp") - - # Do not subtract an identity term from the matrix, so do not compensate. - id_coefficient = 0.0 - pe_scale = self._get_scale(hamiltonian, bound) - unitary = self._get_unitary(hamiltonian, pe_scale, evolution) - else: - raise TypeError(f"Hermitian operator of type {type(hamiltonian)} not supported.") - - if state_preparation is not None and isinstance(state_preparation, StateFn): - state_preparation = state_preparation.to_circuit_op().to_circuit() - # run phase estimation - phase_estimation_result = self._phase_estimation.estimate( - unitary=unitary, state_preparation=state_preparation - ) - return HamiltonianPhaseEstimationResult( - phase_estimation_result=phase_estimation_result, - id_coefficient=id_coefficient, - phase_estimation_scale=pe_scale, - ) - - -def _remove_identity(pauli_sum: SummedOp): - """Remove any identity operators from `pauli_sum`. Return - the sum of the coefficients of the identities and the new operator. - """ - idcoeff = 0.0 - ops = [] - for op in pauli_sum: - p = op.primitive - if p.x.any() or p.z.any(): - ops.append(op) - else: - idcoeff += op.coeff - - return idcoeff, SummedOp(ops) - - -def _remove_identity_pauli_sum_op(pauli_sum: PauliSumOp | SparsePauliOp): - """Remove any identity operators from ``pauli_sum``. Return - the sum of the coefficients of the identities and the new operator. - """ - - def _get_identity(size): - identity = I - for _ in range(size - 1): - identity = identity ^ I - return identity - - idcoeff = 0.0 - if isinstance(pauli_sum, PauliSumOp): - for operator in pauli_sum: - if operator.primitive.paulis == ["I" * pauli_sum.num_qubits]: - idcoeff += operator.primitive.coeffs[0] - pauli_sum = pauli_sum - operator.primitive.coeffs[0] * _get_identity( - pauli_sum.num_qubits - ) - - return idcoeff, pauli_sum.reduce() diff --git a/qiskit/algorithms/phase_estimators/hamiltonian_phase_estimation_result.py b/qiskit/algorithms/phase_estimators/hamiltonian_phase_estimation_result.py deleted file mode 100644 index ce844427b04a..000000000000 --- a/qiskit/algorithms/phase_estimators/hamiltonian_phase_estimation_result.py +++ /dev/null @@ -1,108 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2020. -# -# 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. - -"""Result from running HamiltonianPhaseEstimation""" - -from __future__ import annotations - -from collections.abc import Mapping -from typing import cast -from qiskit.algorithms.algorithm_result import AlgorithmResult -from .phase_estimation_result import PhaseEstimationResult -from .phase_estimation_scale import PhaseEstimationScale - - -class HamiltonianPhaseEstimationResult(AlgorithmResult): - """Store and manipulate results from running `HamiltonianPhaseEstimation`. - - This API of this class is nearly the same as `PhaseEstimatorResult`, differing only in - the presence of an additional keyword argument in the methods. If `scaled` - is `False`, then the phases are not translated and scaled to recover the - eigenvalues of the Hamiltonian. Instead `phi` in :math:`[0, 1)` is returned, - as is the case when then unitary is not derived from a Hamiltonian. - - This class is meant to be instantiated via `HamiltonianPhaseEstimation.estimate`. - """ - - def __init__( - self, - phase_estimation_result: PhaseEstimationResult, - phase_estimation_scale: PhaseEstimationScale, - id_coefficient: float, - ) -> None: - """ - Args: - phase_estimation_result: The result object returned by PhaseEstimation.estimate. - phase_estimation_scale: object used to scale phases to obtain eigenvalues. - id_coefficient: The coefficient of the identity term in the Hamiltonian. - Eigenvalues are computed without this term so that the - coefficient must added to give correct eigenvalues. - This is done automatically when retrieving eigenvalues. - """ - super().__init__() - self._phase_estimation_scale = phase_estimation_scale - self._id_coefficient = id_coefficient - self._phase_estimation_result = phase_estimation_result - - def filter_phases( - self, cutoff: float = 0.0, scaled: bool = True, as_float: bool = True - ) -> Mapping[str | float, float]: - """Filter phases as does `PhaseEstimatorResult.filter_phases`, with - the addition that `phi` is shifted and translated to return eigenvalues - of the Hamiltonian. - - Args: - cutoff: Minimum weight of number of counts required to keep a bit string. - The default value is `0.0`. - scaled: If False, return `phi` in :math:`[0, 1)` rather than the eigenvalues of - the Hamiltonian. - as_float: If `True`, returned keys are floats in :math:`[0.0, 1.0)`. If `False` - returned keys are bit strings. - - Raises: - ValueError: if `as_float` is `False` and `scaled` is `True`. - - Returns: - A dict of filtered phases. - """ - if scaled and not as_float: - raise ValueError("`as_float` must be `True` if `scaled` is `True`.") - - phases = self._phase_estimation_result.filter_phases(cutoff, as_float=as_float) - if scaled: - return cast( - dict, self._phase_estimation_scale.scale_phases(phases, self._id_coefficient) - ) - else: - return cast(dict, phases) - - @property - def phase(self) -> float: - """The most likely phase of the unitary corresponding to the Hamiltonian. - - Returns: - The most likely phase. - """ - return self._phase_estimation_result.phase - - @property - def most_likely_eigenvalue(self) -> float: - """The most likely eigenvalue of the Hamiltonian. - - This method calls `most_likely_phase` and scales the result to - obtain an eigenvalue. - - Returns: - The most likely eigenvalue of the Hamiltonian. - """ - phase = self.phase - return self._phase_estimation_scale.scale_phase(phase, self._id_coefficient) diff --git a/qiskit/algorithms/phase_estimators/ipe.py b/qiskit/algorithms/phase_estimators/ipe.py deleted file mode 100644 index fa7783b32fc5..000000000000 --- a/qiskit/algorithms/phase_estimators/ipe.py +++ /dev/null @@ -1,230 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2021, 2022. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. - - -"""The Iterative Quantum Phase Estimation Algorithm.""" - -from __future__ import annotations - -import numpy - -import qiskit -from qiskit.circuit import QuantumCircuit, QuantumRegister -from qiskit.circuit.classicalregister import ClassicalRegister -from qiskit.providers import Backend -from qiskit.utils import QuantumInstance -from qiskit.utils.deprecation import deprecate_arg -from qiskit.algorithms.exceptions import AlgorithmError -from .phase_estimator import PhaseEstimator -from .phase_estimator import PhaseEstimatorResult -from ...primitives import BaseSampler - - -class IterativePhaseEstimation(PhaseEstimator): - """Run the Iterative quantum phase estimation (QPE) algorithm. - - Given a unitary circuit and a circuit preparing an eigenstate, return the phase of the - eigenvalue as a number in :math:`[0,1)` using the iterative phase estimation algorithm. - - [1]: Dobsicek et al. (2006), Arbitrary accuracy iterative phase estimation algorithm as a two - qubit benchmark, `arxiv/quant-ph/0610214 `_ - """ - - @deprecate_arg( - "quantum_instance", - additional_msg=( - "Instead, use the ``sampler`` argument. See https://qisk.it/algo_migration for a " - "migration guide." - ), - since="0.24.0", - package_name="qiskit-terra", - ) - def __init__( - self, - num_iterations: int, - quantum_instance: QuantumInstance | Backend | None = None, - sampler: BaseSampler | None = None, - ) -> None: - r""" - Args: - num_iterations: The number of iterations (rounds) of the phase estimation to run. - quantum_instance: Deprecated: The quantum instance on which the - circuit will be run. - sampler: The sampler primitive on which the circuit will be sampled. - - Raises: - ValueError: if num_iterations is not greater than zero. - AlgorithmError: If neither sampler nor quantum instance is provided. - """ - if sampler is None and quantum_instance is None: - raise AlgorithmError( - "Neither a sampler nor a quantum instance was provided. Please provide one of them." - ) - if isinstance(quantum_instance, Backend): - quantum_instance = QuantumInstance(quantum_instance) - self._quantum_instance = quantum_instance - if num_iterations <= 0: - raise ValueError("`num_iterations` must be greater than zero.") - self._num_iterations = num_iterations - self._sampler = sampler - - def construct_circuit( - self, - unitary: QuantumCircuit, - state_preparation: QuantumCircuit, - k: int, - omega: float = 0.0, - measurement: bool = False, - ) -> QuantumCircuit: - """Construct the kth iteration Quantum Phase Estimation circuit. - - For details of parameters, see Fig. 2 in https://arxiv.org/pdf/quant-ph/0610214.pdf. - - Args: - unitary: The circuit representing the unitary operator whose eigenvalue (via phase) - will be measured. - state_preparation: The circuit that prepares the state whose eigenphase will be - measured. If this parameter is omitted, no preparation circuit - will be run and input state will be the all-zero state in the - computational basis. - k: the iteration idx. - omega: the feedback angle. - measurement: Boolean flag to indicate if measurement should - be included in the circuit. - - Returns: - QuantumCircuit: the quantum circuit per iteration - """ - k = self._num_iterations if k is None else k - # The auxiliary (phase measurement) qubit - phase_register = QuantumRegister(1, name="a") - eigenstate_register = QuantumRegister(unitary.num_qubits, name="q") - qc = QuantumCircuit(eigenstate_register) - qc.add_register(phase_register) - if isinstance(state_preparation, QuantumCircuit): - qc.append(state_preparation, eigenstate_register) - elif state_preparation is not None: - qc += state_preparation.construct_circuit("circuit", eigenstate_register) - # hadamard on phase_register[0] - qc.h(phase_register[0]) - # controlled-U - # TODO: We may want to allow flexibility in how the power is computed - # For example, it may be desirable to compute the power via Trotterization, if - # we are doing Trotterization anyway. - unitary_power = unitary.power(2 ** (k - 1)).control() - qc = qc.compose(unitary_power, [unitary.num_qubits] + list(range(0, unitary.num_qubits))) - qc.p(omega, phase_register[0]) - # hadamard on phase_register[0] - qc.h(phase_register[0]) - if measurement: - c = ClassicalRegister(1, name="c") - qc.add_register(c) - qc.measure(phase_register, c) - return qc - - def _estimate_phase_iteratively(self, unitary, state_preparation): - """ - Main loop of iterative phase estimation. - """ - omega_coef = 0 - # k runs from the number of iterations back to 1 - for k in range(self._num_iterations, 0, -1): - omega_coef /= 2 - - if self._sampler is not None: - - qc = self.construct_circuit( - unitary, state_preparation, k, -2 * numpy.pi * omega_coef, True - ) - try: - sampler_job = self._sampler.run([qc]) - result = sampler_job.result().quasi_dists[0] - except Exception as exc: - raise AlgorithmError("The primitive job failed!") from exc - x = 1 if result.get(1, 0) > result.get(0, 0) else 0 - - elif self._quantum_instance.is_statevector: - qc = self.construct_circuit( - unitary, state_preparation, k, -2 * numpy.pi * omega_coef, measurement=False - ) - result = self._quantum_instance.execute(qc) - complete_state_vec = result.get_statevector(qc) - ancilla_density_mat = qiskit.quantum_info.partial_trace( - complete_state_vec, range(unitary.num_qubits) - ) - ancilla_density_mat_diag = numpy.diag(ancilla_density_mat) - max_amplitude = max( - ancilla_density_mat_diag.min(), ancilla_density_mat_diag.max(), key=abs - ) - x = numpy.where(ancilla_density_mat_diag == max_amplitude)[0][0] - else: - qc = self.construct_circuit( - unitary, state_preparation, k, -2 * numpy.pi * omega_coef, measurement=True - ) - measurements = self._quantum_instance.execute(qc).get_counts(qc) - x = 1 if measurements.get("1", 0) > measurements.get("0", 0) else 0 - omega_coef = omega_coef + x / 2 - return omega_coef - - # pylint: disable=signature-differs - def estimate( - self, unitary: QuantumCircuit, state_preparation: QuantumCircuit - ) -> "IterativePhaseEstimationResult": - """ - Estimate the eigenphase of the input unitary and initial-state pair. - - Args: - unitary: The circuit representing the unitary operator whose eigenvalue (via phase) - will be measured. - state_preparation: The circuit that prepares the state whose eigenphase will be - measured. If this parameter is omitted, no preparation circuit - will be run and input state will be the all-zero state in the - computational basis. - - Returns: - Estimated phase in an IterativePhaseEstimationResult object. - - Raises: - AlgorithmError: If neither sampler nor quantum instance is provided. - """ - phase = self._estimate_phase_iteratively(unitary, state_preparation) - - return IterativePhaseEstimationResult(self._num_iterations, phase) - - -class IterativePhaseEstimationResult(PhaseEstimatorResult): - """Phase Estimation Result.""" - - def __init__(self, num_iterations: int, phase: float) -> None: - """ - Args: - num_iterations: number of iterations used in the phase estimation. - phase: the estimated phase. - """ - - self._num_iterations = num_iterations - self._phase = phase - - @property - def phase(self) -> float: - r"""Return the estimated phase as a number in :math:`[0.0, 1.0)`. - - 1.0 corresponds to a phase of :math:`2\pi`. It is assumed that the input vector is an - eigenvector of the unitary so that the peak of the probability density occurs at the bit - string that most closely approximates the true phase. - """ - return self._phase - - @property - def num_iterations(self) -> int: - r"""Return the number of iterations used in the estimation algorithm.""" - return self._num_iterations diff --git a/qiskit/algorithms/phase_estimators/phase_estimation.py b/qiskit/algorithms/phase_estimators/phase_estimation.py deleted file mode 100644 index cff8b0e4f7e6..000000000000 --- a/qiskit/algorithms/phase_estimators/phase_estimation.py +++ /dev/null @@ -1,269 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2020, 2022. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. - - -"""The Quantum Phase Estimation Algorithm.""" - -from __future__ import annotations - -import numpy - -from qiskit.circuit import QuantumCircuit -import qiskit -from qiskit import circuit -from qiskit.circuit.classicalregister import ClassicalRegister -from qiskit.providers import Backend -from qiskit.utils import QuantumInstance -from qiskit.utils.deprecation import deprecate_arg -from qiskit.result import Result -from qiskit.algorithms.exceptions import AlgorithmError -from .phase_estimation_result import PhaseEstimationResult, _sort_phases -from .phase_estimator import PhaseEstimator -from ...primitives import BaseSampler - - -class PhaseEstimation(PhaseEstimator): - r"""Run the Quantum Phase Estimation (QPE) algorithm. - - This runs QPE with a multi-qubit register for reading the phases [1] - of input states. - - The algorithm takes as input a unitary :math:`U` and a state :math:`|\psi\rangle`, - which may be written - - .. math:: - - |\psi\rangle = \sum_j c_j |\phi_j\rangle, - - where :math:`|\phi_j\rangle` are eigenstates of :math:`U`. We prepare the quantum register - in the state :math:`|\psi\rangle` then apply :math:`U` leaving the register in the state - - .. math:: - - U|\psi\rangle = \sum_j \exp(i \phi_j) c_j |\phi_j\rangle. - - In the ideal case, one then measures the phase :math:`\phi_j` with probability - :math:`|c_j|^2`. In practice, many (or all) of the bit strings may be measured due to - noise and the possibility that :math:`\phi_j` may not be representable exactly by the - output register. In the latter case the probability for each eigenphase will be spread - across bitstrings, with amplitudes that decrease with distance from the bitstring most - closely approximating the eigenphase. - - The main input to the constructor is the number of qubits in the phase-reading register. - For phase estimation, there are two methods: - - first. `estimate`, which takes a state preparation circuit to prepare an input state, and - a unitary that will act on the input state. In this case, an instance of - :class:`qiskit.circuit.PhaseEstimation`, a QPE circuit, containing - the state preparation and input unitary will be constructed. - second. `estimate_from_pe_circuit`, which takes a quantum-phase-estimation circuit in which - the unitary and state preparation are already embedded. - - In both estimation methods, the QPE circuit is run on a backend - and the frequencies or counts of the phases represented by bitstrings - are recorded. The results are returned as an instance of - :class:`~qiskit.algorithms.phase_estimator_result.PhaseEstimationResult`. - - **Reference:** - - [1]: Michael A. Nielsen and Isaac L. Chuang. 2011. - Quantum Computation and Quantum Information: 10th Anniversary Edition (10th ed.). - Cambridge University Press, New York, NY, USA. - - """ - - @deprecate_arg( - "quantum_instance", - additional_msg=( - "Instead, use the ``sampler`` argument. See https://qisk.it/algo_migration for a " - "migration guide." - ), - since="0.24.0", - package_name="qiskit-terra", - ) - def __init__( - self, - num_evaluation_qubits: int, - quantum_instance: QuantumInstance | Backend | None = None, - sampler: BaseSampler | None = None, - ) -> None: - r""" - Args: - num_evaluation_qubits: The number of qubits used in estimating the phase. The phase will - be estimated as a binary string with this many bits. - quantum_instance: Deprecated: The quantum instance on which the - circuit will be run. - sampler: The sampler primitive on which the circuit will be sampled. - - Raises: - AlgorithmError: If neither sampler nor quantum instance is provided. - """ - if sampler is None and quantum_instance is None: - raise AlgorithmError( - "Neither a sampler nor a quantum instance was provided. Please provide one of them." - ) - self._measurements_added = False - if num_evaluation_qubits is not None: - self._num_evaluation_qubits = num_evaluation_qubits - - if isinstance(quantum_instance, Backend): - quantum_instance = QuantumInstance(quantum_instance) - self._quantum_instance = quantum_instance - self._sampler = sampler - - def construct_circuit( - self, unitary: QuantumCircuit, state_preparation: QuantumCircuit | None = None - ) -> QuantumCircuit: - """Return the circuit to be executed to estimate phases. - - This circuit includes as sub-circuits the core phase estimation circuit, - with the addition of the state-preparation circuit and possibly measurement instructions. - """ - num_evaluation_qubits = self._num_evaluation_qubits - num_unitary_qubits = unitary.num_qubits - - pe_circuit = circuit.library.PhaseEstimation(num_evaluation_qubits, unitary) - - if state_preparation is not None: - pe_circuit.compose( - state_preparation, - qubits=range(num_evaluation_qubits, num_evaluation_qubits + num_unitary_qubits), - inplace=True, - front=True, - ) - - return pe_circuit - - def _add_measurement_if_required(self, pe_circuit): - if self._sampler is not None or not self._quantum_instance.is_statevector: - # Measure only the evaluation qubits. - regname = "meas" - creg = ClassicalRegister(self._num_evaluation_qubits, regname) - pe_circuit.add_register(creg) - pe_circuit.barrier() - pe_circuit.measure( - range(self._num_evaluation_qubits), range(self._num_evaluation_qubits) - ) - - return circuit - - def _compute_phases( - self, num_unitary_qubits: int, circuit_result: Result - ) -> numpy.ndarray | qiskit.result.Counts: - """Compute frequencies/counts of phases from the result of running the QPE circuit. - - How the frequencies are computed depends on whether the backend computes amplitude or - samples outcomes. - - 1) If the backend is a statevector simulator, then the reduced density matrix of the - phase-reading register is computed from the combined phase-reading- and input-state - registers. The elements of the diagonal :math:`(i, i)` give the probability to measure the - each of the states `i`. The index `i` expressed as a binary integer with the LSB rightmost - gives the state of the phase-reading register with the LSB leftmost when interpreted as a - phase. In order to maintain the compact representation, the phases are maintained as decimal - integers. They may be converted to other forms via the results object, - `PhaseEstimationResult` or `HamiltonianPhaseEstimationResult`. - - 2) If the backend samples bitstrings, then the counts are first retrieved as a dict. The - binary strings (the keys) are then reversed so that the LSB is rightmost and the counts are - converted to frequencies. Then the keys are sorted according to increasing phase, so that - they can be easily understood when displaying or plotting a histogram. - - Args: - num_unitary_qubits: The number of qubits in the unitary. - circuit_result: the result object returned by the backend that ran the QPE circuit. - - Returns: - Either a dict or numpy.ndarray representing the frequencies of the phases. - - """ - if self._quantum_instance.is_statevector: - state_vec = circuit_result.get_statevector() - evaluation_density_matrix = qiskit.quantum_info.partial_trace( - state_vec, - range( - self._num_evaluation_qubits, self._num_evaluation_qubits + num_unitary_qubits - ), - ) - phases = evaluation_density_matrix.probabilities() - else: - # return counts with keys sorted numerically - num_shots = circuit_result.results[0].shots - counts = circuit_result.get_counts() - phases = {k[::-1]: counts[k] / num_shots for k in counts.keys()} - phases = _sort_phases(phases) - phases = qiskit.result.Counts( - phases, memory_slots=counts.memory_slots, creg_sizes=counts.creg_sizes - ) - - return phases - - def estimate_from_pe_circuit( - self, pe_circuit: QuantumCircuit, num_unitary_qubits: int - ) -> PhaseEstimationResult: - """Run the phase estimation algorithm on a phase estimation circuit - - Args: - pe_circuit: The phase estimation circuit. - num_unitary_qubits: Must agree with the number of qubits in the unitary in `pe_circuit`. - - Returns: - An instance of qiskit.algorithms.phase_estimator_result.PhaseEstimationResult. - - Raises: - AlgorithmError: Primitive job failed. - """ - - self._add_measurement_if_required(pe_circuit) - - if self._sampler is not None: - try: - circuit_job = self._sampler.run([pe_circuit]) - circuit_result = circuit_job.result() - except Exception as exc: - raise AlgorithmError("The primitive job failed!") from exc - phases = circuit_result.quasi_dists[0] - phases_bitstrings = {} - for key, phase in phases.items(): - bitstring_key = self._get_reversed_bitstring(self._num_evaluation_qubits, key) - phases_bitstrings[bitstring_key] = phase - phases = phases_bitstrings - - else: - circuit_result = self._quantum_instance.execute(pe_circuit) - phases = self._compute_phases(num_unitary_qubits, circuit_result) - return PhaseEstimationResult( - self._num_evaluation_qubits, circuit_result=circuit_result, phases=phases - ) - - def estimate( - self, - unitary: QuantumCircuit, - state_preparation: QuantumCircuit | None = None, - ) -> PhaseEstimationResult: - """Build a phase estimation circuit and run the corresponding algorithm. - - Args: - unitary: The circuit representing the unitary operator whose eigenvalues (via phase) - will be measured. - state_preparation: The circuit that prepares the state whose eigenphase will be - measured. If this parameter is omitted, no preparation circuit - will be run and input state will be the all-zero state in the - computational basis. - - Returns: - An instance of qiskit.algorithms.phase_estimator_result.PhaseEstimationResult. - """ - pe_circuit = self.construct_circuit(unitary, state_preparation) - num_unitary_qubits = unitary.num_qubits - - return self.estimate_from_pe_circuit(pe_circuit, num_unitary_qubits) diff --git a/qiskit/algorithms/phase_estimators/phase_estimation_result.py b/qiskit/algorithms/phase_estimators/phase_estimation_result.py deleted file mode 100644 index 3bac60b012d5..000000000000 --- a/qiskit/algorithms/phase_estimators/phase_estimation_result.py +++ /dev/null @@ -1,175 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2020. -# -# 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. - -"""Result of running PhaseEstimation""" -from __future__ import annotations -import numpy - -from qiskit.utils.deprecation import deprecate_func -from qiskit.result import Result -from .phase_estimator import PhaseEstimatorResult - - -class PhaseEstimationResult(PhaseEstimatorResult): - """Store and manipulate results from running `PhaseEstimation`. - - This class is instantiated by the ``PhaseEstimation`` class, not via user code. - The ``PhaseEstimation`` class generates a list of phases and corresponding weights. Upon - completion it returns the results as an instance of this class. The main method for - accessing the results is `filter_phases`. - - The canonical phase satisfying the ``PhaseEstimator`` interface, returned by the - attribute `phase`, is the most likely phase. - """ - - def __init__( - self, - num_evaluation_qubits: int, - circuit_result: Result, - phases: numpy.ndarray | dict[str, float], - ) -> None: - """ - Args: - num_evaluation_qubits: number of qubits in phase-readout register. - circuit_result: result object returned by method running circuit. - phases: ndarray or dict of phases and frequencies determined by QPE. - """ - super().__init__() - - self._phases = phases - # int: number of qubits in phase-readout register - self._num_evaluation_qubits = num_evaluation_qubits - self._circuit_result = circuit_result - - @property - def phases(self) -> numpy.ndarray | dict: - """Return all phases and their frequencies computed by QPE. - - This is an array or dict whose values correspond to weights on bit strings. - """ - return self._phases - - @property - def circuit_result(self) -> Result: - """Return the result object returned by running the QPE circuit (on hardware or simulator). - - This is useful for inspecting and troubleshooting the QPE algorithm. - """ - return self._circuit_result - - @property - @deprecate_func( - additional_msg="Instead, use the property ``phase``, which behaves the same.", - since="0.18.0", - package_name="qiskit-terra", - is_property=True, - ) - def most_likely_phase(self) -> float: - r"""DEPRECATED - Return the most likely phase as a number in :math:`[0.0, 1.0)`. - - 1.0 corresponds to a phase of :math:`2\pi`. This selects the phase corresponding - to the bit string with the highesest probability. This is the most likely phase. - """ - return self.phase - - @property - def phase(self) -> float: - r"""Return the most likely phase as a number in :math:`[0.0, 1.0)`. - - 1.0 corresponds to a phase of :math:`2\pi`. This selects the phase corresponding - to the bit string with the highesest probability. This is the most likely phase. - """ - if isinstance(self.phases, dict): - binary_phase_string = max(self.phases, key=self.phases.get) - else: - # numpy.argmax ignores complex part of number. But, we take abs anyway - idx = numpy.argmax(abs(self.phases)) - binary_phase_string = numpy.binary_repr(idx, self._num_evaluation_qubits)[::-1] - phase = _bit_string_to_phase(binary_phase_string) - return phase - - def filter_phases(self, cutoff: float = 0.0, as_float: bool = True) -> dict: - """Return a filtered dict of phases (keys) and frequencies (values). - - Only phases with frequencies (counts) larger than `cutoff` are included. - It is assumed that the `run` method has been called so that the phases have been computed. - When using a noiseless, shot-based simulator to read a single phase that can - be represented exactly by `num_evaluation_qubits`, all the weight will - be concentrated on a single phase. In all other cases, many, or all, bit - strings will have non-zero weight. This method is useful for filtering - out these uninteresting bit strings. - - Args: - cutoff: Minimum weight of number of counts required to keep a bit string. - The default value is `0.0`. - as_float: If `True`, returned keys are floats in :math:`[0.0, 1.0)`. If `False` - returned keys are bit strings. - - Returns: - A filtered dict of phases (keys) and frequencies (values). - """ - if isinstance(self.phases, dict): - counts = self.phases - if as_float: - phases = { - _bit_string_to_phase(k): counts[k] for k in counts.keys() if counts[k] > cutoff - } - else: - phases = {k: counts[k] for k in counts.keys() if counts[k] > cutoff} - - else: - phases = {} - for idx, amplitude in enumerate(self.phases): - if amplitude > cutoff: - # Each index corresponds to a computational basis state with the LSB rightmost. - # But, we chose to apply the unitaries such that the phase is recorded - # in reverse order. So, we reverse the bitstrings here. - binary_phase_string = numpy.binary_repr(idx, self._num_evaluation_qubits)[::-1] - if as_float: - _key: str | float = _bit_string_to_phase(binary_phase_string) - else: - _key = binary_phase_string - phases[_key] = amplitude - - phases = _sort_phases(phases) - - return phases - - -def _bit_string_to_phase(binary_string: str) -> float: - """Convert bit string to a normalized phase in :math:`[0,1)`. - - It is assumed that the bit string is correctly padded and that the order of - the bits has been reversed relative to their order when the counts - were recorded. The LSB is the right most when interpreting the bitstring as - a phase. - - Args: - binary_string: A string of characters '0' and '1'. - - Returns: - A phase scaled to :math:`[0,1)`. - """ - n_qubits = len(binary_string) - return int(binary_string, 2) / (2**n_qubits) - - -def _sort_phases(phases: dict) -> dict: - """Sort a dict of bit strings representing phases (keys) and frequencies (values) by bit string. - - The bit strings are sorted according to increasing phase. This relies on Python - preserving insertion order when building dicts. - """ - pkeys = list(phases.keys()) - pkeys.sort(reverse=False) # Sorts in order of the integer encoded by binary string - phases = {k: phases[k] for k in pkeys} - return phases diff --git a/qiskit/algorithms/phase_estimators/phase_estimation_scale.py b/qiskit/algorithms/phase_estimators/phase_estimation_scale.py deleted file mode 100644 index e22b3e18cd9e..000000000000 --- a/qiskit/algorithms/phase_estimators/phase_estimation_scale.py +++ /dev/null @@ -1,160 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2020. -# -# 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. - -"""Scaling for Hamiltonian and eigenvalues to avoid phase wrapping""" -from __future__ import annotations -import numpy as np - -from qiskit.opflow import SummedOp, PauliSumOp -from qiskit.quantum_info import SparsePauliOp, Operator -from qiskit.quantum_info.operators.base_operator import BaseOperator - - -class PhaseEstimationScale: - """Set and use a bound on eigenvalues of a Hermitian operator in order to ensure phases are in - the desired range and to convert measured phases into eigenvectors. - - The ``bound`` is set when constructing this class. Then the method ``scale`` is used to find the - factor by which to scale the operator. - - If ``bound`` is equal exactly to the largest eigenvalue, and the smallest eigenvalue is minus - the largest, then these two eigenvalues will not be distinguished. For example, if the Hermitian - operator is the Pauli Z operator with eigenvalues :math:`1` and :math:`-1`, and ``bound`` is - :math:`1`, then both eigenvalues will be mapped to :math:`1`. - This can be avoided by making ``bound`` a bit larger. - - Increasing ``bound`` decreases the part of the interval :math:`[0, 1)` that is used to map - eigenvalues to ``phi``. However, sometimes this results in a better determination of the - eigenvalues, because 1) although there are fewer discrete phases in the useful range, it may - shift one of the discrete phases closer to the actual phase. And, 2) If one of the discrete - phases is close to, or exactly equal to the actual phase, then artifacts (probability) in - neighboring phases will be reduced. This is important because the artifacts may be larger than - the probability in a phase representing another eigenvalue of interest whose corresponding - eigenstate has a relatively small weight in the input state. - - """ - - def __init__(self, bound: float) -> None: - """ - Args: - bound: an upper bound on the absolute value of the eigenvalues of a Hermitian operator. - (The operator is not needed here.) - """ - self._bound = bound - - @property - def scale(self) -> float: - r"""Return the Hamiltonian scaling factor. - - Return the scale factor by which a Hermitian operator must be multiplied - so that the phase of the corresponding unitary is restricted to :math:`[-\pi, \pi]`. - This factor is computed from the bound on the absolute values of the eigenvalues - of the operator. The methods ``scale_phase`` and ``scale_phases`` are used recover - the eigenvalues corresponding the original (unscaled) Hermitian operator. - - Returns: - The scale factor. - """ - return np.pi / self._bound - - def scale_phase(self, phi: float, id_coefficient: float = 0.0) -> float: - r"""Convert a phase into an eigenvalue. - - The input phase ``phi`` corresponds to the eigenvalue of a unitary obtained by - exponentiating a scaled Hermitian operator. Recall that the phase - is obtained from ``phi`` as :math:`2\pi\phi`. Furthermore, the Hermitian operator - was scaled so that ``phi`` is restricted to :math:`[-1/2, 1/2]`, corresponding to - phases in :math:`[-\pi, \pi]`. But the values of `phi` read from the phase-readout - register are in :math:`[0, 1)`. Any value of ``phi`` greater than :math:`1/2` corresponds - to a raw phase of minus the complement with respect to 1. After this possible - shift, the phase is scaled by the inverse of the factor by which the - Hermitian operator was scaled to recover the eigenvalue of the Hermitian - operator. - - Args: - phi: Normalized phase in :math:`[0, 1)` to be converted to an eigenvalue. - id_coefficient: All eigenvalues are shifted by this value. - - Returns: - An eigenvalue computed from the input phase. - """ - w = 2 * self._bound - if phi <= 0.5: - return phi * w + id_coefficient - else: - return (phi - 1) * w + id_coefficient - - def scale_phases(self, phases: list | dict, id_coefficient: float = 0.0) -> dict | list: - """Convert a list or dict of phases to eigenvalues. - - The values in the list, or keys in the dict, are values of ``phi` and - are converted as described in the description of ``scale_phase``. In case - ``phases`` is a dict, the values of the dict are passed unchanged. - - Args: - phases: a list or dict of values of ``phi``. - id_coefficient: All eigenvalues are shifted by this value. - - Returns: - Eigenvalues computed from phases. - """ - if isinstance(phases, list): - phases = [self.scale_phase(x, id_coefficient) for x in phases] - else: - phases = {self.scale_phase(x, id_coefficient): phases[x] for x in phases.keys()} - - return phases - - @classmethod - def from_pauli_sum( - cls, pauli_sum: SummedOp | PauliSumOp | SparsePauliOp | Operator - ) -> "PhaseEstimationScale" | float: - """Create a PhaseEstimationScale from a `SummedOp` representing a sum of Pauli Operators. - - It is assumed that the ``pauli_sum`` is the sum of ``PauliOp`` objects. The bound on - the absolute value of the eigenvalues of the sum is obtained as the sum of the - absolute values of the coefficients of the terms. This is the best bound available in - the generic case. A ``PhaseEstimationScale`` object is instantiated using this bound. - - Args: - pauli_sum: A ``SummedOp`` whose terms are ``PauliOp`` objects. - - Raises: - ValueError: if ``pauli_sum`` is not a sum of Pauli operators. - - Returns: - A ``PhaseEstimationScale`` object - """ - if isinstance(pauli_sum, PauliSumOp): - bound = abs(pauli_sum.coeff) * sum(abs(coeff) for coeff in pauli_sum.coeffs) - return PhaseEstimationScale(bound) - elif isinstance(pauli_sum, SparsePauliOp): - bound = sum(abs(coeff) for coeff in pauli_sum.coeffs) - return PhaseEstimationScale(bound) - elif isinstance(pauli_sum, Operator): - bound = np.sum(np.abs(np.linalg.eigvalsh(pauli_sum))) - return PhaseEstimationScale(bound) - elif isinstance(pauli_sum, BaseOperator): - raise ValueError( - f"For the operator of type {type(pauli_sum)} the bound needs to be provided in the " - f"algorithm." - ) - else: - if pauli_sum.primitive_strings() != {"Pauli"}: - raise ValueError( - "`pauli_sum` must be a sum of Pauli operators. Got primitives {}.".format( - pauli_sum.primitive_strings() - ) - ) - - bound = abs(pauli_sum.coeff) * sum(abs(pauli.coeff) for pauli in pauli_sum) - return PhaseEstimationScale(bound) diff --git a/qiskit/algorithms/phase_estimators/phase_estimator.py b/qiskit/algorithms/phase_estimators/phase_estimator.py deleted file mode 100644 index 09f8113e5f4a..000000000000 --- a/qiskit/algorithms/phase_estimators/phase_estimator.py +++ /dev/null @@ -1,58 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2020, 2022. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. - -"""The Phase Estimator interface.""" - -from __future__ import annotations -from abc import ABC, abstractmethod -from qiskit.circuit import QuantumCircuit -from qiskit.algorithms.algorithm_result import AlgorithmResult - - -class PhaseEstimator(ABC): - """The Phase Estimator interface. - - Algorithms that can compute a phase for a unitary operator and initial state may implement this - interface to allow different algorithms to be used interchangeably. - - The phase returned is a canonical phase determined by the specific algorithm, such as the most - likely phase. In addition, the algorithm may provide an interface to retrieve phases by other - criteria. - """ - - @abstractmethod - def estimate( - self, - unitary: QuantumCircuit, - state_preparation: QuantumCircuit | None = None, - ) -> "PhaseEstimatorResult": - """Estimate the phase.""" - raise NotImplementedError - - @staticmethod - def _get_reversed_bitstring(length: int, number: int) -> str: - return f"{number:b}".zfill(length)[::-1] - - -class PhaseEstimatorResult(AlgorithmResult): - """Phase Estimator Result.""" - - @property - @abstractmethod - def phase(self) -> float: - r"""Return the estimated phase as a number in :math:`[0.0, 1.0)`. - - 1.0 corresponds to a phase of :math:`2\pi`. In case the phase estimation algorithm - computes more than one phase, this attribute returns a canonical single phase; for - example, the most likely phase. - """ - raise NotImplementedError diff --git a/qiskit/algorithms/state_fidelities/__init__.py b/qiskit/algorithms/state_fidelities/__init__.py deleted file mode 100644 index ea8e4e03bf89..000000000000 --- a/qiskit/algorithms/state_fidelities/__init__.py +++ /dev/null @@ -1,42 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2022. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. -""" -===================================================================== -State Fidelity Interfaces (:mod:`qiskit.algorithms.state_fidelities`) -===================================================================== - -.. currentmodule:: qiskit.algorithms.state_fidelities - -State Fidelities -================ - -.. autosummary:: - :toctree: ../stubs/ - - BaseStateFidelity - ComputeUncompute - -Results -======= - - .. autosummary:: - :toctree: ../stubs/ - - StateFidelityResult - -""" - -from .base_state_fidelity import BaseStateFidelity -from .compute_uncompute import ComputeUncompute -from .state_fidelity_result import StateFidelityResult - -__all__ = ["BaseStateFidelity", "ComputeUncompute", "StateFidelityResult"] diff --git a/qiskit/algorithms/state_fidelities/base_state_fidelity.py b/qiskit/algorithms/state_fidelities/base_state_fidelity.py deleted file mode 100644 index 9395889bc5da..000000000000 --- a/qiskit/algorithms/state_fidelities/base_state_fidelity.py +++ /dev/null @@ -1,308 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2022. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. -""" -Base state fidelity interface -""" - -from __future__ import annotations -from abc import ABC, abstractmethod -from collections.abc import Sequence, Mapping -import numpy as np - -from qiskit import QuantumCircuit -from qiskit.circuit import ParameterVector - -from ..algorithm_job import AlgorithmJob -from .state_fidelity_result import StateFidelityResult - - -class BaseStateFidelity(ABC): - r""" - An interface to calculate state fidelities (state overlaps) for pairs of - (parametrized) quantum circuits. The calculation depends on the particular - fidelity method implementation, but can be always defined as the state overlap: - - .. math:: - - |\langle\psi(x)|\phi(y)\rangle|^2 - - where :math:`x` and :math:`y` are optional parametrizations of the - states :math:`\psi` and :math:`\phi` prepared by the circuits - ``circuit_1`` and ``circuit_2``, respectively. - - """ - - def __init__(self) -> None: - - # use cache for preventing unnecessary circuit compositions - self._circuit_cache: Mapping[tuple[int, int], QuantumCircuit] = {} - - @staticmethod - def _preprocess_values( - circuits: QuantumCircuit | Sequence[QuantumCircuit], - values: Sequence[float] | Sequence[Sequence[float]] | None = None, - ) -> Sequence[Sequence[float]]: - """ - Checks whether the passed values match the shape of the parameters - of the corresponding circuits and formats values to 2D list. - - Args: - circuits: List of circuits to be checked. - values: Parameter values corresponding to the circuits to be checked. - - Returns: - A 2D value list if the values match the circuits, or an empty 2D list - if values is None. - - Raises: - ValueError: if the number of parameter values doesn't match the number of - circuit parameters - TypeError: if the input values are not a sequence. - """ - - if isinstance(circuits, QuantumCircuit): - circuits = [circuits] - - if values is None: - for circuit in circuits: - if circuit.num_parameters != 0: - raise ValueError( - f"`values` cannot be `None` because circuit <{circuit.name}> has " - f"{circuit.num_parameters} free parameters." - ) - return [[]] - else: - - # Support ndarray - if isinstance(values, np.ndarray): - values = values.tolist() - if len(values) > 0 and isinstance(values[0], np.ndarray): - values = [v.tolist() for v in values] - - if not isinstance(values, Sequence): - raise TypeError( - f"Expected a sequence of numerical parameter values, " - f"but got input type {type(values)} instead." - ) - - # ensure 2d - if len(values) > 0 and not isinstance(values[0], Sequence) or len(values) == 0: - values = [values] - - return values - - def _check_qubits_match(self, circuit_1: QuantumCircuit, circuit_2: QuantumCircuit) -> None: - """ - Checks that the number of qubits of 2 circuits matches. - Args: - circuit_1: (Parametrized) quantum circuit. - circuit_2: (Parametrized) quantum circuit. - - Raises: - ValueError: when ``circuit_1`` and ``circuit_2`` don't have the - same number of qubits. - """ - - if circuit_1.num_qubits != circuit_2.num_qubits: - raise ValueError( - f"The number of qubits for the first circuit ({circuit_1.num_qubits}) " - f"and second circuit ({circuit_2.num_qubits}) are not the same." - ) - - @abstractmethod - def create_fidelity_circuit( - self, circuit_1: QuantumCircuit, circuit_2: QuantumCircuit - ) -> QuantumCircuit: - """ - Implementation-dependent method to create a fidelity circuit - from 2 circuit inputs. - - Args: - circuit_1: (Parametrized) quantum circuit. - circuit_2: (Parametrized) quantum circuit. - - Returns: - The fidelity quantum circuit corresponding to ``circuit_1`` and ``circuit_2``. - """ - raise NotImplementedError - - def _construct_circuits( - self, - circuits_1: QuantumCircuit | Sequence[QuantumCircuit], - circuits_2: QuantumCircuit | Sequence[QuantumCircuit], - ) -> Sequence[QuantumCircuit]: - """ - Constructs the list of fidelity circuits to be evaluated. - These circuits represent the state overlap between pairs of input circuits, - and their construction depends on the fidelity method implementations. - - Args: - circuits_1: (Parametrized) quantum circuits. - circuits_2: (Parametrized) quantum circuits. - - Returns: - List of constructed fidelity circuits. - - Raises: - ValueError: if the length of the input circuit lists doesn't match. - """ - - if isinstance(circuits_1, QuantumCircuit): - circuits_1 = [circuits_1] - if isinstance(circuits_2, QuantumCircuit): - circuits_2 = [circuits_2] - - if len(circuits_1) != len(circuits_2): - raise ValueError( - f"The length of the first circuit list({len(circuits_1)}) " - f"and second circuit list ({len(circuits_2)}) is not the same." - ) - - circuits = [] - for (circuit_1, circuit_2) in zip(circuits_1, circuits_2): - - # TODO: improve caching, what if the circuit is modified without changing the id? - circuit = self._circuit_cache.get((id(circuit_1), id(circuit_2))) - - if circuit is not None: - circuits.append(circuit) - else: - self._check_qubits_match(circuit_1, circuit_2) - - # re-parametrize input circuits - # TODO: make smarter checks to avoid unnecesary reparametrizations - parameters_1 = ParameterVector("x", circuit_1.num_parameters) - parametrized_circuit_1 = circuit_1.assign_parameters(parameters_1) - parameters_2 = ParameterVector("y", circuit_2.num_parameters) - parametrized_circuit_2 = circuit_2.assign_parameters(parameters_2) - - circuit = self.create_fidelity_circuit( - parametrized_circuit_1, parametrized_circuit_2 - ) - circuits.append(circuit) - # update cache - self._circuit_cache[id(circuit_1), id(circuit_2)] = circuit - - return circuits - - def _construct_value_list( - self, - circuits_1: Sequence[QuantumCircuit], - circuits_2: Sequence[QuantumCircuit], - values_1: Sequence[float] | Sequence[Sequence[float]] | None = None, - values_2: Sequence[float] | Sequence[Sequence[float]] | None = None, - ) -> list[float]: - """ - Preprocesses input parameter values to match the fidelity - circuit parametrization, and return in list format. - - Args: - circuits_1: (Parametrized) quantum circuits preparing the - first list of quantum states. - circuits_2: (Parametrized) quantum circuits preparing the - second list of quantum states. - values_1: Numerical parameters to be bound to the first circuits. - values_2: Numerical parameters to be bound to the second circuits. - - Returns: - List of parameter values for fidelity circuit. - - """ - values_1 = self._preprocess_values(circuits_1, values_1) - values_2 = self._preprocess_values(circuits_2, values_2) - - values = [] - if len(values_2[0]) == 0: - values = list(values_1) - elif len(values_1[0]) == 0: - values = list(values_2) - else: - for (val_1, val_2) in zip(values_1, values_2): - values.append(val_1 + val_2) - - return values - - @abstractmethod - def _run( - self, - circuits_1: QuantumCircuit | Sequence[QuantumCircuit], - circuits_2: QuantumCircuit | Sequence[QuantumCircuit], - values_1: Sequence[float] | Sequence[Sequence[float]] | None = None, - values_2: Sequence[float] | Sequence[Sequence[float]] | None = None, - **options, - ) -> StateFidelityResult: - r""" - Computes the state overlap (fidelity) calculation between two - (parametrized) circuits (first and second) for a specific set of parameter - values (first and second). - - Args: - circuits_1: (Parametrized) quantum circuits preparing :math:`|\psi\rangle`. - circuits_2: (Parametrized) quantum circuits preparing :math:`|\phi\rangle`. - values_1: Numerical parameters to be bound to the first set of circuits - values_2: Numerical parameters to be bound to the second set of circuits. - options: Primitive backend runtime options used for circuit execution. The order - of priority is\: options in ``run`` method > fidelity's default - options > primitive's default setting. - Higher priority setting overrides lower priority setting. - - Returns: - The result of the fidelity calculation. - """ - raise NotImplementedError - - def run( - self, - circuits_1: QuantumCircuit | Sequence[QuantumCircuit], - circuits_2: QuantumCircuit | Sequence[QuantumCircuit], - values_1: Sequence[float] | Sequence[Sequence[float]] | None = None, - values_2: Sequence[float] | Sequence[Sequence[float]] | None = None, - **options, - ) -> AlgorithmJob: - r""" - Runs asynchronously the state overlap (fidelity) calculation between two - (parametrized) circuits (first and second) for a specific set of parameter - values (first and second). This calculation depends on the particular - fidelity method implementation. - - Args: - circuits_1: (Parametrized) quantum circuits preparing :math:`|\psi\rangle`. - circuits_2: (Parametrized) quantum circuits preparing :math:`|\phi\rangle`. - values_1: Numerical parameters to be bound to the first set of circuits. - values_2: Numerical parameters to be bound to the second set of circuits. - options: Primitive backend runtime options used for circuit execution. The order - of priority is\: options in ``run`` method > fidelity's default - options > primitive's default setting. - Higher priority setting overrides lower priority setting. - - Returns: - Primitive job for the fidelity calculation. - The job's result is an instance of ``StateFidelityResult``. - """ - - job = AlgorithmJob(self._run, circuits_1, circuits_2, values_1, values_2, **options) - - job.submit() - return job - - def _truncate_fidelities(self, fidelities: Sequence[float]) -> Sequence[float]: - """ - Ensures fidelity result in [0,1]. - - Args: - fidelities: Sequence of raw fidelity results. - - Returns: - List of truncated fidelities. - - """ - return np.clip(fidelities, 0, 1).tolist() diff --git a/qiskit/algorithms/state_fidelities/compute_uncompute.py b/qiskit/algorithms/state_fidelities/compute_uncompute.py deleted file mode 100644 index be0879fadc27..000000000000 --- a/qiskit/algorithms/state_fidelities/compute_uncompute.py +++ /dev/null @@ -1,249 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2022, 2023. -# -# 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. -""" -Compute-uncompute fidelity interface using primitives -""" - -from __future__ import annotations -from collections.abc import Sequence -from copy import copy - -from qiskit import QuantumCircuit -from qiskit.primitives import BaseSampler -from qiskit.providers import Options - -from ..exceptions import AlgorithmError -from .base_state_fidelity import BaseStateFidelity -from .state_fidelity_result import StateFidelityResult - - -class ComputeUncompute(BaseStateFidelity): - r""" - This class leverages the sampler primitive to calculate the state - fidelity of two quantum circuits following the compute-uncompute - method (see [1] for further reference). - The fidelity can be defined as the state overlap. - - .. math:: - - |\langle\psi(x)|\phi(y)\rangle|^2 - - where :math:`x` and :math:`y` are optional parametrizations of the - states :math:`\psi` and :math:`\phi` prepared by the circuits - ``circuit_1`` and ``circuit_2``, respectively. - - **Reference:** - [1] Havlíček, V., Córcoles, A. D., Temme, K., Harrow, A. W., Kandala, - A., Chow, J. M., & Gambetta, J. M. (2019). Supervised learning - with quantum-enhanced feature spaces. Nature, 567(7747), 209-212. - `arXiv:1804.11326v2 [quant-ph] `_ - - """ - - def __init__( - self, - sampler: BaseSampler, - options: Options | None = None, - local: bool = False, - ) -> None: - r""" - Args: - sampler: Sampler primitive instance. - options: Primitive backend runtime options used for circuit execution. - The order of priority is: options in ``run`` method > fidelity's - default options > primitive's default setting. - Higher priority setting overrides lower priority setting. - local: If set to ``True``, the fidelity is averaged over - single-qubit projectors - - .. math:: - - \hat{O} = \frac{1}{N}\sum_{i=1}^N|0_i\rangle\langle 0_i|, - - instead of the global projector :math:`|0\rangle\langle 0|^{\otimes n}`. - This coincides with the standard (global) fidelity in the limit of - the fidelity approaching 1. Might be used to increase the variance - to improve trainability in algorithms such as :class:`~.time_evolvers.PVQD`. - - Raises: - ValueError: If the sampler is not an instance of ``BaseSampler``. - """ - if not isinstance(sampler, BaseSampler): - raise ValueError( - f"The sampler should be an instance of BaseSampler, " f"but got {type(sampler)}" - ) - self._sampler: BaseSampler = sampler - self._local = local - self._default_options = Options() - if options is not None: - self._default_options.update_options(**options) - super().__init__() - - def create_fidelity_circuit( - self, circuit_1: QuantumCircuit, circuit_2: QuantumCircuit - ) -> QuantumCircuit: - """ - Combines ``circuit_1`` and ``circuit_2`` to create the - fidelity circuit following the compute-uncompute method. - - Args: - circuit_1: (Parametrized) quantum circuit. - circuit_2: (Parametrized) quantum circuit. - - Returns: - The fidelity quantum circuit corresponding to circuit_1 and circuit_2. - """ - if len(circuit_1.clbits) > 0: - circuit_1.remove_final_measurements() - if len(circuit_2.clbits) > 0: - circuit_2.remove_final_measurements() - - circuit = circuit_1.compose(circuit_2.inverse()) - circuit.measure_all() - return circuit - - def _run( - self, - circuits_1: QuantumCircuit | Sequence[QuantumCircuit], - circuits_2: QuantumCircuit | Sequence[QuantumCircuit], - values_1: Sequence[float] | Sequence[Sequence[float]] | None = None, - values_2: Sequence[float] | Sequence[Sequence[float]] | None = None, - **options, - ) -> StateFidelityResult: - r""" - Computes the state overlap (fidelity) calculation between two - (parametrized) circuits (first and second) for a specific set of parameter - values (first and second) following the compute-uncompute method. - - Args: - circuits_1: (Parametrized) quantum circuits preparing :math:`|\psi\rangle`. - circuits_2: (Parametrized) quantum circuits preparing :math:`|\phi\rangle`. - values_1: Numerical parameters to be bound to the first circuits. - values_2: Numerical parameters to be bound to the second circuits. - options: Primitive backend runtime options used for circuit execution. - The order of priority is: options in ``run`` method > fidelity's - default options > primitive's default setting. - Higher priority setting overrides lower priority setting. - - Returns: - The result of the fidelity calculation. - - Raises: - ValueError: At least one pair of circuits must be defined. - AlgorithmError: If the sampler job is not completed successfully. - """ - - circuits = self._construct_circuits(circuits_1, circuits_2) - if len(circuits) == 0: - raise ValueError( - "At least one pair of circuits must be defined to calculate the state overlap." - ) - values = self._construct_value_list(circuits_1, circuits_2, values_1, values_2) - - # The priority of run options is as follows: - # options in `evaluate` method > fidelity's default options > - # primitive's default options. - opts = copy(self._default_options) - opts.update_options(**options) - - job = self._sampler.run(circuits=circuits, parameter_values=values, **opts.__dict__) - - try: - result = job.result() - except Exception as exc: - raise AlgorithmError("Sampler job failed!") from exc - - if self._local: - raw_fidelities = [ - self._get_local_fidelity(prob_dist, circuit.num_qubits) - for prob_dist, circuit in zip(result.quasi_dists, circuits) - ] - else: - raw_fidelities = [ - self._get_global_fidelity(prob_dist) for prob_dist in result.quasi_dists - ] - fidelities = self._truncate_fidelities(raw_fidelities) - - return StateFidelityResult( - fidelities=fidelities, - raw_fidelities=raw_fidelities, - metadata=result.metadata, - options=self._get_local_options(opts.__dict__), - ) - - @property - def options(self) -> Options: - """Return the union of estimator options setting and fidelity default options, - where, if the same field is set in both, the fidelity's default options override - the primitive's default setting. - - Returns: - The fidelity default + estimator options. - """ - return self._get_local_options(self._default_options.__dict__) - - def update_default_options(self, **options): - """Update the fidelity's default options setting. - - Args: - **options: The fields to update the default options. - """ - - self._default_options.update_options(**options) - - def _get_local_options(self, options: Options) -> Options: - """Return the union of the primitive's default setting, - the fidelity default options, and the options in the ``run`` method. - The order of priority is: options in ``run`` method > fidelity's - default options > primitive's default setting. - - Args: - options: The fields to update the options - - Returns: - The fidelity default + estimator + run options. - """ - opts = copy(self._sampler.options) - opts.update_options(**options) - return opts - - def _get_global_fidelity(self, probability_distribution: dict[int, float]) -> float: - """Process the probability distribution of a measurement to determine the - global fidelity. - - Args: - probability_distribution: Obtained from the measurement result - - Returns: - The global fidelity. - """ - return probability_distribution.get(0, 0) - - def _get_local_fidelity( - self, probability_distribution: dict[int, float], num_qubits: int - ) -> float: - """Process the probability distribution of a measurement to determine the - local fidelity by averaging over single-qubit projectors. - - Args: - probability_distribution: Obtained from the measurement result - - Returns: - The local fidelity. - """ - fidelity = 0.0 - for qubit in range(num_qubits): - for bitstring, prob in probability_distribution.items(): - # Check whether the bit representing the current qubit is 0 - if not bitstring >> qubit & 1: - fidelity += prob / num_qubits - return fidelity diff --git a/qiskit/algorithms/state_fidelities/state_fidelity_result.py b/qiskit/algorithms/state_fidelities/state_fidelity_result.py deleted file mode 100644 index 88dca035f94c..000000000000 --- a/qiskit/algorithms/state_fidelities/state_fidelity_result.py +++ /dev/null @@ -1,37 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2022. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. -""" -Fidelity result class -""" - -from __future__ import annotations - -from collections.abc import Sequence, Mapping -from typing import Any -from dataclasses import dataclass - -from qiskit.providers import Options - - -@dataclass(frozen=True) -class StateFidelityResult: - """This class stores the result of StateFidelity computations.""" - - fidelities: Sequence[float] - """List of truncated fidelity values for each pair of input circuits, ensured to be in [0,1].""" - raw_fidelities: Sequence[float] - """List of raw fidelity values for each pair of input circuits, which might not be in [0,1] - depending on the error mitigation method used.""" - metadata: Sequence[Mapping[str, Any]] - """Additional information about the fidelity calculation.""" - options: Options - """Primitive runtime options for the execution of the fidelity job.""" diff --git a/qiskit/algorithms/time_evolvers/__init__.py b/qiskit/algorithms/time_evolvers/__init__.py deleted file mode 100644 index c2ad1fe7ec26..000000000000 --- a/qiskit/algorithms/time_evolvers/__init__.py +++ /dev/null @@ -1,38 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2022, 2023. -# -# 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. - -"""Quantum Time Evolution package.""" - -from .imaginary_time_evolver import ImaginaryTimeEvolver -from .real_time_evolver import RealTimeEvolver -from .time_evolution_problem import TimeEvolutionProblem -from .time_evolution_result import TimeEvolutionResult -from .trotterization import TrotterQRTE -from .pvqd import PVQD, PVQDResult -from .classical_methods import SciPyImaginaryEvolver, SciPyRealEvolver -from .variational import VarQITE, VarQRTE, VarQTE, VarQTEResult - -__all__ = [ - "ImaginaryTimeEvolver", - "RealTimeEvolver", - "TimeEvolutionProblem", - "TimeEvolutionResult", - "TrotterQRTE", - "PVQD", - "PVQDResult", - "SciPyImaginaryEvolver", - "SciPyRealEvolver", - "VarQITE", - "VarQRTE", - "VarQTE", - "VarQTEResult", -] diff --git a/qiskit/algorithms/time_evolvers/classical_methods/__init__.py b/qiskit/algorithms/time_evolvers/classical_methods/__init__.py deleted file mode 100644 index 266b349fbef7..000000000000 --- a/qiskit/algorithms/time_evolvers/classical_methods/__init__.py +++ /dev/null @@ -1,18 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2022. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. - -"""Classical Methods for Quantum Time Evolution package.""" - -from .scipy_real_evolver import SciPyRealEvolver -from .scipy_imaginary_evolver import SciPyImaginaryEvolver - -__all__ = ["SciPyRealEvolver", "SciPyImaginaryEvolver"] diff --git a/qiskit/algorithms/time_evolvers/classical_methods/evolve.py b/qiskit/algorithms/time_evolvers/classical_methods/evolve.py deleted file mode 100644 index 18c679a03c8f..000000000000 --- a/qiskit/algorithms/time_evolvers/classical_methods/evolve.py +++ /dev/null @@ -1,219 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2022. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. -"""Auxiliary functions for SciPy Time Evolvers""" -from __future__ import annotations -import logging -from scipy.sparse import csr_matrix -from scipy.sparse.linalg import expm_multiply -import numpy as np - -from qiskit.quantum_info.states import Statevector -from qiskit.quantum_info.operators.base_operator import BaseOperator - -from qiskit import QuantumCircuit -from qiskit.opflow import PauliSumOp -from ..time_evolution_problem import TimeEvolutionProblem -from ..time_evolution_result import TimeEvolutionResult -from ...exceptions import AlgorithmError - -from ...list_or_dict import ListOrDict - -logger = logging.getLogger(__name__) - - -def _create_observable_output( - ops_ev_mean: np.ndarray, - evolution_problem: TimeEvolutionProblem, -) -> tuple[ListOrDict[tuple[np.ndarray, np.ndarray]], np.ndarray]: - """Creates the right output format for the evaluated auxiliary operators. - Args: - ops_ev_mean: Array containing the expectation value of each observable at each timestep. - evolution_problem: Time Evolution Problem to create the output of. - - Returns: - An output with the observables mean value at the appropriate times depending on whether - the auxiliary operators in the time evolution problem are a `list` or a `dict`. - - """ - - aux_ops = evolution_problem.aux_operators - - time_array = np.linspace(0, evolution_problem.time, ops_ev_mean.shape[-1]) - zero_array = np.zeros(ops_ev_mean.shape[-1]) # std=0 since it is an exact method - - operators_number = 0 if aux_ops is None else len(aux_ops) - - observable_evolution = [(ops_ev_mean[i], zero_array) for i in range(operators_number)] - - if isinstance(aux_ops, dict): - observable_evolution = dict(zip(aux_ops.keys(), observable_evolution)) - - return observable_evolution, time_array - - -def _create_obs_final( - ops_ev_mean: np.ndarray, - evolution_problem: TimeEvolutionProblem, -) -> ListOrDict[tuple[complex, complex]]: - """Creates the right output format for the final value of the auxiliary operators. - - Args: - ops_ev_mean: Array containing the expectation value of each observable at the final timestep. - evolution_problem: Evolution problem to create the output of. - - Returns: - An output with the observables mean value at the appropriate times depending on whether - the auxiliary operators in the evolution problem are a `list` or a `dict`. - - """ - - aux_ops = evolution_problem.aux_operators - aux_ops_evaluated: ListOrDict[tuple[complex, complex]] = [(op_ev, 0) for op_ev in ops_ev_mean] - if isinstance(aux_ops, dict): - aux_ops_evaluated = dict(zip(aux_ops.keys(), aux_ops_evaluated)) - return aux_ops_evaluated - - -def _evaluate_aux_ops( - aux_ops: list[csr_matrix], - state: np.ndarray, -) -> np.ndarray: - """Evaluates the aux operators if they are provided and stores their value. - - Returns: - Mean of the aux operators for a given state. - """ - op_means = np.array([np.real(state.conjugate().dot(op.dot(state))) for op in aux_ops]) - return op_means - - -def _operator_to_matrix(operator: BaseOperator | PauliSumOp): - - if isinstance(operator, PauliSumOp): - op_matrix = operator.to_spmatrix() - else: - try: - op_matrix = operator.to_matrix(sparse=True) - except TypeError: - logger.debug( - "WARNING: operator of type `%s` does not support sparse matrices. " - "Trying dense computation", - type(operator), - ) - try: - op_matrix = operator.to_matrix() - except AttributeError as ex: - raise AlgorithmError(f"Unsupported operator type `{type(operator)}`.") from ex - return op_matrix - - -def _build_scipy_operators( - evolution_problem: TimeEvolutionProblem, num_timesteps: int, real_time: bool -) -> tuple[np.ndarray, list[csr_matrix], csr_matrix]: - """Returns the matrices and parameters needed for time evolution in the appropriate format. - - Args: - evolution_problem: The definition of the evolution problem. - num_timesteps: Number of timesteps to be performed. - real_time: If `True`, returned operators will correspond to real time evolution, - Else, they will correspond to imaginary time evolution. - - Returns: - A tuple with the initial state, the list of operators to evaluate and the operator to be - exponentiated to perform one timestep. - - Raises: - ValueError: If the Hamiltonian can not be converted into a sparse matrix or dense matrix. - """ - # Convert the initial state and Hamiltonian into sparse matrices. - if isinstance(evolution_problem.initial_state, QuantumCircuit): - state = Statevector(evolution_problem.initial_state).data - else: - state = evolution_problem.initial_state.data - - hamiltonian = _operator_to_matrix(operator=evolution_problem.hamiltonian) - - if isinstance(evolution_problem.aux_operators, list): - aux_ops = [ - _operator_to_matrix(operator=aux_op) for aux_op in evolution_problem.aux_operators - ] - elif isinstance(evolution_problem.aux_operators, dict): - aux_ops = [ - _operator_to_matrix(operator=aux_op) - for aux_op in evolution_problem.aux_operators.values() - ] - else: - aux_ops = [] - timestep = evolution_problem.time / num_timesteps - step_operator = -((1.0j) ** real_time) * timestep * hamiltonian - return state, aux_ops, step_operator - - -def _evolve( - evolution_problem: TimeEvolutionProblem, num_timesteps: int, real_time: bool -) -> TimeEvolutionResult: - r"""Performs either real or imaginary time evolution :math:`\exp(-i t H)|\Psi\rangle`. - - Args: - evolution_problem: The definition of the evolution problem. - num_timesteps: Number of timesteps to be performed. - real_time: If `True`, returned operators will correspond to real time evolution, - Else, they will correspond to imaginary time evolution. - - Returns: - Evolution result which includes an evolved quantum state. - - Raises: - ValueError: If the Hamiltonian is time dependent. - ValueError: If the initial state is `None`. - - """ - if num_timesteps <= 0: - raise ValueError("Variable `num_timesteps` needs to be a positive integer.") - - if evolution_problem.t_param is not None: - raise ValueError("Time dependent Hamiltonians are not supported.") - - if evolution_problem.initial_state is None: - raise ValueError("Initial state is `None`") - - state, aux_ops, step_operator = _build_scipy_operators( - evolution_problem=evolution_problem, num_timesteps=num_timesteps, real_time=real_time - ) - - # Create empty arrays to store the time evolution of the aux operators. - number_operators = ( - 0 if evolution_problem.aux_operators is None else len(evolution_problem.aux_operators) - ) - ops_ev_mean = np.empty(shape=(number_operators, num_timesteps + 1), dtype=complex) - - renormalize = ( - (lambda state: state) if real_time else (lambda state: state / np.linalg.norm(state)) - ) - - # Perform the time evolution and stores the value of the operators at each timestep. - for ts in range(num_timesteps): - ops_ev_mean[:, ts] = _evaluate_aux_ops(aux_ops, state) - state = expm_multiply(A=step_operator, B=state) - state = renormalize(state) - - ops_ev_mean[:, num_timesteps] = _evaluate_aux_ops(aux_ops, state) - - observable_history, times = _create_observable_output(ops_ev_mean, evolution_problem) - aux_ops_evaluated = _create_obs_final(ops_ev_mean[:, -1], evolution_problem) - - return TimeEvolutionResult( - evolved_state=Statevector(state), - aux_ops_evaluated=aux_ops_evaluated, - observables=observable_history, - times=times, - ) diff --git a/qiskit/algorithms/time_evolvers/classical_methods/scipy_imaginary_evolver.py b/qiskit/algorithms/time_evolvers/classical_methods/scipy_imaginary_evolver.py deleted file mode 100644 index f181da10f436..000000000000 --- a/qiskit/algorithms/time_evolvers/classical_methods/scipy_imaginary_evolver.py +++ /dev/null @@ -1,51 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2022. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. - -"""Classical Quantum Imaginary Time Evolution.""" - -from ..time_evolution_problem import TimeEvolutionProblem -from ..time_evolution_result import TimeEvolutionResult -from ..imaginary_time_evolver import ImaginaryTimeEvolver -from .evolve import _evolve - - -class SciPyImaginaryEvolver(ImaginaryTimeEvolver): - r"""Classical Evolver for imaginary time evolution. - - Evolves an initial state :math:`|\Psi\rangle` for an imaginary time :math:`\tau = it` - under a Hamiltonian :math:`H`, as provided in the ``evolution_problem``. - Note that the precision of the evolver does not depend on the number of - timesteps taken. - """ - - def __init__(self, num_timesteps: int): - r""" - Args: - num_timesteps: The number of timesteps in the simulation. - Raises: - ValueError: If `num_timesteps` is not a positive integer. - """ - self.num_timesteps = num_timesteps - - def evolve(self, evolution_problem: TimeEvolutionProblem) -> TimeEvolutionResult: - r"""Perform imaginary time evolution :math:`\exp(-\tau H)|\Psi\rangle`. - - Evolves an initial state :math:`|\Psi\rangle` for an imaginary time :math:`\tau` - under a Hamiltonian :math:`H`, as provided in the ``evolution_problem``. - - Args: - evolution_problem: The definition of the evolution problem. - - Returns: - Evolution result which includes an evolved quantum state. - """ - return _evolve(evolution_problem, self.num_timesteps, real_time=False) diff --git a/qiskit/algorithms/time_evolvers/classical_methods/scipy_real_evolver.py b/qiskit/algorithms/time_evolvers/classical_methods/scipy_real_evolver.py deleted file mode 100644 index b01c16205bfd..000000000000 --- a/qiskit/algorithms/time_evolvers/classical_methods/scipy_real_evolver.py +++ /dev/null @@ -1,50 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2022. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. - -"""Classical Quantum Real Time Evolution.""" -from .evolve import _evolve -from ..time_evolution_problem import TimeEvolutionProblem -from ..time_evolution_result import TimeEvolutionResult -from ..real_time_evolver import RealTimeEvolver - - -class SciPyRealEvolver(RealTimeEvolver): - r"""Classical Evolver for real time evolution. - - Evolves an initial state :math:`|\Psi\rangle` for a time :math:`t` - under a Hamiltonian :math:`H`, as provided in the ``evolution_problem``. - Note that the precision of the evolver does not depend on the number of - timesteps taken. - """ - - def __init__(self, num_timesteps: int): - """ - Args: - num_timesteps: The number of timesteps in the simulation. - Raises: - ValueError: If `steps` is not a positive integer. - """ - self.num_timesteps = num_timesteps - - def evolve(self, evolution_problem: TimeEvolutionProblem) -> TimeEvolutionResult: - r"""Perform real time evolution :math:`\exp(-i t H)|\Psi\rangle`. - - Evolves an initial state :math:`|\Psi\rangle` for a time :math:`t` - under a Hamiltonian :math:`H`, as provided in the ``evolution_problem``. - - Args: - evolution_problem: The definition of the evolution problem. - - Returns: - Evolution result which includes an evolved quantum state. - """ - return _evolve(evolution_problem, self.num_timesteps, real_time=True) diff --git a/qiskit/algorithms/time_evolvers/imaginary_time_evolver.py b/qiskit/algorithms/time_evolvers/imaginary_time_evolver.py deleted file mode 100644 index e62d02e5ab9c..000000000000 --- a/qiskit/algorithms/time_evolvers/imaginary_time_evolver.py +++ /dev/null @@ -1,37 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2021, 2022. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. - -"""Interface for Quantum Imaginary Time Evolution.""" - -from abc import ABC, abstractmethod - -from .time_evolution_problem import TimeEvolutionProblem -from .time_evolution_result import TimeEvolutionResult - - -class ImaginaryTimeEvolver(ABC): - """Interface for Quantum Imaginary Time Evolution.""" - - @abstractmethod - def evolve(self, evolution_problem: TimeEvolutionProblem) -> TimeEvolutionResult: - r"""Perform imaginary time evolution :math:`\exp(-\tau H)|\Psi\rangle`. - - Evolves an initial state :math:`|\Psi\rangle` for an imaginary time :math:`\tau` - under a Hamiltonian :math:`H`, as provided in the ``evolution_problem``. - - Args: - evolution_problem: The definition of the evolution problem. - - Returns: - Evolution result which includes an evolved quantum state. - """ - raise NotImplementedError() diff --git a/qiskit/algorithms/time_evolvers/pvqd/__init__.py b/qiskit/algorithms/time_evolvers/pvqd/__init__.py deleted file mode 100644 index 9377ce631b4e..000000000000 --- a/qiskit/algorithms/time_evolvers/pvqd/__init__.py +++ /dev/null @@ -1,18 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2022. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. - -"""The projected Variational Quantum Dynamic (p-VQD) module.""" - -from .pvqd_result import PVQDResult -from .pvqd import PVQD - -__all__ = ["PVQD", "PVQDResult"] diff --git a/qiskit/algorithms/time_evolvers/pvqd/pvqd.py b/qiskit/algorithms/time_evolvers/pvqd/pvqd.py deleted file mode 100644 index bbd48df86651..000000000000 --- a/qiskit/algorithms/time_evolvers/pvqd/pvqd.py +++ /dev/null @@ -1,435 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2019, 2022, 2023. -# -# 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 projected Variational Quantum Dynamics Algorithm.""" -from __future__ import annotations - -import logging -import warnings -from collections.abc import Callable - -import numpy as np - -from qiskit.circuit import Parameter, ParameterVector, QuantumCircuit -from qiskit.circuit.library import PauliEvolutionGate -from qiskit.opflow import PauliSumOp -from qiskit.primitives import BaseEstimator -from qiskit.quantum_info.operators.base_operator import BaseOperator -from qiskit.synthesis import EvolutionSynthesis, LieTrotter -from qiskit.utils import algorithm_globals - -from ...exceptions import AlgorithmError, QiskitError -from ...optimizers import Minimizer, Optimizer -from ...state_fidelities.base_state_fidelity import BaseStateFidelity -from ..real_time_evolver import RealTimeEvolver -from ..time_evolution_problem import TimeEvolutionProblem -from ..time_evolution_result import TimeEvolutionResult -from .pvqd_result import PVQDResult -from .utils import _get_observable_evaluator, _is_gradient_supported - -logger = logging.getLogger(__name__) - - -class PVQD(RealTimeEvolver): - """The projected Variational Quantum Dynamics (p-VQD) Algorithm. - - In each timestep, this algorithm computes the next state with a Trotter formula - (specified by the ``evolution`` argument) and projects the timestep onto a variational form - (``ansatz``). The projection is determined by maximizing the fidelity of the Trotter-evolved - state and the ansatz, using a classical optimization routine. See Ref. [1] for details. - - The following attributes can be set via the initializer but can also be read and - updated once the PVQD object has been constructed. - - Attributes: - - ansatz (QuantumCircuit): The parameterized circuit representing the time-evolved state. - initial_parameters (np.ndarray): The parameters of the ansatz at time 0. - optimizer (Optional[Union[Optimizer, Minimizer]]): The classical optimization routine - used to maximize the fidelity of the Trotter step and ansatz. - num_timesteps (Optional[int]): The number of timesteps to take. If None, it is automatically - selected to achieve a timestep of approximately 0.01. - evolution (Optional[EvolutionSynthesis]): The method to perform the Trotter step. - Defaults to first-order Lie-Trotter evolution. - use_parameter_shift (bool): If True, use the parameter shift rule for loss function - gradients (if the ansatz supports). - initial_guess (Optional[np.ndarray]): The starting point for the first classical optimization - run, at time 0. Defaults to random values in :math:`[-0.01, 0.01]`. - - Example: - - This snippet computes the real time evolution of a quantum Ising model on two - neighboring sites and keeps track of the magnetization. - - .. code-block:: python - - import numpy as np - - from qiskit.algorithms.state_fidelities import ComputeUncompute - from qiskit.algorithms.time_evolvers import TimeEvolutionProblem, PVQD - from qiskit.primitives import Estimator, Sampler - from qiskit.circuit.library import EfficientSU2 - from qiskit.quantum_info import SparsePauliOp, Pauli - from qiskit.algorithms.optimizers import L_BFGS_B - - sampler = Sampler() - fidelity = ComputeUncompute(sampler) - estimator = Estimator() - hamiltonian = 0.1 * SparsePauliOp(["ZZ", "IX", "XI"]) - observable = Pauli("ZZ") - ansatz = EfficientSU2(2, reps=1) - initial_parameters = np.zeros(ansatz.num_parameters) - - time = 1 - optimizer = L_BFGS_B() - - # setup the algorithm - pvqd = PVQD( - fidelity, - ansatz, - initial_parameters, - estimator, - num_timesteps=100, - optimizer=optimizer, - ) - - # specify the evolution problem - problem = TimeEvolutionProblem( - hamiltonian, time, aux_operators=[hamiltonian, observable] - ) - - # and evolve! - result = pvqd.evolve(problem) - - References: - - [1] Stefano Barison, Filippo Vicentini, and Giuseppe Carleo (2021), An efficient - quantum algorithm for the time evolution of parameterized circuits, - `Quantum 5, 512 `_. - """ - - def __init__( - self, - fidelity: BaseStateFidelity, - ansatz: QuantumCircuit, - initial_parameters: np.ndarray, - estimator: BaseEstimator | None = None, - optimizer: Optimizer | Minimizer | None = None, - num_timesteps: int | None = None, - evolution: EvolutionSynthesis | None = None, - use_parameter_shift: bool = True, - initial_guess: np.ndarray | None = None, - ) -> None: - """ - Args: - fidelity: A fidelity primitive used by the algorithm. - ansatz: A parameterized circuit preparing the variational ansatz to model the - time evolved quantum state. - initial_parameters: The initial parameters for the ansatz. Together with the ansatz, - these define the initial state of the time evolution. - estimator: An estimator primitive used for calculating expected values of auxiliary - operators (if provided via the problem). - optimizer: The classical optimizers used to minimize the overlap between - Trotterization and ansatz. Can be either a :class:`.Optimizer` or a callable - using the :class:`.Minimizer` protocol. This argument is optional since it is - not required for :meth:`get_loss`, but it has to be set before :meth:`evolve` - is called. - num_timesteps: The number of time steps. If ``None`` it will be set such that the - timestep is close to 0.01. - evolution: The evolution synthesis to use for the construction of the Trotter step. - Defaults to first-order Lie-Trotter decomposition, see also - :mod:`~qiskit.synthesis.evolution` for different options. - use_parameter_shift: If True, use the parameter shift rule to compute gradients. - If False, the optimizer will not be passed a gradient callable. In that case, - Qiskit optimizers will use a finite difference rule to approximate the gradients. - initial_guess: The initial guess for the first VQE optimization. Afterwards the - previous iteration result is used as initial guess. If None, this is set to - a random vector with elements in the interval :math:`[-0.01, 0.01]`. - """ - super().__init__() - if evolution is None: - evolution = LieTrotter() - - self.ansatz = ansatz - self.initial_parameters = initial_parameters - self.num_timesteps = num_timesteps - self.optimizer = optimizer - self.initial_guess = initial_guess - self.estimator = estimator - self.fidelity_primitive = fidelity - self.evolution = evolution - self.use_parameter_shift = use_parameter_shift - - def step( - self, - hamiltonian: BaseOperator | PauliSumOp, - ansatz: QuantumCircuit, - theta: np.ndarray, - dt: float, - initial_guess: np.ndarray, - ) -> tuple[np.ndarray, float]: - """Perform a single time step. - - Args: - hamiltonian: The Hamiltonian under which to evolve. - ansatz: The parameterized quantum circuit which attempts to approximate the - time-evolved state. - theta: The current parameters. - dt: The time step. - initial_guess: The initial guess for the classical optimization of the - fidelity between the next variational state and the Trotter-evolved last state. - If None, this is set to a random vector with elements in the interval - :math:`[-0.01, 0.01]`. - - Returns: - A tuple consisting of the next parameters and the fidelity of the optimization. - """ - self._validate_setup() - - loss, gradient = self.get_loss(hamiltonian, ansatz, dt, theta) - - if initial_guess is None: - with warnings.catch_warnings(): - warnings.filterwarnings("ignore", category=DeprecationWarning) - initial_guess = algorithm_globals.random.random(self.initial_parameters.size) * 0.01 - - if isinstance(self.optimizer, Optimizer): - optimizer_result = self.optimizer.minimize(loss, initial_guess, gradient) - else: - optimizer_result = self.optimizer(loss, initial_guess, gradient) - - # clip the fidelity to [0, 1] - fidelity = np.clip(1 - optimizer_result.fun, 0, 1) - - return theta + optimizer_result.x, fidelity - - def get_loss( - self, - hamiltonian: BaseOperator | PauliSumOp, - ansatz: QuantumCircuit, - dt: float, - current_parameters: np.ndarray, - ) -> tuple[Callable[[np.ndarray], float], Callable[[np.ndarray], np.ndarray]] | None: - """Get a function to evaluate the infidelity between Trotter step and ansatz. - - Args: - hamiltonian: The Hamiltonian under which to evolve. - ansatz: The parameterized quantum circuit which attempts to approximate the - time-evolved state. - dt: The time step. - current_parameters: The current parameters. - - Returns: - A callable to evaluate the infidelity and, if gradients are supported and required, - a second callable to evaluate the gradient of the infidelity. - """ - self._validate_setup(skip={"optimizer"}) - - # use Trotterization to evolve the current state - trotterized = ansatz.assign_parameters(current_parameters) - - evolution_gate = PauliEvolutionGate(hamiltonian, time=dt, synthesis=self.evolution) - - trotterized.append(evolution_gate, ansatz.qubits) - - # define the overlap of the Trotterized state and the ansatz - x = ParameterVector("w", ansatz.num_parameters) - shifted = ansatz.assign_parameters(current_parameters + x) - - def evaluate_loss(displacement: np.ndarray | list[np.ndarray]) -> float | np.ndarray: - """Evaluate the overlap of the ansatz with the Trotterized evolution. - - Args: - displacement: The parameters for the ansatz. - - Returns: - The fidelity of the ansatz with parameters ``theta`` and the Trotterized evolution. - - Raises: - AlgorithmError: If a primitive job fails. - """ - if isinstance(displacement, list): - displacement = np.asarray(displacement) - value_dict = {x_i: displacement[:, i].tolist() for i, x_i in enumerate(x)} - else: - value_dict = dict(zip(x, displacement)) - - param_dicts = self._transpose_param_dicts(value_dict) - num_of_param_sets = len(param_dicts) - states1 = [trotterized] * num_of_param_sets - states2 = [shifted] * num_of_param_sets - param_dicts2 = [list(param_dict.values()) for param_dict in param_dicts] - # the first state does not have free parameters so values_1 will be None by default - try: - job = self.fidelity_primitive.run(states1, states2, values_2=param_dicts2) - fidelities = np.array(job.result().fidelities) - except Exception as exc: - raise AlgorithmError("The primitive job failed!") from exc - - if len(fidelities) == 1: - fidelities = fidelities[0] - - # in principle, we could add different loss functions here, but we're currently - # not aware of a use-case for a different one than in the paper - return 1 - fidelities - - if _is_gradient_supported(ansatz) and self.use_parameter_shift: - - def evaluate_gradient(displacement: np.ndarray) -> np.ndarray: - """Evaluate the gradient with the parameter-shift rule. - - This is hardcoded here since the gradient framework does not support computing - gradients for overlaps. - - Args: - displacement: The parameters for the ansatz. - - Returns: - The gradient. - """ - # construct lists where each element is shifted by plus (or minus) pi/2 - dim = displacement.size - plus_shifts = (displacement + np.pi / 2 * np.identity(dim)).tolist() - minus_shifts = (displacement - np.pi / 2 * np.identity(dim)).tolist() - - evaluated = evaluate_loss(plus_shifts + minus_shifts) - - gradient = (evaluated[:dim] - evaluated[dim:]) / 2 - - return gradient - - else: - evaluate_gradient = None - - return evaluate_loss, evaluate_gradient - - def _transpose_param_dicts(self, params: dict) -> list[dict[Parameter, float]]: - p_0 = list(params.values())[0] - if isinstance(p_0, (list, np.ndarray)): - num_parameterizations = len(p_0) - param_bindings = [ - {param: value_list[i] for param, value_list in params.items()} # type: ignore - for i in range(num_parameterizations) - ] - else: - param_bindings = [params] - - return param_bindings - - def evolve(self, evolution_problem: TimeEvolutionProblem) -> TimeEvolutionResult: - r"""Perform real time evolution :math:`\exp(-i t H)|\Psi\rangle`. - - Evolves an initial state :math:`|\Psi\rangle` for a time :math:`t` - under a Hamiltonian :math:`H`, as provided in the ``evolution_problem``. - - Args: - evolution_problem: The evolution problem containing the hamiltonian, total evolution - time and observables to evaluate. - - Returns: - A result object containing the evolution information and evaluated observables. - - Raises: - ValueError: If ``aux_operators`` provided in the time evolution problem but no estimator - provided to the algorithm. - NotImplementedError: If the evolution problem contains an initial state. - """ - self._validate_setup() - - time = evolution_problem.time - observables = evolution_problem.aux_operators - hamiltonian = evolution_problem.hamiltonian - - # determine the number of timesteps and set the timestep - num_timesteps = ( - int(np.ceil(time / 0.01)) if self.num_timesteps is None else self.num_timesteps - ) - timestep = time / num_timesteps - - if evolution_problem.initial_state is not None: - raise NotImplementedError( - "Setting an initial state for the evolution is not yet supported for PVQD." - ) - - # get the function to evaluate the observables for a given set of ansatz parameters - if observables is not None: - if self.estimator is None: - raise ValueError( - "The evolution problem contained aux_operators but no estimator was provided. " - ) - evaluate_observables = _get_observable_evaluator( - self.ansatz, observables, self.estimator - ) - observable_values = [evaluate_observables(self.initial_parameters)] - - fidelities = [1.0] - parameters = [self.initial_parameters] - times = np.linspace(0, time, num_timesteps + 1).tolist() # +1 to include initial time 0 - - initial_guess = self.initial_guess - - for _ in range(num_timesteps): - # perform VQE to find the next parameters - next_parameters, fidelity = self.step( - hamiltonian, self.ansatz, parameters[-1], timestep, initial_guess - ) - - # set initial guess to last parameter update - initial_guess = next_parameters - parameters[-1] - - parameters.append(next_parameters) - fidelities.append(fidelity) - if observables is not None: - observable_values.append(evaluate_observables(next_parameters)) - - evolved_state = self.ansatz.assign_parameters(parameters[-1]) - - result = PVQDResult( - evolved_state=evolved_state, - times=times, - parameters=parameters, - fidelities=fidelities, - estimated_error=1 - np.prod(fidelities), - ) - if observables is not None: - result.observables = observable_values - result.aux_ops_evaluated = observable_values[-1] - - return result - - def _validate_setup(self, skip=None): - """Validate the current setup and raise an error if something misses to run.""" - - if skip is None: - skip = {} - - required_attributes = {"optimizer"}.difference(skip) - - for attr in required_attributes: - if getattr(self, attr, None) is None: - raise ValueError(f"The {attr} cannot be None.") - - if self.num_timesteps is not None and self.num_timesteps <= 0: - raise ValueError( - f"The number of timesteps must be positive but is {self.num_timesteps}." - ) - - if self.ansatz.num_parameters == 0: - raise QiskitError( - "The ansatz cannot have 0 parameters, otherwise it cannot be trained." - ) - - if len(self.initial_parameters) != self.ansatz.num_parameters: - raise QiskitError( - f"Mismatching number of parameters in the ansatz ({self.ansatz.num_parameters}) " - f"and the initial parameters ({len(self.initial_parameters)})." - ) diff --git a/qiskit/algorithms/time_evolvers/pvqd/pvqd_result.py b/qiskit/algorithms/time_evolvers/pvqd/pvqd_result.py deleted file mode 100644 index 65c2a8b18604..000000000000 --- a/qiskit/algorithms/time_evolvers/pvqd/pvqd_result.py +++ /dev/null @@ -1,54 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2022. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. - -"""Result object for p-VQD.""" -from __future__ import annotations - -from collections.abc import Sequence - -import numpy as np -from qiskit.circuit import QuantumCircuit -from ..time_evolution_result import TimeEvolutionResult - - -class PVQDResult(TimeEvolutionResult): - """The result object for the p-VQD algorithm.""" - - def __init__( - self, - evolved_state: QuantumCircuit, - aux_ops_evaluated: list[tuple[complex, complex]] | None = None, - times: list[float] | None = None, - parameters: list[np.ndarray] | None = None, - fidelities: Sequence[float] | None = None, - estimated_error: float | None = None, - observables: list[list[float]] | None = None, - ): - """ - Args: - evolved_state: An evolved quantum state. - aux_ops_evaluated: Optional list of observables for which expected values on an evolved - state are calculated. These values are in fact tuples formatted as (mean, standard - deviation). - times: The times evaluated during the time integration. - parameters: The parameter values at each evaluation time. - fidelities: The fidelity of the Trotter step and variational update at each iteration. - estimated_error: The overall estimated error evaluated as one minus the - product of all fidelities. - observables: The value of the observables evaluated at each iteration. - """ - super().__init__(evolved_state, aux_ops_evaluated) - self.times = times - self.parameters = parameters - self.fidelities = fidelities - self.estimated_error = estimated_error - self.observables = observables diff --git a/qiskit/algorithms/time_evolvers/pvqd/utils.py b/qiskit/algorithms/time_evolvers/pvqd/utils.py deleted file mode 100644 index 9b3f330dd350..000000000000 --- a/qiskit/algorithms/time_evolvers/pvqd/utils.py +++ /dev/null @@ -1,109 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2022. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. - - -"""Utilities for p-VQD.""" -from __future__ import annotations -import logging -from collections.abc import Callable - -import numpy as np - -from qiskit.circuit import QuantumCircuit, Parameter, ParameterExpression -from qiskit.compiler import transpile -from qiskit.exceptions import QiskitError -from qiskit.opflow.gradients.circuit_gradients import ParamShift -from qiskit.primitives import BaseEstimator -from qiskit.quantum_info.operators.base_operator import BaseOperator -from ...exceptions import AlgorithmError - -logger = logging.getLogger(__name__) - - -def _is_gradient_supported(ansatz: QuantumCircuit) -> bool: - """Check whether we can apply a simple parameter shift rule to obtain gradients.""" - - # check whether the circuit can be unrolled to supported gates - try: - unrolled = transpile(ansatz, basis_gates=ParamShift.SUPPORTED_GATES, optimization_level=0) - except QiskitError: - # failed to map to supported basis - logger.log( - logging.INFO, - "No gradient support: Failed to unroll to gates supported by parameter-shift.", - ) - return False - - # check whether all parameters are unique and we do not need to apply the chain rule - # (since it's not implemented yet) - total_num_parameters = 0 - for circuit_instruction in unrolled.data: - for param in circuit_instruction.operation.params: - if isinstance(param, ParameterExpression): - if isinstance(param, Parameter): - total_num_parameters += 1 - else: - logger.log( - logging.INFO, - "No gradient support: Circuit is only allowed to have plain parameters, " - "as the chain rule is not yet implemented.", - ) - return False - - if total_num_parameters != ansatz.num_parameters: - logger.log( - logging.INFO, - "No gradient support: Circuit is only allowed to have unique parameters, " - "as the product rule is not yet implemented.", - ) - return False - - return True - - -def _get_observable_evaluator( - ansatz: QuantumCircuit, - observables: BaseOperator | list[BaseOperator], - estimator: BaseEstimator, -) -> Callable[[np.ndarray], float | list[float]]: - """Get a callable to evaluate a (list of) observable(s) for given circuit parameters.""" - - def evaluate_observables(theta: np.ndarray) -> float | list[float]: - """Evaluate the observables for the ansatz parameters ``theta``. - - Args: - theta: The ansatz parameters. - - Returns: - The observables evaluated at the ansatz parameters. - - Raises: - AlgorithmError: If a primitive job fails. - """ - if isinstance(observables, list): - num_observables = len(observables) - obs = observables - else: - num_observables = 1 - obs = [observables] - states = [ansatz] * num_observables - parameter_values = [theta] * num_observables - - try: - estimator_job = estimator.run(states, obs, parameter_values=parameter_values) - results = estimator_job.result().values - except Exception as exc: - raise AlgorithmError("The primitive job failed!") from exc - - return results - - return evaluate_observables diff --git a/qiskit/algorithms/time_evolvers/real_time_evolver.py b/qiskit/algorithms/time_evolvers/real_time_evolver.py deleted file mode 100644 index 585da953755b..000000000000 --- a/qiskit/algorithms/time_evolvers/real_time_evolver.py +++ /dev/null @@ -1,37 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2021, 2022. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. - -"""Interface for Quantum Real Time Evolution.""" - -from abc import ABC, abstractmethod - -from .time_evolution_problem import TimeEvolutionProblem -from .time_evolution_result import TimeEvolutionResult - - -class RealTimeEvolver(ABC): - """Interface for Quantum Real Time Evolution.""" - - @abstractmethod - def evolve(self, evolution_problem: TimeEvolutionProblem) -> TimeEvolutionResult: - r"""Perform real time evolution :math:`\exp(-i t H)|\Psi\rangle`. - - Evolves an initial state :math:`|\Psi\rangle` for a time :math:`t` - under a Hamiltonian :math:`H`, as provided in the ``evolution_problem``. - - Args: - evolution_problem: The definition of the evolution problem. - - Returns: - Evolution result which includes an evolved quantum state. - """ - raise NotImplementedError() diff --git a/qiskit/algorithms/time_evolvers/time_evolution_problem.py b/qiskit/algorithms/time_evolvers/time_evolution_problem.py deleted file mode 100644 index 87159558baf5..000000000000 --- a/qiskit/algorithms/time_evolvers/time_evolution_problem.py +++ /dev/null @@ -1,114 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2022, 2023. -# -# 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. - -"""Time evolution problem class.""" -from __future__ import annotations - -from collections.abc import Mapping - -from qiskit import QuantumCircuit -from qiskit.circuit import Parameter, ParameterExpression -from qiskit.opflow import PauliSumOp -from ..list_or_dict import ListOrDict -from ...quantum_info import Statevector -from ...quantum_info.operators.base_operator import BaseOperator - - -class TimeEvolutionProblem: - """Time evolution problem class. - - This class is the input to time evolution algorithms and must contain information on the total - evolution time, a quantum state to be evolved and under which Hamiltonian the state is evolved. - - Attributes: - hamiltonian (BaseOperator | PauliSumOp): The Hamiltonian under which to evolve the system. - initial_state (QuantumCircuit | Statevector | None): The quantum state to be evolved for - methods like Trotterization. For variational time evolutions, where the evolution - happens in an ansatz, this argument is not required. - aux_operators (ListOrDict[BaseOperator | PauliSumOp] | None): Optional list of auxiliary - operators to be evaluated with the evolved ``initial_state`` and their expectation - values returned. - truncation_threshold (float): Defines a threshold under which values can be assumed to be 0. - Used when ``aux_operators`` is provided. - t_param (Parameter | None): Time parameter in case of a time-dependent Hamiltonian. This - free parameter must be within the ``hamiltonian``. - param_value_map (dict[Parameter, complex] | None): Maps free parameters in the problem to - values. Depending on the algorithm, it might refer to e.g. a Hamiltonian or an initial - state. - """ - - def __init__( - self, - hamiltonian: BaseOperator | PauliSumOp, - time: float, - initial_state: QuantumCircuit | Statevector | None = None, - aux_operators: ListOrDict[BaseOperator | PauliSumOp] | None = None, - truncation_threshold: float = 1e-12, - t_param: Parameter | None = None, - param_value_map: Mapping[Parameter, complex] | None = None, - ): - """ - Args: - hamiltonian: The Hamiltonian under which to evolve the system. - time: Total time of evolution. - initial_state: The quantum state to be evolved for methods like Trotterization. - For variational time evolutions, where the evolution happens in an ansatz, - this argument is not required. - aux_operators: Optional list of auxiliary operators to be evaluated with the - evolved ``initial_state`` and their expectation values returned. - truncation_threshold: Defines a threshold under which values can be assumed to be 0. - Used when ``aux_operators`` is provided. - t_param: Time parameter in case of a time-dependent Hamiltonian. This - free parameter must be within the ``hamiltonian``. - param_value_map: Maps free parameters in the problem to values. Depending on the - algorithm, it might refer to e.g. a Hamiltonian or an initial state. - - Raises: - ValueError: If non-positive time of evolution is provided. - """ - - self.t_param = t_param - self.param_value_map = param_value_map - self.hamiltonian = hamiltonian - self.time = time - if isinstance(initial_state, Statevector): - circuit = QuantumCircuit(initial_state.num_qubits) - circuit.prepare_state(initial_state.data) - initial_state = circuit - self.initial_state: QuantumCircuit | None = initial_state - self.aux_operators = aux_operators - self.truncation_threshold = truncation_threshold - - @property - def time(self) -> float: - """Returns time.""" - return self._time - - @time.setter - def time(self, time: float) -> None: - """ - Sets time and validates it. - """ - self._time = time - - def validate_params(self) -> None: - """ - Checks if all parameters present in the Hamiltonian are also present in the dictionary - that maps them to values. - - Raises: - ValueError: If Hamiltonian parameters cannot be bound with data provided. - """ - if isinstance(self.hamiltonian, PauliSumOp) and isinstance( - self.hamiltonian.coeff, ParameterExpression - ): - raise ValueError("A global parametrized coefficient for PauliSumOp is not allowed.") diff --git a/qiskit/algorithms/time_evolvers/time_evolution_result.py b/qiskit/algorithms/time_evolvers/time_evolution_result.py deleted file mode 100644 index 8741367f681f..000000000000 --- a/qiskit/algorithms/time_evolvers/time_evolution_result.py +++ /dev/null @@ -1,60 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2021, 2023. -# -# 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. - -"""Class for holding time evolution result.""" -from __future__ import annotations -import numpy as np - -from qiskit import QuantumCircuit -from qiskit.quantum_info import Statevector -from qiskit.algorithms.list_or_dict import ListOrDict -from ..algorithm_result import AlgorithmResult - - -class TimeEvolutionResult(AlgorithmResult): - """ - Class for holding time evolution result. - - Attributes: - evolved_state (QuantumCircuit|Statevector): An evolved quantum state. - aux_ops_evaluated (ListOrDict[tuple[complex, complex]] | None): Optional list of - observables for which expected values on an evolved state are calculated. These values - are in fact tuples formatted as (mean, standard deviation). - observables (ListOrDict[tuple[np.ndarray, np.ndarray]] | None): Optional list of - observables for which expected on an evolved state are calculated at each timestep. - These values are in fact lists of tuples formatted as (mean, standard deviation). - times (np.array | None): Optional list of times at which each observable has been evaluated. - """ - - def __init__( - self, - evolved_state: QuantumCircuit | Statevector, - aux_ops_evaluated: ListOrDict[tuple[complex, complex]] | None = None, - observables: ListOrDict[tuple[np.ndarray, np.ndarray]] | None = None, - times: np.ndarray | None = None, - ): - """ - Args: - evolved_state: An evolved quantum state. - aux_ops_evaluated: Optional list of observables for which expected values on an evolved - state are calculated. These values are in fact tuples formatted as (mean, standard - deviation). - observables: Optional list of observables for which expected values are calculated for - each timestep. These values are in fact tuples formatted as (mean array, standard - deviation array). - times: Optional list of times at which each observable has been evaluated. - """ - - self.evolved_state = evolved_state - self.aux_ops_evaluated = aux_ops_evaluated - self.observables = observables - self.times = times diff --git a/qiskit/algorithms/time_evolvers/trotterization/__init__.py b/qiskit/algorithms/time_evolvers/trotterization/__init__.py deleted file mode 100644 index c5e7e128728d..000000000000 --- a/qiskit/algorithms/time_evolvers/trotterization/__init__.py +++ /dev/null @@ -1,29 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2022. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. -"""This package contains Trotterization-based Quantum Real Time Evolution algorithm. -It is compliant with the new Quantum Time Evolution Framework and makes use of -:class:`qiskit.synthesis.evolution.ProductFormula` and -:class:`~qiskit.circuit.library.PauliEvolutionGate` implementations. - -Trotterization-based Quantum Real Time Evolution ------------------------------------------------- - -.. autosummary:: - :toctree: ../stubs/ - :nosignatures: - - TrotterQRTE -""" - -from qiskit.algorithms.time_evolvers.trotterization.trotter_qrte import TrotterQRTE - -__all__ = ["TrotterQRTE"] diff --git a/qiskit/algorithms/time_evolvers/trotterization/trotter_qrte.py b/qiskit/algorithms/time_evolvers/trotterization/trotter_qrte.py deleted file mode 100644 index cb43e297aed2..000000000000 --- a/qiskit/algorithms/time_evolvers/trotterization/trotter_qrte.py +++ /dev/null @@ -1,246 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2021, 2023. -# -# 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. - -"""An algorithm to implement a Trotterization real time-evolution.""" - -from __future__ import annotations - -from qiskit import QuantumCircuit -from qiskit.algorithms.time_evolvers.time_evolution_problem import TimeEvolutionProblem -from qiskit.algorithms.time_evolvers.time_evolution_result import TimeEvolutionResult -from qiskit.algorithms.time_evolvers.real_time_evolver import RealTimeEvolver -from qiskit.algorithms.observables_evaluator import estimate_observables -from qiskit.opflow import PauliSumOp -from qiskit.circuit.library import PauliEvolutionGate -from qiskit.circuit.parametertable import ParameterView -from qiskit.primitives import BaseEstimator -from qiskit.quantum_info import Pauli, SparsePauliOp -from qiskit.synthesis import ProductFormula, LieTrotter - - -class TrotterQRTE(RealTimeEvolver): - """Quantum Real Time Evolution using Trotterization. - Type of Trotterization is defined by a ``ProductFormula`` provided. - - Examples: - - .. code-block:: python - - from qiskit.opflow import PauliSumOp - from qiskit.quantum_info import Pauli, SparsePauliOp - from qiskit import QuantumCircuit - from qiskit.algorithms import TimeEvolutionProblem - from qiskit.algorithms.time_evolvers import TrotterQRTE - from qiskit.primitives import Estimator - - operator = PauliSumOp(SparsePauliOp([Pauli("X"), Pauli("Z")])) - initial_state = QuantumCircuit(1) - time = 1 - evolution_problem = TimeEvolutionProblem(operator, time, initial_state) - # LieTrotter with 1 rep - estimator = Estimator() - trotter_qrte = TrotterQRTE(estimator=estimator) - evolved_state = trotter_qrte.evolve(evolution_problem).evolved_state - """ - - def __init__( - self, - product_formula: ProductFormula | None = None, - estimator: BaseEstimator | None = None, - num_timesteps: int = 1, - ) -> None: - """ - Args: - product_formula: A Lie-Trotter-Suzuki product formula. If ``None`` provided, the - Lie-Trotter first order product formula with a single repetition is used. ``reps`` - should be 1 to obtain a number of time-steps equal to ``num_timesteps`` and an - evaluation of :attr:`.TimeEvolutionProblem.aux_operators` at every time-step. If ``reps`` - is larger than 1, the true number of time-steps will be ``num_timesteps * reps``. - num_timesteps: The number of time-steps the full evolution time is devided into - (repetitions of ``product_formula``) - estimator: An estimator primitive used for calculating expectation values of - ``TimeEvolutionProblem.aux_operators``. - """ - - self.product_formula = product_formula - self.num_timesteps = num_timesteps - self.estimator = estimator - - @property - def product_formula(self) -> ProductFormula: - """Returns a product formula.""" - return self._product_formula - - @product_formula.setter - def product_formula(self, product_formula: ProductFormula | None): - """Sets a product formula. If ``None`` provided, sets the Lie-Trotter first order product - formula with a single repetition.""" - if product_formula is None: - product_formula = LieTrotter() - self._product_formula = product_formula - - @property - def estimator(self) -> BaseEstimator | None: - """ - Returns an estimator. - """ - return self._estimator - - @estimator.setter - def estimator(self, estimator: BaseEstimator) -> None: - """ - Sets an estimator. - """ - self._estimator = estimator - - @property - def num_timesteps(self) -> int: - """Returns the number of timesteps.""" - return self._num_timesteps - - @num_timesteps.setter - def num_timesteps(self, num_timesteps: int) -> None: - """ - Sets the number of time-steps. - - Raises: - ValueError: If num_timesteps is not positive. - """ - if num_timesteps <= 0: - raise ValueError( - f"Number of time steps must be positive integer, {num_timesteps} provided" - ) - self._num_timesteps = num_timesteps - - @classmethod - def supports_aux_operators(cls) -> bool: - """ - Whether computing the expectation value of auxiliary operators is supported. - - Returns: - ``True`` if ``aux_operators`` expectations in the ``TimeEvolutionProblem`` can be - evaluated, ``False`` otherwise. - """ - return True - - def evolve(self, evolution_problem: TimeEvolutionProblem) -> TimeEvolutionResult: - """ - Evolves a quantum state for a given time using the Trotterization method - based on a product formula provided. The result is provided in the form of a quantum - circuit. If auxiliary operators are included in the ``evolution_problem``, they are - evaluated on the ``init_state`` and on the evolved state at every step (``num_timesteps`` - times) using an estimator primitive provided. - - Args: - evolution_problem: Instance defining evolution problem. For the included Hamiltonian, - ``Pauli`` or ``PauliSumOp`` are supported by TrotterQRTE. - - Returns: - Evolution result that includes an evolved state as a quantum circuit and, optionally, - auxiliary operators evaluated for a resulting state on an estimator primitive. - - Raises: - ValueError: If ``t_param`` is not set to ``None`` in the ``TimeEvolutionProblem`` - (feature not currently supported). - ValueError: If ``aux_operators`` provided in the time evolution problem but no estimator - provided to the algorithm. - ValueError: If the ``initial_state`` is not provided in the ``TimeEvolutionProblem``. - ValueError: If an unsupported Hamiltonian type is provided. - """ - evolution_problem.validate_params() - - if evolution_problem.aux_operators is not None and self.estimator is None: - raise ValueError( - "The time evolution problem contained ``aux_operators`` but no estimator was " - "provided. The algorithm continues without calculating these quantities. " - ) - - # ensure the hamiltonian is a sparse pauli op - hamiltonian = evolution_problem.hamiltonian - if not isinstance(hamiltonian, (Pauli, PauliSumOp, SparsePauliOp)): - raise ValueError( - f"TrotterQRTE only accepts Pauli | PauliSumOp | SparsePauliOp, {type(hamiltonian)} " - "provided." - ) - if isinstance(hamiltonian, PauliSumOp): - hamiltonian = hamiltonian.primitive * hamiltonian.coeff - elif isinstance(hamiltonian, Pauli): - hamiltonian = SparsePauliOp(hamiltonian) - - t_param = evolution_problem.t_param - free_parameters = hamiltonian.parameters - if t_param is not None and free_parameters != ParameterView([t_param]): - raise ValueError( - f"Hamiltonian time parameters ({free_parameters}) do not match " - f"evolution_problem.t_param ({t_param})." - ) - - # make sure PauliEvolutionGate does not implement more than one Trotter step - dt = evolution_problem.time / self.num_timesteps - - if evolution_problem.initial_state is not None: - initial_state = evolution_problem.initial_state - else: - raise ValueError("``initial_state`` must be provided in the ``TimeEvolutionProblem``.") - - evolved_state = QuantumCircuit(initial_state.num_qubits) - evolved_state.append(initial_state, evolved_state.qubits) - - if evolution_problem.aux_operators is not None: - observables = [] - observables.append( - estimate_observables( - self.estimator, - evolved_state, - evolution_problem.aux_operators, - None, - evolution_problem.truncation_threshold, - ) - ) - else: - observables = None - - if t_param is None: - # the evolution gate - single_step_evolution_gate = PauliEvolutionGate( - hamiltonian, dt, synthesis=self.product_formula - ) - - for n in range(self.num_timesteps): - # if hamiltonian is time-dependent, bind new time-value at every step to construct - # evolution for next step - if t_param is not None: - time_value = (n + 1) * dt - bound_hamiltonian = hamiltonian.assign_parameters([time_value]) - single_step_evolution_gate = PauliEvolutionGate( - bound_hamiltonian, - dt, - synthesis=self.product_formula, - ) - evolved_state.append(single_step_evolution_gate, evolved_state.qubits) - - if evolution_problem.aux_operators is not None: - observables.append( - estimate_observables( - self.estimator, - evolved_state, - evolution_problem.aux_operators, - None, - evolution_problem.truncation_threshold, - ) - ) - - evaluated_aux_ops = None - if evolution_problem.aux_operators is not None: - evaluated_aux_ops = observables[-1] - - return TimeEvolutionResult(evolved_state, evaluated_aux_ops, observables) diff --git a/qiskit/algorithms/time_evolvers/variational/__init__.py b/qiskit/algorithms/time_evolvers/variational/__init__.py deleted file mode 100644 index ae22bca5adea..000000000000 --- a/qiskit/algorithms/time_evolvers/variational/__init__.py +++ /dev/null @@ -1,117 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2023. -# -# 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. -""" -Variational Quantum Time Evolutions (:mod:`qiskit.algorithms.time_evolvers.variational`) -======================================================================================== - -Algorithms for performing Variational Quantum Time Evolution of quantum states, -which can be tailored to near-term devices. -:class:`~qiskit.algorithms.time_evolvers.variational.VarQTE` base class exposes an interface, compliant -with the Quantum Time Evolution Framework in Qiskit Terra, that is implemented by -:class:`~qiskit.algorithms.VarQRTE` and :class:`~qiskit.algorithms.VarQITE` classes for real and -imaginary time evolution respectively. The variational approach is taken according to a variational -principle chosen by a user. - -Example: - - .. code-block:: python - - import numpy as np - - from qiskit.algorithms import TimeEvolutionProblem, VarQITE - from qiskit.algorithms.time_evolvers.variational import ImaginaryMcLachlanPrinciple - from qiskit.circuit.library import EfficientSU2 - from qiskit.quantum_info import SparsePauliOp - - observable = SparsePauliOp.from_list( - [ - ("II", 0.2252), - ("ZZ", 0.5716), - ("IZ", 0.3435), - ("ZI", -0.4347), - ("YY", 0.091), - ("XX", 0.091), - ] - ) - - ansatz = EfficientSU2(observable.num_qubits, reps=1) - init_param_values = np.zeros(len(ansatz.parameters)) - for i in range(len(ansatz.parameters)): - init_param_values[i] = np.pi / 2 - var_principle = ImaginaryMcLachlanPrinciple() - time = 1 - evolution_problem = TimeEvolutionProblem(observable, time) - var_qite = VarQITE(ansatz, var_principle, init_param_values) - evolution_result = var_qite.evolve(evolution_problem) - -.. currentmodule:: qiskit.algorithms.time_evolvers.variational - -Variational Principles ----------------------- - -With variational principles we can project time evolution of a quantum state -onto the parameters of a model, in our case a variational quantum circuit. - -They can be divided into two categories: Variational Quantum _Real_ Time Evolution, which evolves -the variational ansatz under the standard Schroediger equation and -Variational Quantum _Imaginary_ Time Evolution, which evolves under the normalized -Wick-rotated Schroedinger equation. - -.. autosummary:: - :toctree: ../stubs/ - :template: autosummary/class_no_inherited_members.rst - - VariationalPrinciple - RealVariationalPrinciple - ImaginaryVariationalPrinciple - RealMcLachlanPrinciple - ImaginaryMcLachlanPrinciple - -ODE solvers ------------ -ODE solvers that implement the SciPy ODE Solver interface. The Forward Euler Solver is -a preferred choice in the presence of noise. One might also use solvers provided by SciPy directly, -e.g. RK45. - -.. autosummary:: - :toctree: ../stubs/ - :template: autosummary/class_no_inherited_members.rst - - ForwardEulerSolver - -""" -from .solvers.ode.forward_euler_solver import ForwardEulerSolver -from .var_qrte import VarQRTE -from .var_qite import VarQITE - -from .var_qte import VarQTE -from .var_qte_result import VarQTEResult -from .variational_principles import ( - VariationalPrinciple, - RealVariationalPrinciple, - ImaginaryVariationalPrinciple, - ImaginaryMcLachlanPrinciple, - RealMcLachlanPrinciple, -) - -__all__ = [ - "ForwardEulerSolver", - "VarQTE", - "VarQTEResult", - "VariationalPrinciple", - "RealVariationalPrinciple", - "ImaginaryVariationalPrinciple", - "RealMcLachlanPrinciple", - "ImaginaryMcLachlanPrinciple", - "VarQITE", - "VarQRTE", -] diff --git a/qiskit/algorithms/time_evolvers/variational/solvers/__init__.py b/qiskit/algorithms/time_evolvers/variational/solvers/__init__.py deleted file mode 100644 index b4537b41e839..000000000000 --- a/qiskit/algorithms/time_evolvers/variational/solvers/__init__.py +++ /dev/null @@ -1,48 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2023. -# -# 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. - -""" -Solvers (:mod:`qiskit.algorithms.time_evolvers.variational.solvers`) -==================================================================== - -This package contains the necessary classes to solve systems of equations arising in the -Variational Quantum Time Evolution. They include ordinary differential equations (ODE) which -describe ansatz parameter propagation and systems of linear equations. - - -Systems of Linear Equations Solver ----------------------------------- - -.. autosummary:: - :toctree: ../stubs/ - :template: autosummary/class_no_inherited_members.rst - - VarQTELinearSolver - - -ODE Solver ----------- -.. autosummary:: - :toctree: ../stubs/ - :template: autosummary/class_no_inherited_members.rst - - VarQTEOdeSolver -""" - -from qiskit.algorithms.time_evolvers.variational.solvers.ode.var_qte_ode_solver import ( - VarQTEOdeSolver, -) -from qiskit.algorithms.time_evolvers.variational.solvers.var_qte_linear_solver import ( - VarQTELinearSolver, -) - -__all__ = ["VarQTELinearSolver", "VarQTEOdeSolver"] diff --git a/qiskit/algorithms/time_evolvers/variational/solvers/ode/__init__.py b/qiskit/algorithms/time_evolvers/variational/solvers/ode/__init__.py deleted file mode 100644 index 06684cb2d012..000000000000 --- a/qiskit/algorithms/time_evolvers/variational/solvers/ode/__init__.py +++ /dev/null @@ -1,13 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2023. -# -# 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. - -"""ODE Solvers""" diff --git a/qiskit/algorithms/time_evolvers/variational/solvers/ode/abstract_ode_function.py b/qiskit/algorithms/time_evolvers/variational/solvers/ode/abstract_ode_function.py deleted file mode 100644 index b94ded552a81..000000000000 --- a/qiskit/algorithms/time_evolvers/variational/solvers/ode/abstract_ode_function.py +++ /dev/null @@ -1,51 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2023. -# -# 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. - -"""Abstract class for generating ODE functions.""" -from __future__ import annotations - -from abc import ABC, abstractmethod -from collections.abc import Mapping, Iterable - -from qiskit.circuit import Parameter - -from ..var_qte_linear_solver import VarQTELinearSolver - - -class AbstractOdeFunction(ABC): - """Abstract class for generating ODE functions.""" - - def __init__( - self, - varqte_linear_solver: VarQTELinearSolver, - param_dict: Mapping[Parameter, float], - t_param: Parameter | None = None, - ) -> None: - - self._varqte_linear_solver = varqte_linear_solver - self._param_dict = param_dict - self._t_param = t_param - - @abstractmethod - def var_qte_ode_function(self, time: float, parameter_values: Iterable) -> Iterable: - """ - Evaluates an ODE function for a given time and parameter values. It is used by an ODE - solver. - - Args: - time: Current time of evolution. - parameter_values: Current values of parameters. - - Returns: - ODE gradient arising from solving a system of linear equations. - """ - pass diff --git a/qiskit/algorithms/time_evolvers/variational/solvers/ode/forward_euler_solver.py b/qiskit/algorithms/time_evolvers/variational/solvers/ode/forward_euler_solver.py deleted file mode 100644 index d48ee5b6c4e1..000000000000 --- a/qiskit/algorithms/time_evolvers/variational/solvers/ode/forward_euler_solver.py +++ /dev/null @@ -1,72 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2023. -# -# 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. -"""Forward Euler ODE solver.""" -from collections.abc import Callable, Sequence - -import numpy as np -from scipy.integrate import OdeSolver -from scipy.integrate._ivp.base import ConstantDenseOutput - - -class ForwardEulerSolver(OdeSolver): - """Forward Euler ODE solver.""" - - def __init__( - self, - function: Callable, - t0: float, - y0: Sequence, - t_bound: float, - vectorized: bool = False, - support_complex: bool = False, - num_t_steps: int = 15, - ): - """ - Forward Euler ODE solver that implements an interface from SciPy. - - Args: - function: Right-hand side of the system. The calling signature is ``fun(t, y)``. Here - ``t`` is a scalar, and there are two options for the ndarray ``y``: - It can either have shape (n,); then ``fun`` must return array_like with - shape (n,). Alternatively it can have shape (n, k); then ``fun`` - must return an array_like with shape (n, k), i.e., each column - corresponds to a single column in ``y``. The choice between the two - options is determined by `vectorized` argument (see below). The - vectorized implementation allows a faster approximation of the Jacobian - by finite differences (required for this solver). - t0: Initial time. - y0: Initial state. - t_bound: Boundary time - the integration won't continue beyond it. It also determines - the direction of the integration. - vectorized: Whether ``fun`` is implemented in a vectorized fashion. Default is False. - support_complex: Whether integration in a complex domain should be supported. - Generally determined by a derived solver class capabilities. Default is False. - num_t_steps: Number of time steps for the forward Euler method. - """ - self._y_old = None - self._step_length = (t_bound - t0) / num_t_steps - super().__init__(function, t0, y0, t_bound, vectorized, support_complex) - - def _step_impl(self): - """ - Takes an Euler step. - """ - try: - self._y_old = self.y - self.y = list(np.add(self.y, self._step_length * self.fun(self.t, self.y))) - self.t += self._step_length - return True, None - except Exception as ex: # pylint: disable=broad-except - return False, f"Unknown ODE solver error: {str(ex)}." - - def _dense_output_impl(self): - return ConstantDenseOutput(self.t_old, self.t, self._y_old) diff --git a/qiskit/algorithms/time_evolvers/variational/solvers/ode/ode_function.py b/qiskit/algorithms/time_evolvers/variational/solvers/ode/ode_function.py deleted file mode 100644 index a7d8453c29b8..000000000000 --- a/qiskit/algorithms/time_evolvers/variational/solvers/ode/ode_function.py +++ /dev/null @@ -1,41 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2023. -# -# 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. - -"""Class for generating ODE functions based on ODE gradients.""" -from collections.abc import Iterable - -from .abstract_ode_function import AbstractOdeFunction - - -class OdeFunction(AbstractOdeFunction): - """Class for generating ODE functions based on ODE gradients.""" - - def var_qte_ode_function(self, time: float, parameter_values: Iterable) -> Iterable: - """ - Evaluates an ODE function for a given time and parameter values. It is used by an ODE - solver. - - Args: - time: Current time of evolution. - parameter_values: Current values of parameters. - - Returns: - ODE gradient arising from solving a system of linear equations. - """ - current_param_dict = dict(zip(self._param_dict.keys(), parameter_values)) - - ode_grad_res, _, _ = self._varqte_linear_solver.solve_lse( - current_param_dict, - time, - ) - - return ode_grad_res diff --git a/qiskit/algorithms/time_evolvers/variational/solvers/ode/ode_function_factory.py b/qiskit/algorithms/time_evolvers/variational/solvers/ode/ode_function_factory.py deleted file mode 100644 index 0d094c4b7950..000000000000 --- a/qiskit/algorithms/time_evolvers/variational/solvers/ode/ode_function_factory.py +++ /dev/null @@ -1,72 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2023. -# -# 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. - -"""Abstract class for generating ODE functions.""" -from __future__ import annotations - -from abc import ABC -from collections.abc import Mapping -from enum import Enum - -from qiskit.circuit import Parameter - -from .abstract_ode_function import AbstractOdeFunction -from .ode_function import OdeFunction - -from ..var_qte_linear_solver import VarQTELinearSolver - - -class OdeFunctionType(Enum): - """Types of ODE functions for VatQTE algorithms.""" - - # Other types may be supported in the future - STANDARD_ODE = "STANDARD_ODE" - - -class OdeFunctionFactory(ABC): - """Factory for building ODE functions.""" - - def __init__(self, ode_function_type: OdeFunctionType = OdeFunctionType.STANDARD_ODE) -> None: - """ - Args: - ode_function_type: An Enum that defines a type of an ODE function to be built. If - not provided, a default ``STANDARD_ODE`` is used. - """ - self._ode_function_type = ode_function_type - - def _build( - self, - varqte_linear_solver: VarQTELinearSolver, - param_dict: Mapping[Parameter, float], - t_param: Parameter | None = None, - ) -> AbstractOdeFunction: - """ - Initializes an ODE function specified in the class. - - Args: - varqte_linear_solver: Solver of LSE for the VarQTE algorithm. - param_dict: Dictionary which relates parameter values to the parameters in the ansatz. - t_param: Time parameter in case of a time-dependent Hamiltonian. - - Returns: - An ODE function. - - Raises: - ValueError: If unsupported ODE function provided. - - """ - if self._ode_function_type == OdeFunctionType.STANDARD_ODE: - return OdeFunction(varqte_linear_solver, param_dict, t_param) - raise ValueError( - f"Unsupported ODE function provided: {self._ode_function_type}." - f" Only {[tp.value for tp in OdeFunctionType]} are supported." - ) diff --git a/qiskit/algorithms/time_evolvers/variational/solvers/ode/var_qte_ode_solver.py b/qiskit/algorithms/time_evolvers/variational/solvers/ode/var_qte_ode_solver.py deleted file mode 100644 index aad1d96155ce..000000000000 --- a/qiskit/algorithms/time_evolvers/variational/solvers/ode/var_qte_ode_solver.py +++ /dev/null @@ -1,89 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2023. -# -# 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. - -"""Class for solving ODEs for Quantum Time Evolution.""" -from __future__ import annotations - -from collections.abc import Sequence -from functools import partial -from typing import Type - -import numpy as np -from scipy.integrate import OdeSolver, solve_ivp - -from .abstract_ode_function import AbstractOdeFunction -from .forward_euler_solver import ForwardEulerSolver - - -class VarQTEOdeSolver: - """Class for solving ODEs for Quantum Time Evolution.""" - - def __init__( - self, - init_params: Sequence[float], - ode_function: AbstractOdeFunction, - ode_solver: Type[OdeSolver] | str = ForwardEulerSolver, - num_timesteps: int | None = None, - ) -> None: - """ - Initialize ODE Solver. - - Args: - init_params: Set of initial parameters for time 0. - ode_function: Generates the ODE function. - ode_solver: ODE solver callable that implements a SciPy ``OdeSolver`` interface or a - string indicating a valid method offered by SciPy. - num_timesteps: The number of timesteps to take. If None, it is - automatically selected to achieve a timestep of approximately 0.01. Only - relevant in case of the ``ForwardEulerSolver``. - """ - self._init_params = init_params - self._ode_function = ode_function.var_qte_ode_function - self._ode_solver = ode_solver - self._num_timesteps = num_timesteps - - def run( - self, evolution_time: float - ) -> tuple[Sequence[float], Sequence[Sequence[float]], Sequence[float]]: - """ - Finds numerical solution with ODE Solver. - - Args: - evolution_time: Evolution time. - - Returns: - List of parameters found by an ODE solver for a given ODE function callable. - """ - # determine the number of timesteps and set the timestep - num_timesteps = ( - int(np.ceil(evolution_time / 0.01)) - if self._num_timesteps is None - else self._num_timesteps - ) - - if self._ode_solver == ForwardEulerSolver: - solve = partial(solve_ivp, num_t_steps=num_timesteps) - else: - solve = solve_ivp - - sol = solve( - self._ode_function, - (0, evolution_time), - self._init_params, - method=self._ode_solver, - ) - - param_vals = sol.y.T - time_points = sol.t - final_param_vals = param_vals[-1] - - return final_param_vals, param_vals, time_points diff --git a/qiskit/algorithms/time_evolvers/variational/solvers/var_qte_linear_solver.py b/qiskit/algorithms/time_evolvers/variational/solvers/var_qte_linear_solver.py deleted file mode 100644 index ec06fba0a685..000000000000 --- a/qiskit/algorithms/time_evolvers/variational/solvers/var_qte_linear_solver.py +++ /dev/null @@ -1,129 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2023. -# -# 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. - -"""Class for solving linear equations for Quantum Time Evolution.""" -from __future__ import annotations - -from collections.abc import Mapping, Sequence, Callable - -import numpy as np - -from qiskit import QuantumCircuit -from qiskit.circuit import Parameter -from qiskit.quantum_info import SparsePauliOp -from qiskit.quantum_info.operators.base_operator import BaseOperator - -from ..variational_principles import VariationalPrinciple - - -class VarQTELinearSolver: - """Class for solving linear equations for Quantum Time Evolution.""" - - def __init__( - self, - var_principle: VariationalPrinciple, - hamiltonian: BaseOperator, - ansatz: QuantumCircuit, - gradient_params: Sequence[Parameter] | None = None, - t_param: Parameter | None = None, - lse_solver: Callable[[np.ndarray, np.ndarray], np.ndarray] | None = None, - imag_part_tol: float = 1e-7, - ) -> None: - """ - Args: - var_principle: Variational Principle to be used. - hamiltonian: Operator used for Variational Quantum Time Evolution. - ansatz: Quantum state in the form of a parametrized quantum circuit. - gradient_params: List of parameters with respect to which gradients should be computed. - If ``None`` given, gradients w.r.t. all parameters will be computed. - t_param: Time parameter in case of a time-dependent Hamiltonian. - lse_solver: Linear system of equations solver callable. It accepts ``A`` and ``b`` to - solve ``Ax=b`` and returns ``x``. If ``None``, the default ``np.linalg.lstsq`` - solver is used. - imag_part_tol: Allowed value of an imaginary part that can be neglected if no - imaginary part is expected. - - Raises: - TypeError: If t_param is provided and Hamiltonian is not of type SparsePauliOp. - """ - self._var_principle = var_principle - self._hamiltonian = hamiltonian - self._ansatz = ansatz - self._gradient_params = gradient_params - self._bind_params = gradient_params - self._time_param = t_param - self.lse_solver = lse_solver - self._imag_part_tol = imag_part_tol - - if self._time_param is not None and not isinstance(self._hamiltonian, SparsePauliOp): - raise TypeError( - f"A time parameter {t_param} has been specified, so a time-dependent " - f"hamiltonian is expected. The operator provided is of type {type(self._hamiltonian)}, " - f"which might not support parametrization. " - f"Please provide the parametrized hamiltonian as a SparsePauliOp." - ) - - @property - def lse_solver(self) -> Callable[[np.ndarray, np.ndarray], np.ndarray]: - """Returns an LSE solver callable.""" - return self._lse_solver - - @lse_solver.setter - def lse_solver(self, lse_solver: Callable[[np.ndarray, np.ndarray], np.ndarray] | None) -> None: - """Sets an LSE solver. Uses a ``np.linalg.lstsq`` callable if ``None`` provided.""" - if lse_solver is None: - lse_solver = lambda a, b: np.linalg.lstsq(a, b, rcond=1e-2)[0] - - self._lse_solver = lse_solver - - def solve_lse( - self, - param_dict: Mapping[Parameter, float], - time_value: float | None = None, - ) -> tuple[np.ndarray, np.ndarray, np.ndarray]: - """ - Solve the system of linear equations underlying McLachlan's variational principle for the - calculation without error bounds. - - Args: - param_dict: Dictionary which relates parameter values to the parameters in the ansatz. - time_value: Time value that will be bound to ``t_param``. It is required if ``t_param`` - is not ``None``. - - Returns: - Solution to the LSE, A from Ax=b, b from Ax=b. - - Raises: - ValueError: If no time value is provided for time dependent hamiltonians. - - """ - param_values = list(param_dict.values()) - metric_tensor_lse_lhs = self._var_principle.metric_tensor(self._ansatz, param_values) - hamiltonian = self._hamiltonian - - if self._time_param is not None: - if time_value is not None: - hamiltonian = hamiltonian.assign_parameters([time_value]) - else: - raise ValueError( - "Providing a time_value is required for time-dependent hamiltonians, " - f"but got time_value = {time_value}. " - "Please provide a time_value to the solve_lse method." - ) - - evolution_grad_lse_rhs = self._var_principle.evolution_gradient( - hamiltonian, self._ansatz, param_values, self._gradient_params - ) - - x = self._lse_solver(metric_tensor_lse_lhs, evolution_grad_lse_rhs) - - return np.real(x), metric_tensor_lse_lhs, evolution_grad_lse_rhs diff --git a/qiskit/algorithms/time_evolvers/variational/var_qite.py b/qiskit/algorithms/time_evolvers/variational/var_qite.py deleted file mode 100644 index 4200389c83cf..000000000000 --- a/qiskit/algorithms/time_evolvers/variational/var_qite.py +++ /dev/null @@ -1,120 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2023. -# -# 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. - -"""Variational Quantum Imaginary Time Evolution algorithm.""" -from __future__ import annotations - -from collections.abc import Mapping, Sequence -from typing import Type, Callable - -import numpy as np -from scipy.integrate import OdeSolver - -from qiskit import QuantumCircuit -from qiskit.circuit import Parameter -from qiskit.primitives import BaseEstimator - -from .solvers.ode.forward_euler_solver import ForwardEulerSolver - -from .variational_principles import ImaginaryVariationalPrinciple, ImaginaryMcLachlanPrinciple -from .var_qte import VarQTE - -from ..imaginary_time_evolver import ImaginaryTimeEvolver - - -class VarQITE(VarQTE, ImaginaryTimeEvolver): - """Variational Quantum Imaginary Time Evolution algorithm. - - .. code-block::python - - import numpy as np - - from qiskit.algorithms import TimeEvolutionProblem, VarQITE - from qiskit.algorithms.time_evolvers.variational import ImaginaryMcLachlanPrinciple - from qiskit.circuit.library import EfficientSU2 - from qiskit.quantum_info import SparsePauliOp, Pauli - from qiskit.primitives import Estimator - - observable = SparsePauliOp.from_list( - [ - ("II", 0.2252), - ("ZZ", 0.5716), - ("IZ", 0.3435), - ("ZI", -0.4347), - ("YY", 0.091), - ("XX", 0.091), - ] - ) - - ansatz = EfficientSU2(observable.num_qubits, reps=1) - init_param_values = np.ones(len(ansatz.parameters)) * np.pi/2 - var_principle = ImaginaryMcLachlanPrinciple() - time = 1 - - # without evaluating auxiliary operators - evolution_problem = TimeEvolutionProblem(observable, time) - var_qite = VarQITE(ansatz, init_param_values, var_principle) - evolution_result = var_qite.evolve(evolution_problem) - - # evaluating auxiliary operators - aux_ops = [Pauli("XX"), Pauli("YZ")] - evolution_problem = TimeEvolutionProblem(observable, time, aux_operators=aux_ops) - var_qite = VarQITE(ansatz, init_param_values, var_principle, Estimator()) - evolution_result = var_qite.evolve(evolution_problem) - """ - - def __init__( - self, - ansatz: QuantumCircuit, - initial_parameters: Mapping[Parameter, float] | Sequence[float], - variational_principle: ImaginaryVariationalPrinciple | None = None, - estimator: BaseEstimator | None = None, - ode_solver: Type[OdeSolver] | str = ForwardEulerSolver, - lse_solver: Callable[[np.ndarray, np.ndarray], np.ndarray] | None = None, - num_timesteps: int | None = None, - imag_part_tol: float = 1e-7, - num_instability_tol: float = 1e-7, - ) -> None: - r""" - Args: - ansatz: Ansatz to be used for variational time evolution. - initial_parameters: Initial parameter values for the ansatz. - variational_principle: Variational Principle to be used. Defaults to - ``ImaginaryMcLachlanPrinciple``. - estimator: An estimator primitive used for calculating expectation values of - TimeEvolutionProblem.aux_operators. - ode_solver: ODE solver callable that implements a SciPy ``OdeSolver`` interface or a - string indicating a valid method offered by SciPy. - lse_solver: Linear system of equations solver callable. It accepts ``A`` and ``b`` to - solve ``Ax=b`` and returns ``x``. If ``None``, the default ``np.linalg.lstsq`` - solver is used. - num_timesteps: The number of timesteps to take. If ``None``, it is - automatically selected to achieve a timestep of approximately 0.01. Only - relevant in case of the ``ForwardEulerSolver``. - imag_part_tol: Allowed value of an imaginary part that can be neglected if no - imaginary part is expected. - num_instability_tol: The amount of negative value that is allowed to be - rounded up to 0 for quantities that are expected to be non-negative. - """ - if variational_principle is None: - variational_principle = ImaginaryMcLachlanPrinciple() - super().__init__( - ansatz, - initial_parameters, - variational_principle, - estimator, - ode_solver, - lse_solver=lse_solver, - num_timesteps=num_timesteps, - imag_part_tol=imag_part_tol, - num_instability_tol=num_instability_tol, - ) diff --git a/qiskit/algorithms/time_evolvers/variational/var_qrte.py b/qiskit/algorithms/time_evolvers/variational/var_qrte.py deleted file mode 100644 index f8305f643cb5..000000000000 --- a/qiskit/algorithms/time_evolvers/variational/var_qrte.py +++ /dev/null @@ -1,122 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2023. -# -# 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. - -"""Variational Quantum Real Time Evolution algorithm.""" -from __future__ import annotations - -from collections.abc import Mapping, Sequence -from typing import Type, Callable - -import numpy as np -from scipy.integrate import OdeSolver - -from qiskit import QuantumCircuit -from qiskit.circuit import Parameter -from qiskit.primitives import BaseEstimator - -from .solvers.ode.forward_euler_solver import ForwardEulerSolver - -from .variational_principles import RealVariationalPrinciple, RealMcLachlanPrinciple -from .var_qte import VarQTE - -from ..real_time_evolver import RealTimeEvolver - - -class VarQRTE(VarQTE, RealTimeEvolver): - """Variational Quantum Real Time Evolution algorithm. - - .. code-block::python - - import numpy as np - - from qiskit.algorithms import TimeEvolutionProblem, VarQRTE - from qiskit.circuit.library import EfficientSU2 - from qiskit.algorithms.time_evolvers.variational import RealMcLachlanPrinciple - from qiskit.quantum_info import SparsePauliOp - from qiskit.quantum_info import SparsePauliOp, Pauli - from qiskit.primitives import Estimator - - observable = SparsePauliOp.from_list( - [ - ("II", 0.2252), - ("ZZ", 0.5716), - ("IZ", 0.3435), - ("ZI", -0.4347), - ("YY", 0.091), - ("XX", 0.091), - ] - ) - - ansatz = EfficientSU2(observable.num_qubits, reps=1) - init_param_values = np.ones(len(ansatz.parameters)) * np.pi/2 - var_principle = RealMcLachlanPrinciple() - time = 1 - - # without evaluating auxiliary operators - evolution_problem = TimeEvolutionProblem(observable, time) - var_qrte = VarQRTE(ansatz, init_param_values, var_principle) - evolution_result = var_qrte.evolve(evolution_problem) - - # evaluating auxiliary operators - aux_ops = [Pauli("XX"), Pauli("YZ")] - evolution_problem = TimeEvolutionProblem(observable, time, aux_operators=aux_ops) - var_qrte = VarQRTE(ansatz, init_param_values, var_principle, Estimator()) - evolution_result = var_qrte.evolve(evolution_problem) - """ - - def __init__( - self, - ansatz: QuantumCircuit, - initial_parameters: Mapping[Parameter, float] | Sequence[float], - variational_principle: RealVariationalPrinciple | None = None, - estimator: BaseEstimator | None = None, - ode_solver: Type[OdeSolver] | str = ForwardEulerSolver, - lse_solver: Callable[[np.ndarray, np.ndarray], np.ndarray] | None = None, - num_timesteps: int | None = None, - imag_part_tol: float = 1e-7, - num_instability_tol: float = 1e-7, - ) -> None: - r""" - Args: - ansatz: Ansatz to be used for variational time evolution. - initial_parameters: Initial parameter values for an ansatz. - variational_principle: Variational Principle to be used. Defaults to - ``RealMcLachlanPrinciple``. - estimator: An estimator primitive used for calculating expectation values of - TimeEvolutionProblem.aux_operators. - ode_solver: ODE solver callable that implements a SciPy ``OdeSolver`` interface or a - string indicating a valid method offered by SciPy. - lse_solver: Linear system of equations solver callable. It accepts ``A`` and ``b`` to - solve ``Ax=b`` and returns ``x``. If ``None``, the default ``np.linalg.lstsq`` - solver is used. - num_timesteps: The number of timesteps to take. If ``None``, it is - automatically selected to achieve a timestep of approximately 0.01. Only - relevant in case of the ``ForwardEulerSolver``. - imag_part_tol: Allowed value of an imaginary part that can be neglected if no - imaginary part is expected. - num_instability_tol: The amount of negative value that is allowed to be - rounded up to 0 for quantities that are expected to be - non-negative. - """ - if variational_principle is None: - variational_principle = RealMcLachlanPrinciple() - super().__init__( - ansatz, - initial_parameters, - variational_principle, - estimator, - ode_solver, - lse_solver=lse_solver, - num_timesteps=num_timesteps, - imag_part_tol=imag_part_tol, - num_instability_tol=num_instability_tol, - ) diff --git a/qiskit/algorithms/time_evolvers/variational/var_qte.py b/qiskit/algorithms/time_evolvers/variational/var_qte.py deleted file mode 100644 index f0d33a6b8ae5..000000000000 --- a/qiskit/algorithms/time_evolvers/variational/var_qte.py +++ /dev/null @@ -1,290 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2023. -# -# 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 Time Evolution Interface""" -from __future__ import annotations - -from abc import ABC -from collections.abc import Mapping, Callable, Sequence -from typing import Type - -import numpy as np -from scipy.integrate import OdeSolver - -from qiskit import QuantumCircuit -from qiskit.circuit import Parameter -from qiskit.opflow import PauliSumOp -from qiskit.primitives import BaseEstimator -from qiskit.quantum_info.operators.base_operator import BaseOperator - -from .solvers.ode.forward_euler_solver import ForwardEulerSolver -from .solvers.ode.ode_function_factory import OdeFunctionFactory -from .solvers.ode.var_qte_ode_solver import VarQTEOdeSolver -from .solvers.var_qte_linear_solver import VarQTELinearSolver - -from .variational_principles.variational_principle import VariationalPrinciple -from .var_qte_result import VarQTEResult - -from ..time_evolution_problem import TimeEvolutionProblem - -from ...observables_evaluator import estimate_observables - - -class VarQTE(ABC): - """Variational Quantum Time Evolution. - - Algorithms that use variational principles to compute a time evolution for a given - Hermitian operator (Hamiltonian) and a quantum state prepared by a parameterized quantum - circuit. - - Attributes: - ansatz (QuantumCircuit): Ansatz to be used for variational time evolution. - initial_parameters (Mapping[Parameter, float] | Sequence[float]): Initial - parameter values for an ansatz. - variational_principle (VariationalPrinciple): Variational Principle to be used. - estimator (BaseEstimator): An estimator primitive used for calculating expectation - values of ``TimeEvolutionProblem.aux_operators``. - ode_solver(Type[OdeSolver] | str): ODE solver callable that implements a SciPy - ``OdeSolver`` interface or a string indicating a valid method offered by SciPy. - lse_solver (Callable[[np.ndarray, np.ndarray], np.ndarray] | None): Linear system - of equations solver callable. It accepts ``A`` and ``b`` to solve ``Ax=b`` - and returns ``x``. - num_timesteps (int | None): The number of timesteps to take. If None, it is - automatically selected to achieve a timestep of approximately 0.01. Only - relevant in case of the ``ForwardEulerSolver``. - imag_part_tol (float): Allowed value of an imaginary part that can be neglected if no - imaginary part is expected. - num_instability_tol (float): The amount of negative value that is allowed to be - rounded up to 0 for quantities that are expected to be - non-negative. - References: - - [1] Benjamin, Simon C. et al. (2019). - Theory of variational quantum simulation. ``_ - """ - - def __init__( - self, - ansatz: QuantumCircuit, - initial_parameters: Mapping[Parameter, float] | Sequence[float], - variational_principle: VariationalPrinciple, - estimator: BaseEstimator, - ode_solver: Type[OdeSolver] | str = ForwardEulerSolver, - lse_solver: Callable[[np.ndarray, np.ndarray], np.ndarray] | None = None, - num_timesteps: int | None = None, - imag_part_tol: float = 1e-7, - num_instability_tol: float = 1e-7, - ) -> None: - r""" - Args: - ansatz: Ansatz to be used for variational time evolution. - initial_parameters: Initial parameter values for an ansatz. - variational_principle: Variational Principle to be used. - estimator: An estimator primitive used for calculating expectation values of - TimeEvolutionProblem.aux_operators. - ode_solver: ODE solver callable that implements a SciPy ``OdeSolver`` interface or a - string indicating a valid method offered by SciPy. - lse_solver: Linear system of equations solver callable. It accepts ``A`` and ``b`` to - solve ``Ax=b`` and returns ``x``. - num_timesteps: The number of timesteps to take. If None, it is - automatically selected to achieve a timestep of approximately 0.01. Only - relevant in case of the ``ForwardEulerSolver``. - imag_part_tol: Allowed value of an imaginary part that can be neglected if no - imaginary part is expected. - num_instability_tol: The amount of negative value that is allowed to be - rounded up to 0 for quantities that are expected to be - non-negative. - """ - super().__init__() - self.ansatz = ansatz - self.initial_parameters = initial_parameters - self.variational_principle = variational_principle - self.estimator = estimator - self.num_timesteps = num_timesteps - self.lse_solver = lse_solver - self.ode_solver = ode_solver - self.imag_part_tol = imag_part_tol - self.num_instability_tol = num_instability_tol - # OdeFunction abstraction kept for potential extensions - unclear at the moment; - # currently hidden from the user - self._ode_function_factory = OdeFunctionFactory() - - def evolve(self, evolution_problem: TimeEvolutionProblem) -> VarQTEResult: - """Apply Variational Quantum Time Evolution to the given operator. - - Args: - evolution_problem: Instance defining an evolution problem. - Returns: - Result of the evolution which includes a quantum circuit with bound parameters as an - evolved state and, if provided, observables evaluated on the evolved state. - - Raises: - ValueError: If ``initial_state`` is included in the ``evolution_problem``. - """ - self._validate_aux_ops(evolution_problem) - - if evolution_problem.initial_state is not None: - raise ValueError( - "An initial_state was provided to the TimeEvolutionProblem but this is not " - "supported by VarQTE. Please remove this state from the problem definition " - "and set VarQTE.initial_parameters with the corresponding initial parameter " - "values instead." - ) - - init_state_param_dict = self._create_init_state_param_dict( - self.initial_parameters, self.ansatz.parameters - ) - - # unwrap PauliSumOp (in the future this will be deprecated) - if isinstance(evolution_problem.hamiltonian, PauliSumOp): - hamiltonian = ( - evolution_problem.hamiltonian.primitive * evolution_problem.hamiltonian.coeff - ) - else: - hamiltonian = evolution_problem.hamiltonian - - evolved_state, param_values, time_points = self._evolve( - init_state_param_dict, - hamiltonian, - evolution_problem.time, - evolution_problem.t_param, - ) - - observables = [] - if evolution_problem.aux_operators is not None: - for values in param_values: - # cannot batch evaluation because estimate_observables - # only accepts single circuits - evol_state = self.ansatz.assign_parameters( - dict(zip(init_state_param_dict.keys(), values)) - ) - observable = estimate_observables( - self.estimator, - evol_state, - evolution_problem.aux_operators, - ) - observables.append(observable) - - # TODO: deprecate returning evaluated_aux_ops. - # As these are the observables for the last time step. - evaluated_aux_ops = observables[-1] if len(observables) > 0 else None - - return VarQTEResult( - evolved_state, evaluated_aux_ops, observables, time_points, param_values - ) - - def _evolve( - self, - init_state_param_dict: Mapping[Parameter, float], - hamiltonian: BaseOperator, - time: float, - t_param: Parameter | None = None, - ) -> tuple[QuantumCircuit | None, Sequence[Sequence[float]], Sequence[float]]: - r""" - Helper method for performing time evolution. Works both for imaginary and real case. - - Args: - init_state_param_dict: Parameter dictionary with initial values for a given - parametrized state/ansatz. - hamiltonian: Operator used for Variational Quantum Time Evolution (VarQTE). - time: Total time of evolution. - t_param: Time parameter in case of a time-dependent Hamiltonian. - - Returns: - Result of the evolution which is a quantum circuit with bound parameters as an - evolved state. - """ - - init_state_parameters = list(init_state_param_dict.keys()) - init_state_parameter_values = list(init_state_param_dict.values()) - - linear_solver = VarQTELinearSolver( - self.variational_principle, - hamiltonian, - self.ansatz, - init_state_parameters, - t_param, - self.lse_solver, - self.imag_part_tol, - ) - - # Convert the operator that holds the Hamiltonian and ansatz into a NaturalGradient operator - ode_function = self._ode_function_factory._build( - linear_solver, init_state_param_dict, t_param - ) - - ode_solver = VarQTEOdeSolver( - init_state_parameter_values, ode_function, self.ode_solver, self.num_timesteps - ) - final_param_values, param_values, time_points = ode_solver.run(time) - param_dict_from_ode = dict(zip(init_state_parameters, final_param_values)) - - return self.ansatz.assign_parameters(param_dict_from_ode), param_values, time_points - - @staticmethod - def _create_init_state_param_dict( - param_values: Mapping[Parameter, float] | Sequence[float], - init_state_parameters: Sequence[Parameter], - ) -> Mapping[Parameter, float]: - r""" - If ``param_values`` is a dictionary, it looks for parameters present in an initial state - (an ansatz) in a ``param_values``. Based on that, it creates a new dictionary containing - only parameters present in an initial state and their respective values. - If ``param_values`` is a list of values, it creates a new dictionary containing - parameters present in an initial state and their respective values. - - Args: - param_values: Dictionary which relates parameter values to the parameters or a list of - values. - init_state_parameters: Parameters present in a quantum state. - - Returns: - Dictionary that maps parameters of an initial state to some values. - - Raises: - ValueError: If the dictionary with parameter values provided does not include all - parameters present in the initial state or if the list of values provided is not the - same length as the list of parameters. - TypeError: If an unsupported type of ``param_values`` provided. - """ - if isinstance(param_values, Mapping): - init_state_parameter_values: Sequence[float] = [] - for param in init_state_parameters: - if param in param_values.keys(): - init_state_parameter_values.append(param_values[param]) - else: - raise ValueError( - f"The dictionary with parameter values provided does not " - f"include all parameters present in the initial state." - f"Parameters present in the state: {init_state_parameters}, " - f"parameters in the dictionary: " - f"{list(param_values.keys())}." - ) - elif isinstance(param_values, (Sequence, np.ndarray)): - if len(init_state_parameters) != len(param_values): - raise ValueError( - f"Initial state has {len(init_state_parameters)} parameters and the" - f" list of values has {len(param_values)} elements. They should be" - f" equal in length." - ) - init_state_parameter_values = param_values - else: - raise TypeError(f"Unsupported type of param_values provided: {type(param_values)}.") - - init_state_param_dict = dict(zip(init_state_parameters, init_state_parameter_values)) - return init_state_param_dict - - def _validate_aux_ops(self, evolution_problem: TimeEvolutionProblem) -> None: - if evolution_problem.aux_operators is not None and self.estimator is None: - raise ValueError( - "aux_operators were provided for evaluations but no ``estimator`` was provided." - ) diff --git a/qiskit/algorithms/time_evolvers/variational/var_qte_result.py b/qiskit/algorithms/time_evolvers/variational/var_qte_result.py deleted file mode 100644 index 3efb5e7c1789..000000000000 --- a/qiskit/algorithms/time_evolvers/variational/var_qte_result.py +++ /dev/null @@ -1,56 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2023. -# -# 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. - -"""Result object for varQTE.""" -from __future__ import annotations - -import numpy as np - -from qiskit.circuit import QuantumCircuit - -from ..time_evolution_result import TimeEvolutionResult - -from ...list_or_dict import ListOrDict - - -class VarQTEResult(TimeEvolutionResult): - """The result object for the variational quantum time evolution algorithms. - - Attributes: - parameter_values (np.array | None): Optional list of parameter values obtained after - each evolution step. - """ - - def __init__( - self, - evolved_state: QuantumCircuit, - aux_ops_evaluated: ListOrDict[tuple[complex, complex]] | None = None, - observables: ListOrDict[tuple[np.ndarray, np.ndarray]] | None = None, - times: np.ndarray | None = None, - parameter_values: np.ndarray | None = None, - ): - """ - Args: - evolved_state: An evolved quantum state. - aux_ops_evaluated: Optional list of observables for which expected values on an evolved - state are calculated. These values are in fact tuples formatted as (mean, standard - deviation). - observables: Optional list of observables for which expected on an evolved state are - calculated at each timestep. - These values are in fact lists of tuples formatted as (mean, standard deviation). - times: Optional list of times at which each observable has been evaluated. - parameter_values: Optional list of parameter values obtained after each evolution step. - - """ - - super().__init__(evolved_state, aux_ops_evaluated, observables, times) - self.parameter_values = parameter_values diff --git a/qiskit/algorithms/time_evolvers/variational/variational_principles/__init__.py b/qiskit/algorithms/time_evolvers/variational/variational_principles/__init__.py deleted file mode 100644 index be04c03d7bcf..000000000000 --- a/qiskit/algorithms/time_evolvers/variational/variational_principles/__init__.py +++ /dev/null @@ -1,27 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2023. -# -# 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. - -"""Variational Principles""" - -from .variational_principle import VariationalPrinciple -from .imaginary_mc_lachlan_principle import ImaginaryMcLachlanPrinciple -from .imaginary_variational_principle import ImaginaryVariationalPrinciple -from .real_mc_lachlan_principle import RealMcLachlanPrinciple -from .real_variational_principle import RealVariationalPrinciple - -__all__ = [ - "VariationalPrinciple", - "ImaginaryMcLachlanPrinciple", - "ImaginaryVariationalPrinciple", - "RealMcLachlanPrinciple", - "RealVariationalPrinciple", -] diff --git a/qiskit/algorithms/time_evolvers/variational/variational_principles/imaginary_mc_lachlan_principle.py b/qiskit/algorithms/time_evolvers/variational/variational_principles/imaginary_mc_lachlan_principle.py deleted file mode 100644 index 09e1e03d3473..000000000000 --- a/qiskit/algorithms/time_evolvers/variational/variational_principles/imaginary_mc_lachlan_principle.py +++ /dev/null @@ -1,128 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2023. -# -# 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. - -"""Class for an Imaginary McLachlan's Variational Principle.""" -from __future__ import annotations - -import warnings - -from collections.abc import Sequence - -import numpy as np - -from qiskit import QuantumCircuit -from qiskit.circuit import Parameter -from qiskit.primitives import Estimator -from qiskit.quantum_info.operators.base_operator import BaseOperator - -from .imaginary_variational_principle import ImaginaryVariationalPrinciple - -from ....exceptions import AlgorithmError -from ....gradients import ( - BaseEstimatorGradient, - BaseQGT, - DerivativeType, - LinCombQGT, - LinCombEstimatorGradient, -) - - -class ImaginaryMcLachlanPrinciple(ImaginaryVariationalPrinciple): - """Class for an Imaginary McLachlan's Variational Principle. It aims to minimize the distance - between both sides of the Wick-rotated Schrödinger equation with a quantum state given as a - parametrized trial state. The principle leads to a system of linear equations handled by a - linear solver. The imaginary variant means that we consider imaginary time dynamics. - """ - - def __init__( - self, - qgt: BaseQGT | None = None, - gradient: BaseEstimatorGradient | None = None, - ) -> None: - """ - Args: - qgt: Instance of a the GQT class used to compute the QFI. - If ``None`` provided, ``LinCombQGT`` is used. - gradient: Instance of a class used to compute the state gradient. - If ``None`` provided, ``LinCombEstimatorGradient`` is used. - - Raises: - AlgorithmError: If the gradient instance does not contain an estimator. - """ - - self._validate_grad_settings(gradient) - - if gradient is not None: - try: - estimator = gradient._estimator - except Exception as exc: - raise AlgorithmError( - "The provided gradient instance does not contain an estimator primitive." - ) from exc - else: - estimator = Estimator() - gradient = LinCombEstimatorGradient(estimator) - - if qgt is None: - qgt = LinCombQGT(estimator) - - super().__init__(qgt, gradient) - - def evolution_gradient( - self, - hamiltonian: BaseOperator, - ansatz: QuantumCircuit, - param_values: Sequence[float], - gradient_params: Sequence[Parameter] | None = None, - ) -> np.ndarray: - """ - Calculates an evolution gradient according to the rules of this variational principle. - - Args: - hamiltonian: Operator used for Variational Quantum Time Evolution. - ansatz: Quantum state in the form of a parametrized quantum circuit. - param_values: Values of parameters to be bound. - gradient_params: List of parameters with respect to which gradients should be computed. - If ``None`` given, gradients w.r.t. all parameters will be computed. - - Returns: - An evolution gradient. - - Raises: - AlgorithmError: If a gradient job fails. - """ - - try: - evolution_grad_lse_rhs = ( - self.gradient.run([ansatz], [hamiltonian], [param_values], [gradient_params]) - .result() - .gradients[0] - ) - - except Exception as exc: - raise AlgorithmError("The gradient primitive job failed!") from exc - - return -0.5 * evolution_grad_lse_rhs - - @staticmethod - def _validate_grad_settings(gradient): - if ( - gradient is not None - and hasattr(gradient, "_derivative_type") - and gradient._derivative_type != DerivativeType.REAL - ): - warnings.warn( - "A gradient instance with a setting for calculating imaginary part of " - "the gradient was provided. This variational principle requires the" - "real part. The setting to real was changed automatically." - ) - gradient._derivative_type = DerivativeType.REAL diff --git a/qiskit/algorithms/time_evolvers/variational/variational_principles/imaginary_variational_principle.py b/qiskit/algorithms/time_evolvers/variational/variational_principles/imaginary_variational_principle.py deleted file mode 100644 index 1255e52b7c65..000000000000 --- a/qiskit/algorithms/time_evolvers/variational/variational_principles/imaginary_variational_principle.py +++ /dev/null @@ -1,22 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2023. -# -# 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. - -"""Abstract class for an Imaginary Variational Principle.""" - -from abc import ABC - -from .variational_principle import VariationalPrinciple - - -class ImaginaryVariationalPrinciple(VariationalPrinciple, ABC): - """Abstract class for an Imaginary Variational Principle. The imaginary variant - means that we consider imaginary time dynamics.""" diff --git a/qiskit/algorithms/time_evolvers/variational/variational_principles/real_mc_lachlan_principle.py b/qiskit/algorithms/time_evolvers/variational/variational_principles/real_mc_lachlan_principle.py deleted file mode 100644 index d7a946b8ab70..000000000000 --- a/qiskit/algorithms/time_evolvers/variational/variational_principles/real_mc_lachlan_principle.py +++ /dev/null @@ -1,166 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2023. -# -# 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. - -"""Class for a Real McLachlan's Variational Principle.""" -from __future__ import annotations - -import warnings - -from collections.abc import Sequence - -import numpy as np -from numpy import real - -from qiskit import QuantumCircuit -from qiskit.circuit import Parameter -from qiskit.primitives import Estimator -from qiskit.quantum_info import SparsePauliOp -from qiskit.quantum_info.operators.base_operator import BaseOperator - -from .real_variational_principle import RealVariationalPrinciple - -from ....exceptions import AlgorithmError -from ....gradients import ( - BaseEstimatorGradient, - BaseQGT, - DerivativeType, - LinCombQGT, - LinCombEstimatorGradient, -) - - -class RealMcLachlanPrinciple(RealVariationalPrinciple): - """Class for a Real McLachlan's Variational Principle. It aims to minimize the distance - between both sides of the Schrödinger equation with a quantum state given as a parametrized - trial state. The principle leads to a system of linear equations handled by a linear solver. - The real variant means that we consider real time dynamics. - """ - - def __init__( - self, - qgt: BaseQGT | None = None, - gradient: BaseEstimatorGradient | None = None, - ) -> None: - """ - Args: - qgt: Instance of a the GQT class used to compute the QFI. - If ``None`` provided, ``LinCombQGT`` is used. - gradient: Instance of a class used to compute the state gradient. - If ``None`` provided, ``LinCombEstimatorGradient`` is used. - - Raises: - AlgorithmError: If the gradient instance does not contain an estimator. - """ - self._validate_grad_settings(gradient) - - if gradient is not None: - try: - estimator = gradient._estimator - except Exception as exc: - raise AlgorithmError( - "The provided gradient instance does not contain an estimator primitive." - ) from exc - else: - estimator = Estimator() - gradient = LinCombEstimatorGradient(estimator, derivative_type=DerivativeType.IMAG) - - if qgt is None: - qgt = LinCombQGT(estimator) - - super().__init__(qgt, gradient) - - def evolution_gradient( - self, - hamiltonian: BaseOperator, - ansatz: QuantumCircuit, - param_values: Sequence[float], - gradient_params: Sequence[Parameter] | None = None, - ) -> np.ndarray: - """ - Calculates an evolution gradient according to the rules of this variational principle. - - Args: - hamiltonian: Operator used for Variational Quantum Time Evolution. - ansatz: Quantum state in the form of a parametrized quantum circuit. - param_values: Values of parameters to be bound. - gradient_params: List of parameters with respect to which gradients should be computed. - If ``None`` given, gradients w.r.t. all parameters will be computed. - - Returns: - An evolution gradient. - - Raises: - AlgorithmError: If a gradient job fails. - """ - - try: - estimator_job = self.gradient._estimator.run([ansatz], [hamiltonian], [param_values]) - energy = estimator_job.result().values[0] - except Exception as exc: - raise AlgorithmError("The primitive job failed!") from exc - - modified_hamiltonian = self._construct_modified_hamiltonian(hamiltonian, real(energy)) - - try: - evolution_grad = ( - 0.5 - * self.gradient.run( - [ansatz], - [modified_hamiltonian], - parameters=[gradient_params], - parameter_values=[param_values], - ) - .result() - .gradients[0] - ) - except Exception as exc: - raise AlgorithmError("The gradient primitive job failed!") from exc - - # The BaseEstimatorGradient class returns the gradient of the opposite sign than we expect - # here (i.e. with a minus sign), hence the correction that cancels it to recover the - # real McLachlan's principle equations that do not have a minus sign. - evolution_grad = (-1) * evolution_grad - return evolution_grad - - @staticmethod - def _construct_modified_hamiltonian(hamiltonian: BaseOperator, energy: float) -> BaseOperator: - """ - Modifies a Hamiltonian according to the rules of this variational principle. - - Args: - hamiltonian: Operator used for Variational Quantum Time Evolution. - energy: The energy correction value. - - Returns: - A modified Hamiltonian. - """ - energy_term = SparsePauliOp.from_list( - hamiltonian.to_list() + [("I" * hamiltonian.num_qubits, -energy)] - ) - return energy_term - - @staticmethod - def _validate_grad_settings(gradient): - - if gradient is not None: - if not hasattr(gradient, "_derivative_type"): - raise ValueError( - "The gradient instance provided does not support calculating imaginary part. " - "Please choose a different gradient class." - ) - if gradient._derivative_type != DerivativeType.IMAG: - warnings.warn( - "A gradient instance with a setting for calculating real part of the" - "gradient was provided. This variational principle requires the" - "imaginary part. The setting to imaginary was changed automatically." - ) - gradient._derivative_type = DerivativeType.IMAG diff --git a/qiskit/algorithms/time_evolvers/variational/variational_principles/real_variational_principle.py b/qiskit/algorithms/time_evolvers/variational/variational_principles/real_variational_principle.py deleted file mode 100644 index a93c50675e3e..000000000000 --- a/qiskit/algorithms/time_evolvers/variational/variational_principles/real_variational_principle.py +++ /dev/null @@ -1,22 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2023. -# -# 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. - -"""Class for a Real Variational Principle.""" - -from abc import ABC - -from .variational_principle import VariationalPrinciple - - -class RealVariationalPrinciple(VariationalPrinciple, ABC): - """Class for a Real Variational Principle. The real variant - means that we consider real time dynamics.""" diff --git a/qiskit/algorithms/time_evolvers/variational/variational_principles/variational_principle.py b/qiskit/algorithms/time_evolvers/variational/variational_principles/variational_principle.py deleted file mode 100644 index be16849155c4..000000000000 --- a/qiskit/algorithms/time_evolvers/variational/variational_principles/variational_principle.py +++ /dev/null @@ -1,98 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2023. -# -# 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. - -"""Class for a Variational Principle.""" -from __future__ import annotations - -from abc import ABC, abstractmethod -from collections.abc import Sequence - -import numpy as np - -from qiskit import QuantumCircuit -from qiskit.circuit import Parameter -from qiskit.quantum_info.operators.base_operator import BaseOperator - -from ....exceptions import AlgorithmError -from ....gradients import BaseEstimatorGradient, BaseQGT, DerivativeType - - -class VariationalPrinciple(ABC): - """A Variational Principle class. It determines the time propagation of parameters in a - quantum state provided as a parametrized quantum circuit (ansatz). - - Attributes: - qgt (BaseQGT): Instance of a class used to compute the GQT. - gradient (BaseEstimatorGradient): Instance of a class used to compute the - state gradient. - """ - - def __init__( - self, - qgt: BaseQGT, - gradient: BaseEstimatorGradient, - ) -> None: - """ - Args: - qgt: Instance of a class used to compute the GQT. - gradient: Instance of a class used to compute the state gradient. - """ - self.qgt = qgt - self.gradient = gradient - - def metric_tensor( - self, ansatz: QuantumCircuit, param_values: Sequence[float] - ) -> Sequence[float]: - """ - Calculates a metric tensor according to the rules of this variational principle. - - Args: - ansatz: Quantum state in the form of a parametrized quantum circuit. - param_values: Values of parameters to be bound. - - Returns: - Metric tensor. - - Raises: - AlgorithmError: If a QFI job fails. - """ - - self.qgt.derivative_type = DerivativeType.REAL - try: - metric_tensor = self.qgt.run([ansatz], [param_values], [None]).result().qgts[0] - except Exception as exc: - - raise AlgorithmError("The QFI primitive job failed!") from exc - return metric_tensor - - @abstractmethod - def evolution_gradient( - self, - hamiltonian: BaseOperator, - ansatz: QuantumCircuit, - param_values: Sequence[float], - gradient_params: Sequence[Parameter] | None = None, - ) -> np.ndarray: - """ - Calculates an evolution gradient according to the rules of this variational principle. - - Args: - hamiltonian: Operator used for Variational Quantum Time Evolution. - ansatz: Quantum state in the form of a parametrized quantum circuit. - param_values: Values of parameters to be bound. - gradient_params: List of parameters with respect to which gradients should be computed. - If ``None`` given, gradients w.r.t. all parameters will be computed. - - Returns: - An evolution gradient. - """ - pass diff --git a/qiskit/algorithms/utils/__init__.py b/qiskit/algorithms/utils/__init__.py deleted file mode 100644 index 2b49396270c7..000000000000 --- a/qiskit/algorithms/utils/__init__.py +++ /dev/null @@ -1,21 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2022. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. - -"""Common Qiskit algorithms utility functions.""" - -from .validate_initial_point import validate_initial_point -from .validate_bounds import validate_bounds - -__all__ = [ - "validate_initial_point", - "validate_bounds", -] diff --git a/qiskit/algorithms/utils/set_batching.py b/qiskit/algorithms/utils/set_batching.py deleted file mode 100644 index 225f50a6fed8..000000000000 --- a/qiskit/algorithms/utils/set_batching.py +++ /dev/null @@ -1,27 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2022. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. - -"""Set default batch sizes for the optimizers.""" - -from qiskit.algorithms.optimizers import Optimizer, SPSA - - -def _set_default_batchsize(optimizer: Optimizer) -> bool: - """Set the default batchsize, if None is set and return whether it was updated or not.""" - if isinstance(optimizer, SPSA): - updated = optimizer._max_evals_grouped is None - if updated: - optimizer.set_max_evals_grouped(50) - else: # we only set a batchsize for SPSA - updated = False - - return updated diff --git a/qiskit/algorithms/utils/validate_bounds.py b/qiskit/algorithms/utils/validate_bounds.py deleted file mode 100644 index 747e68f78a52..000000000000 --- a/qiskit/algorithms/utils/validate_bounds.py +++ /dev/null @@ -1,44 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2022. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. - -"""Validate parameter bounds.""" - -from __future__ import annotations - -from qiskit.circuit import QuantumCircuit - - -def validate_bounds(circuit: QuantumCircuit) -> list[tuple[float | None, float | None]]: - """ - Validate the bounds provided by a quantum circuit against its number of parameters. - If no bounds are obtained, return ``None`` for all lower and upper bounds. - - Args: - circuit: A parameterized quantum circuit. - - Returns: - A list of tuples (lower_bound, upper_bound)). - - Raises: - ValueError: If the number of bounds does not the match the number of circuit parameters. - """ - if hasattr(circuit, "parameter_bounds") and circuit.parameter_bounds is not None: - bounds = circuit.parameter_bounds - if len(bounds) != circuit.num_parameters: - raise ValueError( - f"The number of bounds ({len(bounds)}) does not match the number of " - f"parameters in the circuit ({circuit.num_parameters})." - ) - else: - bounds = [(None, None)] * circuit.num_parameters - - return bounds diff --git a/qiskit/algorithms/utils/validate_initial_point.py b/qiskit/algorithms/utils/validate_initial_point.py deleted file mode 100644 index 56a7654a16e4..000000000000 --- a/qiskit/algorithms/utils/validate_initial_point.py +++ /dev/null @@ -1,72 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2022. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. - -"""Validate an initial point.""" - -from __future__ import annotations - -import warnings -from collections.abc import Sequence - -import numpy as np - -from qiskit.circuit import QuantumCircuit -from qiskit.utils import algorithm_globals - - -def validate_initial_point( - point: Sequence[float] | None, circuit: QuantumCircuit -) -> Sequence[float]: - r""" - Validate a choice of initial point against a choice of circuit. If no point is provided, a - random point will be generated within certain parameter bounds. It will first look to the - circuit for these bounds. If the circuit does not specify bounds, bounds of :math:`-2\pi`, - :math:`2\pi` will be used. - - Args: - point: An initial point. - circuit: A parameterized quantum circuit. - - Returns: - A validated initial point. - - Raises: - ValueError: If the dimension of the initial point does not match the number of circuit - parameters. - """ - expected_size = circuit.num_parameters - - if point is None: - # get bounds if circuit has them set, otherwise use [-2pi, 2pi] for each parameter - bounds = getattr(circuit, "parameter_bounds", None) - if bounds is None: - bounds = [(-2 * np.pi, 2 * np.pi)] * expected_size - - # replace all Nones by [-2pi, 2pi] - lower_bounds = [] - upper_bounds = [] - for lower, upper in bounds: - lower_bounds.append(lower if lower is not None else -2 * np.pi) - upper_bounds.append(upper if upper is not None else 2 * np.pi) - - # sample from within bounds - with warnings.catch_warnings(): - warnings.filterwarnings("ignore", category=DeprecationWarning) - point = algorithm_globals.random.uniform(lower_bounds, upper_bounds) - - elif len(point) != expected_size: - raise ValueError( - f"The dimension of the initial point ({len(point)}) does not match the " - f"number of parameters in the circuit ({expected_size})." - ) - - return point diff --git a/qiskit/algorithms/variational_algorithm.py b/qiskit/algorithms/variational_algorithm.py deleted file mode 100644 index 1b8b2ec6a164..000000000000 --- a/qiskit/algorithms/variational_algorithm.py +++ /dev/null @@ -1,137 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2019, 2021. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. - -"""The Variational Algorithm Base Class. - -This class can be used an interface for working with Variation Algorithms, such as VQE, -QAOA, or QSVM, and also provides helper utilities for implementing new variational algorithms. -Writing a new variational algorithm is a simple as extending this class, implementing a cost -function for the new algorithm to pass to the optimizer, and running :meth:`find_minimum` method -of this class to carry out the optimization. Alternatively, all of the functions below can be -overridden to opt-out of this infrastructure but still meet the interface requirements. - -.. note:: - - 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.utils.algorithm_globals.random_seed = seed``). -""" - -from __future__ import annotations -from abc import ABC, abstractmethod -import numpy as np - -from qiskit.circuit import QuantumCircuit - -from .algorithm_result import AlgorithmResult -from .optimizers import OptimizerResult - - -class VariationalAlgorithm(ABC): - """The Variational Algorithm Base Class.""" - - @property - @abstractmethod - def initial_point(self) -> np.ndarray | None: - """Returns initial point.""" - pass - - @initial_point.setter - @abstractmethod - def initial_point(self, initial_point: np.ndarray | None) -> None: - """Sets initial point.""" - pass - - -class VariationalResult(AlgorithmResult): - """Variation Algorithm Result.""" - - def __init__(self) -> None: - super().__init__() - self._optimizer_evals: int | None = None - self._optimizer_time: float | None = None - self._optimal_value: float | None = None - self._optimal_point: np.ndarray | None = None - self._optimal_parameters: dict | None = None - self._optimizer_result: OptimizerResult | None = None - self._optimal_circuit: QuantumCircuit | None = None - - @property - def optimizer_evals(self) -> int | None: - """Returns number of optimizer evaluations""" - return self._optimizer_evals - - @optimizer_evals.setter - def optimizer_evals(self, value: int) -> None: - """Sets number of optimizer evaluations""" - self._optimizer_evals = value - - @property - def optimizer_time(self) -> float | None: - """Returns time taken for optimization""" - return self._optimizer_time - - @optimizer_time.setter - def optimizer_time(self, value: float) -> None: - """Sets time taken for optimization""" - self._optimizer_time = value - - @property - def optimal_value(self) -> float | None: - """Returns optimal value""" - return self._optimal_value - - @optimal_value.setter - def optimal_value(self, value: int) -> None: - """Sets optimal value""" - self._optimal_value = value - - @property - def optimal_point(self) -> np.ndarray | None: - """Returns optimal point""" - return self._optimal_point - - @optimal_point.setter - def optimal_point(self, value: np.ndarray) -> None: - """Sets optimal point""" - self._optimal_point = value - - @property - def optimal_parameters(self) -> dict | None: - """Returns the optimal parameters in a dictionary""" - return self._optimal_parameters - - @optimal_parameters.setter - def optimal_parameters(self, value: dict) -> None: - """Sets optimal parameters""" - self._optimal_parameters = value - - @property - def optimizer_result(self) -> OptimizerResult | None: - """Returns the optimizer result""" - return self._optimizer_result - - @optimizer_result.setter - def optimizer_result(self, value: OptimizerResult) -> None: - """Sets optimizer result""" - self._optimizer_result = value - - @property - def optimal_circuit(self) -> QuantumCircuit: - """The optimal circuits. Along with the optimal parameters, - these can be used to retrieve the minimum eigenstate. - """ - return self._optimal_circuit - - @optimal_circuit.setter - def optimal_circuit(self, optimal_circuit: QuantumCircuit) -> None: - self._optimal_circuit = optimal_circuit diff --git a/qiskit/primitives/backend_estimator.py b/qiskit/primitives/backend_estimator.py index 3627afb40fbf..8ae707553359 100644 --- a/qiskit/primitives/backend_estimator.py +++ b/qiskit/primitives/backend_estimator.py @@ -98,8 +98,7 @@ class BackendEstimator(BaseEstimator[PrimitiveJob[EstimatorResult]]): (or :class:`~.BackendV1`) object in the :class:`~.BaseEstimator` API. It facilitates using backends that do not provide a native :class:`~.BaseEstimator` implementation in places that work with - :class:`~.BaseEstimator`, such as algorithms in :mod:`qiskit.algorithms` - including :class:`~.qiskit.algorithms.minimum_eigensolvers.VQE`. However, + :class:`~.BaseEstimator`. However, if you're using a provider that has a native implementation of :class:`~.BaseEstimator`, it is a better choice to leverage that native implementation as it will likely include additional optimizations and be diff --git a/qiskit/primitives/backend_sampler.py b/qiskit/primitives/backend_sampler.py index 64458f2d9caf..140a3091f34a 100644 --- a/qiskit/primitives/backend_sampler.py +++ b/qiskit/primitives/backend_sampler.py @@ -38,8 +38,7 @@ class BackendSampler(BaseSampler[PrimitiveJob[SamplerResult]]): any measurement mitigation, it just computes the probability distribution from the counts. It facilitates using backends that do not provide a native :class:`~.BaseSampler` implementation in places that work with - :class:`~.BaseSampler`, such as algorithms in :mod:`qiskit.algorithms` - including :class:`~.qiskit.algorithms.minimum_eigensolvers.SamplingVQE`. + :class:`~.BaseSampler`. However, if you're using a provider that has a native implementation of :class:`~.BaseSampler`, it is a better choice to leverage that native implementation as it will likely include additional optimizations and be diff --git a/qiskit/providers/__init__.py b/qiskit/providers/__init__.py index d7ec4b21b9fe..dc63b6df705f 100644 --- a/qiskit/providers/__init__.py +++ b/qiskit/providers/__init__.py @@ -144,8 +144,8 @@ backend. It also provides the :meth:`~qiskit.providers.BackendV2.run` method which can run the :class:`~qiskit.circuit.QuantumCircuit` objects and/or :class:`~qiskit.pulse.Schedule` objects. This enables users and other Qiskit -APIs, such as :func:`~qiskit.execute_function.execute` and higher level algorithms in -:mod:`qiskit.algorithms`, to get results from executing circuits on devices in a standard +APIs, such as :func:`~qiskit.execute_function.execute` to get results from +executing circuits on devices in a standard fashion regardless of how the backend is implemented. At a high level the basic steps for writing a provider are: @@ -635,7 +635,7 @@ def status(self): provider-specific :class:`~.Sampler` implementation that leverages the ``M3Mitigation`` class internally to run the circuits and return quasi-probabilities directly from mthree in the result. Doing this would -enable algorithms from :mod:`qiskit.algorithms` to get the best results with +enable algorithms to get the best results with mitigation applied directly from your backends. You can refer to the documentation in :mod:`qiskit.primitives` on how to write custom implementations. Also the built-in implementations: :class:`~.Sampler`, diff --git a/qiskit/transpiler/synthesis/aqc/aqc.py b/qiskit/transpiler/synthesis/aqc/aqc.py index 905116cd2a6c..4ced39a7e4ac 100644 --- a/qiskit/transpiler/synthesis/aqc/aqc.py +++ b/qiskit/transpiler/synthesis/aqc/aqc.py @@ -20,9 +20,7 @@ import numpy as np from scipy.optimize import OptimizeResult, minimize -from qiskit.algorithms.optimizers import Optimizer from qiskit.quantum_info import Operator -from qiskit.utils.deprecation import deprecate_arg from .approximate import ApproximateCircuit, ApproximatingObjective @@ -102,19 +100,9 @@ class AQC: also allocates a number of temporary memory buffers comparable in size to the target matrix. """ - @deprecate_arg( - "optimizer", - deprecation_description=( - "Setting the `optimizer` argument to an instance " - "of `qiskit.algorithms.optimizers.Optimizer` " - ), - additional_msg=("Please, submit a callable that follows the `Minimizer` protocol instead."), - predicate=lambda optimizer: isinstance(optimizer, Optimizer), - since="0.45.0", - ) def __init__( self, - optimizer: Minimizer | Optimizer | None = None, + optimizer: Minimizer | None = None, seed: int | None = None, ): """ @@ -128,9 +116,6 @@ def __init__( self._optimizer = optimizer or partial( minimize, args=(), method="L-BFGS-B", options={"maxiter": 1000} ) - # temporary fix -> remove after deprecation period of Optimizer - if isinstance(self._optimizer, Optimizer): - self._optimizer = self._optimizer.minimize self._seed = seed diff --git a/qiskit/transpiler/synthesis/aqc/aqc_plugin.py b/qiskit/transpiler/synthesis/aqc/aqc_plugin.py index 93403a64f81c..0fa153566557 100644 --- a/qiskit/transpiler/synthesis/aqc/aqc_plugin.py +++ b/qiskit/transpiler/synthesis/aqc/aqc_plugin.py @@ -44,8 +44,8 @@ class AQCSynthesisPlugin(UnitarySynthesisPlugin): depth of the CNOT-network, i.e. the number of layers, where each layer consists of a single CNOT-block. - optimizer (:class:`~qiskit.algorithms.optimizers.Optimizer`) - An instance of optimizer to be used in the optimization process. + optimizer (:class:`~.Minimizer`) + An implementation of the ``Minimizer`` protocol to be used in the optimization process. seed (int) A random seed. diff --git a/qiskit/utils/__init__.py b/qiskit/utils/__init__.py index 789b8c0da9b0..45200413ba2a 100644 --- a/qiskit/utils/__init__.py +++ b/qiskit/utils/__init__.py @@ -48,9 +48,8 @@ A QuantumInstance holds the Qiskit `backend` as well as a number of compile and runtime parameters controlling circuit compilation and execution. Quantum -:mod:`algorithms ` -are run on a device or simulator by passing a QuantumInstance setup with the desired -backend etc. +algorithms are run on a device or simulator by passing a QuantumInstance setup +with the desired backend etc. Optional Dependency Checkers (:mod:`qiskit.utils.optionals`) diff --git a/qiskit/utils/mitigation/__init__.py b/qiskit/utils/mitigation/__init__.py index c79b107cdc0f..54251715424d 100644 --- a/qiskit/utils/mitigation/__init__.py +++ b/qiskit/utils/mitigation/__init__.py @@ -44,7 +44,7 @@ From these calibrations, it is possible to correct the average results of another experiment of interest. These tools are intended for use solely with the :class:`~qiskit.utils.QuantumInstance` class as part of -:mod:`qiskit.algorithms` and :mod:`qiskit.opflow`. +:mod:`qiskit.opflow`. .. autosummary:: :toctree: ../stubs/ diff --git a/releasenotes/notes/remove-qiskit-algorithms-a43541fe24b72208.yaml b/releasenotes/notes/remove-qiskit-algorithms-a43541fe24b72208.yaml new file mode 100644 index 000000000000..51112c0d064a --- /dev/null +++ b/releasenotes/notes/remove-qiskit-algorithms-a43541fe24b72208.yaml @@ -0,0 +1,9 @@ +--- +upgrade: + - | + The ``qiskit.algorithms`` module has been removed, following its deprecation in + Qiskit 0.44. The primitive-based algorithms from this module have been migrated to a standalone library (``qiskit_algorithms``) + and can be found on PyPi or `GitHub `_. + The decision to migrate the algorithms module to a separate package + was made to clarify the purpose Qiskit and make a distinction between the tools + and libraries built on top of it. diff --git a/test/python/algorithms/__init__.py b/test/python/algorithms/__init__.py deleted file mode 100644 index 5303c5b1256c..000000000000 --- a/test/python/algorithms/__init__.py +++ /dev/null @@ -1,17 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2018, 2020. -# -# 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. - -"""Algorithms test module""" - -from .algorithms_test_case import QiskitAlgorithmsTestCase - -__all__ = ["QiskitAlgorithmsTestCase"] diff --git a/test/python/algorithms/algorithms_test_case.py b/test/python/algorithms/algorithms_test_case.py deleted file mode 100644 index 8fc9effd0bf7..000000000000 --- a/test/python/algorithms/algorithms_test_case.py +++ /dev/null @@ -1,21 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2018, 2020. -# -# 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. - -"""Algorithms Test Case""" - -from qiskit.test import QiskitTestCase - - -class QiskitAlgorithmsTestCase(QiskitTestCase): - """Algorithms test Case""" - - pass diff --git a/test/python/algorithms/eigensolvers/__init__.py b/test/python/algorithms/eigensolvers/__init__.py deleted file mode 100644 index e2113e5c114e..000000000000 --- a/test/python/algorithms/eigensolvers/__init__.py +++ /dev/null @@ -1,13 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2022. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. - -"""Tests for the eigensolvers.""" diff --git a/test/python/algorithms/eigensolvers/test_numpy_eigensolver.py b/test/python/algorithms/eigensolvers/test_numpy_eigensolver.py deleted file mode 100644 index 13f2f9a67e4b..000000000000 --- a/test/python/algorithms/eigensolvers/test_numpy_eigensolver.py +++ /dev/null @@ -1,216 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2018, 2022. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. - -"""Test NumPyEigensolver""" - -import unittest -from test.python.algorithms import QiskitAlgorithmsTestCase - -import numpy as np -from ddt import data, ddt - -from qiskit.algorithms.eigensolvers import NumPyEigensolver -from qiskit.algorithms import AlgorithmError -from qiskit.opflow import PauliSumOp -from qiskit.quantum_info import Operator, SparsePauliOp, Pauli, ScalarOp - -H2_SPARSE_PAULI = SparsePauliOp( - ["II", "ZI", "IZ", "ZZ", "XX"], - coeffs=[ - -1.052373245772859, - 0.39793742484318045, - -0.39793742484318045, - -0.01128010425623538, - 0.18093119978423156, - ], -) - -H2_OP = Operator(H2_SPARSE_PAULI.to_matrix()) - -H2_PAULI = PauliSumOp(H2_SPARSE_PAULI) - - -@ddt -class TestNumPyEigensolver(QiskitAlgorithmsTestCase): - """Test NumPy Eigen solver""" - - @data(H2_SPARSE_PAULI, H2_PAULI, H2_OP) - def test_ce(self, op): - """Test basics""" - algo = NumPyEigensolver() - result = algo.compute_eigenvalues(operator=op, aux_operators=[]) - self.assertEqual(len(result.eigenvalues), 1) - self.assertEqual(len(result.eigenstates), 1) - self.assertEqual(result.eigenvalues.dtype, np.float64) - self.assertAlmostEqual(result.eigenvalues[0], -1.85727503) - - @data(H2_SPARSE_PAULI, H2_PAULI, H2_OP) - def test_ce_k4(self, op): - """Test for k=4 eigenvalues""" - algo = NumPyEigensolver(k=4) - result = algo.compute_eigenvalues(operator=op, aux_operators=[]) - self.assertEqual(len(result.eigenvalues), 4) - self.assertEqual(len(result.eigenstates), 4) - self.assertEqual(result.eigenvalues.dtype, np.float64) - np.testing.assert_array_almost_equal( - result.eigenvalues, [-1.85727503, -1.24458455, -0.88272215, -0.22491125] - ) - - @data(H2_SPARSE_PAULI, H2_PAULI, H2_OP) - def test_ce_k4_filtered(self, op): - """Test for k=4 eigenvalues with filter""" - - # define filter criterion - # pylint: disable=unused-argument - def criterion(x, v, a_v): - return v >= -1 - - algo = NumPyEigensolver(k=4, filter_criterion=criterion) - result = algo.compute_eigenvalues(operator=op, aux_operators=[]) - self.assertEqual(len(result.eigenvalues), 2) - self.assertEqual(len(result.eigenstates), 2) - self.assertEqual(result.eigenvalues.dtype, np.float64) - np.testing.assert_array_almost_equal(result.eigenvalues, [-0.88272215, -0.22491125]) - - @data(H2_SPARSE_PAULI, H2_PAULI, H2_OP) - def test_ce_k4_filtered_empty(self, op): - """Test for k=4 eigenvalues with filter always returning False""" - - # define filter criterion - # pylint: disable=unused-argument - def criterion(x, v, a_v): - return False - - algo = NumPyEigensolver(k=4, filter_criterion=criterion) - result = algo.compute_eigenvalues(operator=op, aux_operators=[]) - self.assertEqual(len(result.eigenvalues), 0) - self.assertEqual(len(result.eigenstates), 0) - - @data( - SparsePauliOp(["X"], coeffs=[1.0]), - SparsePauliOp(["Y"], coeffs=[1.0]), - SparsePauliOp(["Z"], coeffs=[1.0]), - ) - def test_ce_k1_1q(self, op): - """Test for 1 qubit operator""" - algo = NumPyEigensolver(k=1) - result = algo.compute_eigenvalues(operator=op) - np.testing.assert_array_almost_equal(result.eigenvalues, [-1]) - - @data( - SparsePauliOp(["X"], coeffs=[1.0]), - SparsePauliOp(["Y"], coeffs=[1.0]), - SparsePauliOp(["Z"], coeffs=[1.0]), - ) - def test_ce_k2_1q(self, op): - """Test for 1 qubit operator""" - algo = NumPyEigensolver(k=2) - result = algo.compute_eigenvalues(operator=op) - np.testing.assert_array_almost_equal(result.eigenvalues, [-1, 1]) - - @data(H2_SPARSE_PAULI, H2_PAULI, H2_OP) - def test_aux_operators_list(self, op): - """Test list-based aux_operators.""" - aux_op1 = Operator(SparsePauliOp(["II"], coeffs=[2.0]).to_matrix()) - aux_op2 = SparsePauliOp(["II", "ZZ", "YY", "XX"], coeffs=[0.5, 0.5, 0.5, -0.5]) - aux_ops = [aux_op1, aux_op2] - algo = NumPyEigensolver() - result = algo.compute_eigenvalues(operator=op, aux_operators=aux_ops) - self.assertEqual(len(result.eigenvalues), 1) - self.assertEqual(len(result.eigenstates), 1) - self.assertEqual(result.eigenvalues.dtype, np.float64) - self.assertAlmostEqual(result.eigenvalues[0], -1.85727503) - self.assertEqual(len(result.aux_operators_evaluated), 1) - self.assertEqual(len(result.aux_operators_evaluated[0]), 2) - # expectation values - self.assertAlmostEqual(result.aux_operators_evaluated[0][0][0], 2, places=6) - self.assertAlmostEqual(result.aux_operators_evaluated[0][1][0], 0, places=6) - # metadata - self.assertAlmostEqual(result.aux_operators_evaluated[0][0][1].pop("variance"), 0.0) - self.assertAlmostEqual(result.aux_operators_evaluated[0][1][1].pop("variance"), 0.0) - - # Go again with additional None and zero operators - extra_ops = [*aux_ops, None, 0] - result = algo.compute_eigenvalues(operator=op, aux_operators=extra_ops) - self.assertEqual(len(result.eigenvalues), 1) - self.assertEqual(len(result.eigenstates), 1) - self.assertEqual(result.eigenvalues.dtype, np.float64) - self.assertAlmostEqual(result.eigenvalues[0], -1.85727503) - self.assertEqual(len(result.aux_operators_evaluated), 1) - self.assertEqual(len(result.aux_operators_evaluated[0]), 4) - # expectation values - self.assertAlmostEqual(result.aux_operators_evaluated[0][0][0], 2, places=6) - self.assertAlmostEqual(result.aux_operators_evaluated[0][1][0], 0, places=6) - self.assertIsNone(result.aux_operators_evaluated[0][2], None) - self.assertEqual(result.aux_operators_evaluated[0][3][0], 0.0) - # metadata - self.assertAlmostEqual(result.aux_operators_evaluated[0][0][1].pop("variance"), 0.0) - self.assertAlmostEqual(result.aux_operators_evaluated[0][1][1].pop("variance"), 0.0) - self.assertEqual(result.aux_operators_evaluated[0][3][1].pop("variance"), 0.0) - - @data(H2_SPARSE_PAULI, H2_PAULI, H2_OP) - def test_aux_operators_dict(self, op): - """Test dict-based aux_operators.""" - aux_op1 = Operator(SparsePauliOp(["II"], coeffs=[2.0]).to_matrix()) - aux_op2 = SparsePauliOp(["II", "ZZ", "YY", "XX"], coeffs=[0.5, 0.5, 0.5, -0.5]) - aux_ops = {"aux_op1": aux_op1, "aux_op2": aux_op2} - algo = NumPyEigensolver() - result = algo.compute_eigenvalues(operator=op, aux_operators=aux_ops) - self.assertEqual(len(result.eigenvalues), 1) - self.assertEqual(len(result.eigenstates), 1) - self.assertEqual(result.eigenvalues.dtype, np.float64) - self.assertAlmostEqual(result.eigenvalues[0], -1.85727503) - self.assertEqual(len(result.aux_operators_evaluated), 1) - self.assertEqual(len(result.aux_operators_evaluated[0]), 2) - # expectation values - self.assertAlmostEqual(result.aux_operators_evaluated[0]["aux_op1"][0], 2, places=6) - self.assertAlmostEqual(result.aux_operators_evaluated[0]["aux_op2"][0], 0, places=6) - # metadata - self.assertAlmostEqual(result.aux_operators_evaluated[0]["aux_op1"][1].pop("variance"), 0.0) - self.assertAlmostEqual(result.aux_operators_evaluated[0]["aux_op2"][1].pop("variance"), 0.0) - - # Go again with additional None and zero operators - extra_ops = {**aux_ops, "None_operator": None, "zero_operator": 0} - result = algo.compute_eigenvalues(operator=op, aux_operators=extra_ops) - self.assertEqual(len(result.eigenvalues), 1) - self.assertEqual(len(result.eigenstates), 1) - self.assertEqual(result.eigenvalues.dtype, np.float64) - self.assertAlmostEqual(result.eigenvalues[0], -1.85727503) - self.assertEqual(len(result.aux_operators_evaluated), 1) - self.assertEqual(len(result.aux_operators_evaluated[0]), 3) - # expectation values - self.assertAlmostEqual(result.aux_operators_evaluated[0]["aux_op1"][0], 2, places=6) - self.assertAlmostEqual(result.aux_operators_evaluated[0]["aux_op2"][0], 0, places=6) - self.assertEqual(result.aux_operators_evaluated[0]["zero_operator"][0], 0.0) - self.assertTrue("None_operator" not in result.aux_operators_evaluated[0].keys()) - # metadata - self.assertAlmostEqual(result.aux_operators_evaluated[0]["aux_op1"][1].pop("variance"), 0.0) - self.assertAlmostEqual(result.aux_operators_evaluated[0]["aux_op2"][1].pop("variance"), 0.0) - self.assertAlmostEqual( - result.aux_operators_evaluated[0]["zero_operator"][1].pop("variance"), 0.0 - ) - - def test_pauli_op(self): - """Test simple pauli operator""" - algo = NumPyEigensolver(k=1) - result = algo.compute_eigenvalues(operator=Pauli("X")) - np.testing.assert_array_almost_equal(result.eigenvalues, [-1]) - - def test_scalar_op(self): - """Test scalar operator""" - algo = NumPyEigensolver(k=1) - with self.assertRaises(AlgorithmError): - algo.compute_eigenvalues(operator=ScalarOp(1)) - - -if __name__ == "__main__": - unittest.main() diff --git a/test/python/algorithms/eigensolvers/test_vqd.py b/test/python/algorithms/eigensolvers/test_vqd.py deleted file mode 100644 index 47eab6404f2c..000000000000 --- a/test/python/algorithms/eigensolvers/test_vqd.py +++ /dev/null @@ -1,453 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2022 -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. - -"""Test VQD""" - -import unittest -import warnings -from test.python.algorithms import QiskitAlgorithmsTestCase - -import numpy as np -from ddt import data, ddt - -from qiskit import QuantumCircuit -from qiskit.algorithms.eigensolvers import VQD, VQDResult -from qiskit.algorithms import AlgorithmError -from qiskit.algorithms.optimizers import COBYLA, L_BFGS_B, SLSQP, SPSA -from qiskit.algorithms.state_fidelities import ComputeUncompute -from qiskit.circuit.library import TwoLocal, RealAmplitudes -from qiskit.opflow import PauliSumOp -from qiskit.primitives import Sampler, Estimator -from qiskit.quantum_info import SparsePauliOp -from qiskit.quantum_info.operators import Operator -from qiskit.utils import algorithm_globals - - -H2_SPARSE_PAULI = SparsePauliOp.from_list( - [ - ("II", -1.052373245772859), - ("IZ", 0.39793742484318045), - ("ZI", -0.39793742484318045), - ("ZZ", -0.01128010425623538), - ("XX", 0.18093119978423156), - ] -) -H2_OP = Operator(H2_SPARSE_PAULI.to_matrix()) - -H2_PAULI = PauliSumOp(H2_SPARSE_PAULI) - - -@ddt -class TestVQD(QiskitAlgorithmsTestCase): - """Test VQD""" - - def setUp(self): - super().setUp() - self.seed = 50 - with warnings.catch_warnings(): - warnings.filterwarnings("ignore", category=DeprecationWarning) - algorithm_globals.random_seed = self.seed - - self.h2_energy = -1.85727503 - self.h2_energy_excited = [-1.85727503, -1.24458455, -0.88272215, -0.22491125] - - self.ryrz_wavefunction = TwoLocal( - rotation_blocks=["ry", "rz"], entanglement_blocks="cz", reps=1 - ) - self.ry_wavefunction = TwoLocal(rotation_blocks="ry", entanglement_blocks="cz") - - self.estimator = Estimator() - self.estimator_shots = Estimator(options={"shots": 1024, "seed": self.seed}) - self.fidelity = ComputeUncompute(Sampler()) - self.betas = [50, 50] - - @data(H2_PAULI, H2_OP, H2_SPARSE_PAULI) - def test_basic_operator(self, op): - """Test the VQD without aux_operators.""" - wavefunction = self.ryrz_wavefunction - vqd = VQD( - estimator=self.estimator, - fidelity=self.fidelity, - ansatz=wavefunction, - optimizer=COBYLA(), - betas=self.betas, - ) - - result = vqd.compute_eigenvalues(operator=op) - - with self.subTest(msg="test eigenvalue"): - np.testing.assert_array_almost_equal( - result.eigenvalues.real, self.h2_energy_excited[:2], decimal=1 - ) - - with self.subTest(msg="test dimension of optimal point"): - self.assertEqual(len(result.optimal_points[-1]), 8) - - with self.subTest(msg="assert cost_function_evals is set"): - self.assertIsNotNone(result.cost_function_evals) - - with self.subTest(msg="assert optimizer_times is set"): - self.assertIsNotNone(result.optimizer_times) - - with self.subTest(msg="assert return ansatz is set"): - job = self.estimator.run( - result.optimal_circuits, - [op] * len(result.optimal_points), - result.optimal_points, - ) - np.testing.assert_array_almost_equal(job.result().values, result.eigenvalues, 6) - - with self.subTest(msg="assert returned values are eigenvalues"): - np.testing.assert_array_almost_equal( - result.optimal_values, self.h2_energy_excited[:2], decimal=3 - ) - - def test_full_spectrum(self): - """Test obtaining all eigenvalues.""" - vqd = VQD(self.estimator, self.fidelity, self.ryrz_wavefunction, optimizer=L_BFGS_B(), k=4) - result = vqd.compute_eigenvalues(H2_PAULI) - np.testing.assert_array_almost_equal( - result.eigenvalues.real, self.h2_energy_excited, decimal=2 - ) - - @data(H2_PAULI, H2_SPARSE_PAULI) - def test_beta_autoeval(self, op): - """Test beta autoevaluation for different operator types.""" - - with self.assertLogs(level="INFO") as logs: - vqd = VQD( - self.estimator_shots, self.fidelity, self.ryrz_wavefunction, optimizer=L_BFGS_B() - ) - _ = vqd.compute_eigenvalues(op) - - # the first log message shows the value of beta[0] - beta = float(logs.output[0].split()[-1]) - self.assertAlmostEqual(beta, 20.40459399499687, 4) - - @data(H2_PAULI, H2_OP, H2_SPARSE_PAULI) - def test_mismatching_num_qubits(self, op): - """Ensuring circuit and operator mismatch is caught""" - wavefunction = QuantumCircuit(1) - optimizer = SLSQP(maxiter=50) - vqd = VQD( - estimator=self.estimator, - fidelity=self.fidelity, - k=1, - ansatz=wavefunction, - optimizer=optimizer, - betas=self.betas, - ) - with self.assertRaises(AlgorithmError): - _ = vqd.compute_eigenvalues(operator=op) - - @data(H2_PAULI, H2_OP, H2_SPARSE_PAULI) - def test_missing_varform_params(self, op): - """Test specifying a variational form with no parameters raises an error.""" - circuit = QuantumCircuit(op.num_qubits) - vqd = VQD( - estimator=self.estimator, - fidelity=self.fidelity, - ansatz=circuit, - optimizer=SLSQP(), - k=1, - betas=self.betas, - ) - with self.assertRaises(AlgorithmError): - vqd.compute_eigenvalues(operator=op) - - @data(H2_PAULI, H2_OP, H2_SPARSE_PAULI) - def test_callback(self, op): - """Test the callback on VQD.""" - history = {"eval_count": [], "parameters": [], "mean": [], "metadata": [], "step": []} - - def store_intermediate_result(eval_count, parameters, mean, metadata, step): - history["eval_count"].append(eval_count) - history["parameters"].append(parameters) - history["mean"].append(mean) - history["metadata"].append(metadata) - history["step"].append(step) - - optimizer = COBYLA(maxiter=3) - wavefunction = self.ry_wavefunction - - vqd = VQD( - estimator=self.estimator_shots, - fidelity=self.fidelity, - ansatz=wavefunction, - optimizer=optimizer, - callback=store_intermediate_result, - betas=self.betas, - ) - - vqd.compute_eigenvalues(operator=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"])) - self.assertTrue(all(isinstance(count, int) for count in history["step"])) - for params in history["parameters"]: - self.assertTrue(all(isinstance(param, float) for param in params)) - - ref_eval_count = [1, 2, 3, 1, 2, 3] - ref_mean = [-1.07, -1.45, -1.37, 37.43, 48.55, 28.94] - # new ref_mean for statevector simulator. The old unit test was on qasm - # and the ref_mean values were slightly different. - - ref_step = [1, 1, 1, 2, 2, 2] - - np.testing.assert_array_almost_equal(history["eval_count"], ref_eval_count, decimal=0) - np.testing.assert_array_almost_equal(history["mean"], ref_mean, decimal=2) - np.testing.assert_array_almost_equal(history["step"], ref_step, decimal=0) - - @data(H2_PAULI, H2_OP, H2_SPARSE_PAULI) - def test_vqd_optimizer(self, op): - """Test running same VQD twice to re-use optimizer, then switch optimizer""" - - vqd = VQD( - estimator=self.estimator, - fidelity=self.fidelity, - ansatz=RealAmplitudes(), - optimizer=SLSQP(), - k=2, - betas=self.betas, - ) - - def run_check(): - result = vqd.compute_eigenvalues(operator=op) - np.testing.assert_array_almost_equal( - result.eigenvalues.real, self.h2_energy_excited[:2], decimal=3 - ) - - run_check() - - with self.subTest("Optimizer re-use"): - run_check() - - with self.subTest("Optimizer replace"): - vqd.optimizer = L_BFGS_B() - run_check() - - with self.subTest("Batched optimizer replace"): - vqd.optimizer = SLSQP(maxiter=60, max_evals_grouped=10) - run_check() - - with self.subTest("SPSA replace"): - # SPSA takes too long to converge, so we will - # only check that it runs with no errors. - vqd.optimizer = SPSA(maxiter=5, learning_rate=0.01, perturbation=0.01) - result = vqd.compute_eigenvalues(operator=op) - self.assertIsInstance(result, VQDResult) - - @data(H2_PAULI, H2_OP, H2_SPARSE_PAULI) - def test_optimizer_list(self, op): - """Test sending an optimizer list""" - - optimizers = [SLSQP(), L_BFGS_B()] - initial_point_1 = [ - 1.70256666, - -5.34843975, - -0.39542903, - 5.99477786, - -2.74374986, - -4.85284669, - 0.2442925, - -1.51638917, - ] - initial_point_2 = [ - 0.5, - 0.5, - 0.5, - 0.5, - 0.5, - 0.5, - 0.5, - 0.5, - ] - vqd = VQD( - estimator=self.estimator, - fidelity=self.fidelity, - ansatz=RealAmplitudes(), - optimizer=optimizers, - initial_point=[initial_point_1, initial_point_2], - k=2, - betas=self.betas, - ) - - result = vqd.compute_eigenvalues(operator=op) - np.testing.assert_array_almost_equal( - result.eigenvalues.real, self.h2_energy_excited[:2], decimal=3 - ) - - @data(H2_PAULI, H2_OP, H2_SPARSE_PAULI) - def test_aux_operators_list(self, op): - """Test list-based aux_operators.""" - wavefunction = self.ry_wavefunction - vqd = VQD( - estimator=self.estimator, - fidelity=self.fidelity, - ansatz=wavefunction, - optimizer=SLSQP(), - k=2, - betas=self.betas, - ) - - # Start with an empty list - result = vqd.compute_eigenvalues(op, aux_operators=[]) - np.testing.assert_array_almost_equal( - result.eigenvalues.real, self.h2_energy_excited[:2], decimal=2 - ) - self.assertIsNone(result.aux_operators_evaluated) - - # Go again 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 = vqd.compute_eigenvalues(op, aux_operators=aux_ops) - np.testing.assert_array_almost_equal( - result.eigenvalues.real, self.h2_energy_excited[:2], decimal=2 - ) - self.assertEqual(len(result.aux_operators_evaluated), 2) - # expectation values - self.assertAlmostEqual(result.aux_operators_evaluated[0][0][0], 2, places=2) - self.assertAlmostEqual(result.aux_operators_evaluated[0][1][0], 0, places=2) - # metadata - self.assertIsInstance(result.aux_operators_evaluated[0][1][1], dict) - self.assertIsInstance(result.aux_operators_evaluated[0][1][1], dict) - - # Go again with additional None and zero operators - extra_ops = [*aux_ops, None, 0] - result = vqd.compute_eigenvalues(op, aux_operators=extra_ops) - np.testing.assert_array_almost_equal( - result.eigenvalues.real, self.h2_energy_excited[:2], decimal=2 - ) - self.assertEqual(len(result.aux_operators_evaluated), 2) - # expectation values - self.assertAlmostEqual(result.aux_operators_evaluated[0][0][0], 2, places=2) - self.assertAlmostEqual(result.aux_operators_evaluated[0][1][0], 0, places=2) - self.assertEqual(result.aux_operators_evaluated[0][2][0], 0.0) - self.assertEqual(result.aux_operators_evaluated[0][3][0], 0.0) - # metadata - self.assertIsInstance(result.aux_operators_evaluated[0][0][1], dict) - self.assertIsInstance(result.aux_operators_evaluated[0][1][1], dict) - self.assertIsInstance(result.aux_operators_evaluated[0][3][1], dict) - - @data(H2_PAULI, H2_OP, H2_SPARSE_PAULI) - def test_aux_operators_dict(self, op): - """Test dictionary compatibility of aux_operators""" - wavefunction = self.ry_wavefunction - vqd = VQD( - estimator=self.estimator, - fidelity=self.fidelity, - ansatz=wavefunction, - optimizer=SLSQP(), - betas=self.betas, - ) - - # Start with an empty dictionary - result = vqd.compute_eigenvalues(op, aux_operators={}) - np.testing.assert_array_almost_equal( - result.eigenvalues.real, self.h2_energy_excited[:2], decimal=2 - ) - self.assertIsNone(result.aux_operators_evaluated) - - # Go again 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 = vqd.compute_eigenvalues(op, aux_operators=aux_ops) - self.assertEqual(len(result.eigenvalues), 2) - self.assertEqual(result.eigenvalues.dtype, np.complex128) - self.assertAlmostEqual(result.eigenvalues[0], -1.85727503, 2) - self.assertEqual(len(result.aux_operators_evaluated), 2) - self.assertEqual(len(result.aux_operators_evaluated[0]), 2) - # expectation values - self.assertAlmostEqual(result.aux_operators_evaluated[0]["aux_op1"][0], 2, places=6) - self.assertAlmostEqual(result.aux_operators_evaluated[0]["aux_op2"][0], 0, places=1) - # metadata - self.assertIsInstance(result.aux_operators_evaluated[0]["aux_op1"][1], dict) - self.assertIsInstance(result.aux_operators_evaluated[0]["aux_op2"][1], dict) - - # Go again with additional None and zero operators - extra_ops = {**aux_ops, "None_operator": None, "zero_operator": 0} - result = vqd.compute_eigenvalues(op, aux_operators=extra_ops) - self.assertEqual(len(result.eigenvalues), 2) - self.assertEqual(result.eigenvalues.dtype, np.complex128) - self.assertAlmostEqual(result.eigenvalues[0], -1.85727503, places=5) - self.assertEqual(len(result.aux_operators_evaluated), 2) - self.assertEqual(len(result.aux_operators_evaluated[0]), 3) - # expectation values - self.assertAlmostEqual(result.aux_operators_evaluated[0]["aux_op1"][0], 2, places=6) - self.assertAlmostEqual(result.aux_operators_evaluated[0]["aux_op2"][0], 0.0, places=2) - self.assertEqual(result.aux_operators_evaluated[0]["zero_operator"][0], 0.0) - self.assertTrue("None_operator" not in result.aux_operators_evaluated[0].keys()) - # metadata - self.assertIsInstance(result.aux_operators_evaluated[0]["aux_op1"][1], dict) - self.assertIsInstance(result.aux_operators_evaluated[0]["aux_op2"][1], dict) - self.assertIsInstance(result.aux_operators_evaluated[0]["zero_operator"][1], dict) - - @data(H2_PAULI, H2_OP, H2_SPARSE_PAULI) - def test_aux_operator_std_dev(self, op): - """Test non-zero standard deviations of aux operators.""" - wavefunction = self.ry_wavefunction - vqd = VQD( - estimator=self.estimator, - fidelity=self.fidelity, - ansatz=wavefunction, - initial_point=[ - 1.70256666, - -5.34843975, - -0.39542903, - 5.99477786, - -2.74374986, - -4.85284669, - 0.2442925, - -1.51638917, - ], - optimizer=COBYLA(maxiter=0), - betas=self.betas, - ) - - # Go again 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 = vqd.compute_eigenvalues(op, aux_operators=aux_ops) - self.assertEqual(len(result.aux_operators_evaluated), 2) - # expectation values - self.assertAlmostEqual(result.aux_operators_evaluated[0][0][0], 2.0, places=1) - self.assertAlmostEqual( - result.aux_operators_evaluated[0][1][0], 0.0019531249999999445, places=1 - ) - # metadata - self.assertIsInstance(result.aux_operators_evaluated[0][0][1], dict) - self.assertIsInstance(result.aux_operators_evaluated[0][1][1], dict) - - # Go again with additional None and zero operators - aux_ops = [*aux_ops, None, 0] - result = vqd.compute_eigenvalues(op, aux_operators=aux_ops) - self.assertEqual(len(result.aux_operators_evaluated[0]), 4) - # expectation values - self.assertAlmostEqual(result.aux_operators_evaluated[0][0][0], 2.0, places=1) - self.assertAlmostEqual( - result.aux_operators_evaluated[0][1][0], 0.0019531249999999445, places=1 - ) - self.assertEqual(result.aux_operators_evaluated[0][2][0], 0.0) - self.assertEqual(result.aux_operators_evaluated[0][3][0], 0.0) - # metadata - self.assertIsInstance(result.aux_operators_evaluated[0][0][1], dict) - self.assertIsInstance(result.aux_operators_evaluated[0][1][1], dict) - self.assertIsInstance(result.aux_operators_evaluated[0][2][1], dict) - self.assertIsInstance(result.aux_operators_evaluated[0][3][1], dict) - - -if __name__ == "__main__": - unittest.main() diff --git a/test/python/algorithms/evolvers/__init__.py b/test/python/algorithms/evolvers/__init__.py deleted file mode 100644 index fdb172d367f0..000000000000 --- a/test/python/algorithms/evolvers/__init__.py +++ /dev/null @@ -1,11 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2022. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. diff --git a/test/python/algorithms/evolvers/test_evolution_problem.py b/test/python/algorithms/evolvers/test_evolution_problem.py deleted file mode 100644 index dc9d45f548a2..000000000000 --- a/test/python/algorithms/evolvers/test_evolution_problem.py +++ /dev/null @@ -1,136 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2022, 2023. -# -# 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 evolver problem class.""" -import unittest -from test.python.algorithms import QiskitAlgorithmsTestCase -from ddt import data, ddt, unpack -from numpy.testing import assert_raises - -from qiskit.algorithms.evolvers.evolution_problem import EvolutionProblem -from qiskit.circuit import Parameter -from qiskit.opflow import Y, Z, One, X, Zero - - -@ddt -class TestEvolutionProblem(QiskitAlgorithmsTestCase): - """Test evolver problem class.""" - - def test_init_default(self): - """Tests that all default fields are initialized correctly.""" - hamiltonian = Y - time = 2.5 - initial_state = One - - with self.assertWarns(DeprecationWarning): - evo_problem = EvolutionProblem(hamiltonian, time, initial_state) - - expected_hamiltonian = Y - expected_time = 2.5 - expected_initial_state = One - expected_aux_operators = None - expected_t_param = None - expected_param_value_dict = None - - self.assertEqual(evo_problem.hamiltonian, expected_hamiltonian) - self.assertEqual(evo_problem.time, expected_time) - self.assertEqual(evo_problem.initial_state, expected_initial_state) - self.assertEqual(evo_problem.aux_operators, expected_aux_operators) - self.assertEqual(evo_problem.t_param, expected_t_param) - self.assertEqual(evo_problem.param_value_dict, expected_param_value_dict) - - def test_init_all(self): - """Tests that all fields are initialized correctly.""" - t_parameter = Parameter("t") - with self.assertWarns(DeprecationWarning): - hamiltonian = t_parameter * Z + Y - time = 2 - initial_state = One - aux_operators = [X, Y] - param_value_dict = {t_parameter: 3.2} - - with self.assertWarns(DeprecationWarning): - evo_problem = EvolutionProblem( - hamiltonian, - time, - initial_state, - aux_operators, - t_param=t_parameter, - param_value_dict=param_value_dict, - ) - expected_hamiltonian = Y + t_parameter * Z - - expected_time = 2 - expected_initial_state = One - expected_aux_operators = [X, Y] - expected_t_param = t_parameter - expected_param_value_dict = {t_parameter: 3.2} - - with self.assertWarns(DeprecationWarning): - self.assertEqual(evo_problem.hamiltonian, expected_hamiltonian) - - self.assertEqual(evo_problem.time, expected_time) - self.assertEqual(evo_problem.initial_state, expected_initial_state) - self.assertEqual(evo_problem.aux_operators, expected_aux_operators) - self.assertEqual(evo_problem.t_param, expected_t_param) - self.assertEqual(evo_problem.param_value_dict, expected_param_value_dict) - - @data([Y, -1, One], [Y, -1.2, One], [Y, 0, One]) - @unpack - def test_init_errors(self, hamiltonian, time, initial_state): - """Tests expected errors are thrown on invalid time argument.""" - with self.assertWarns(DeprecationWarning), assert_raises(ValueError): - _ = EvolutionProblem(hamiltonian, time, initial_state) - - def test_validate_params(self): - """Tests expected errors are thrown on parameters mismatch.""" - param_x = Parameter("x") - param_y = Parameter("y") - with self.subTest(msg="Parameter missing in dict."): - with self.assertWarns(DeprecationWarning): - hamiltonian = param_x * X + param_y * Y - - param_dict = {param_y: 2} - with self.assertWarns(DeprecationWarning): - evolution_problem = EvolutionProblem( - hamiltonian, 2, Zero, param_value_dict=param_dict - ) - with assert_raises(ValueError): - evolution_problem.validate_params() - - with self.subTest(msg="Empty dict."): - with self.assertWarns(DeprecationWarning): - hamiltonian = param_x * X + param_y * Y - - param_dict = {} - with self.assertWarns(DeprecationWarning): - evolution_problem = EvolutionProblem( - hamiltonian, 2, Zero, param_value_dict=param_dict - ) - with assert_raises(ValueError): - evolution_problem.validate_params() - - with self.subTest(msg="Extra parameter in dict."): - with self.assertWarns(DeprecationWarning): - hamiltonian = param_x * X + param_y * Y - - param_dict = {param_y: 2, param_x: 1, Parameter("z"): 1} - with self.assertWarns(DeprecationWarning): - evolution_problem = EvolutionProblem( - hamiltonian, 2, Zero, param_value_dict=param_dict - ) - with assert_raises(ValueError): - evolution_problem.validate_params() - - -if __name__ == "__main__": - unittest.main() diff --git a/test/python/algorithms/evolvers/test_evolution_result.py b/test/python/algorithms/evolvers/test_evolution_result.py deleted file mode 100644 index 2fe40d8c66f8..000000000000 --- a/test/python/algorithms/evolvers/test_evolution_result.py +++ /dev/null @@ -1,50 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2022. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. -"""Class for testing evolution result.""" -import unittest - -from test.python.algorithms import QiskitAlgorithmsTestCase -from qiskit.algorithms.evolvers.evolution_result import EvolutionResult -from qiskit.opflow import Zero - - -class TestEvolutionResult(QiskitAlgorithmsTestCase): - """Class for testing evolution result and relevant metadata.""" - - def test_init_state(self): - """Tests that a class is initialized correctly with an evolved_state.""" - evolved_state = Zero - with self.assertWarns(DeprecationWarning): - evo_result = EvolutionResult(evolved_state=evolved_state) - - expected_state = Zero - expected_aux_ops_evaluated = None - - self.assertEqual(evo_result.evolved_state, expected_state) - self.assertEqual(evo_result.aux_ops_evaluated, expected_aux_ops_evaluated) - - def test_init_observable(self): - """Tests that a class is initialized correctly with an evolved_observable.""" - evolved_state = Zero - evolved_aux_ops_evaluated = [(5j, 5j), (1.0, 8j), (5 + 1j, 6 + 1j)] - with self.assertWarns(DeprecationWarning): - evo_result = EvolutionResult(evolved_state, evolved_aux_ops_evaluated) - - expected_state = Zero - expected_aux_ops_evaluated = [(5j, 5j), (1.0, 8j), (5 + 1j, 6 + 1j)] - - self.assertEqual(evo_result.evolved_state, expected_state) - self.assertEqual(evo_result.aux_ops_evaluated, expected_aux_ops_evaluated) - - -if __name__ == "__main__": - unittest.main() diff --git a/test/python/algorithms/evolvers/trotterization/__init__.py b/test/python/algorithms/evolvers/trotterization/__init__.py deleted file mode 100644 index 96c0cf22bec9..000000000000 --- a/test/python/algorithms/evolvers/trotterization/__init__.py +++ /dev/null @@ -1,11 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2021. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. diff --git a/test/python/algorithms/evolvers/trotterization/test_trotter_qrte.py b/test/python/algorithms/evolvers/trotterization/test_trotter_qrte.py deleted file mode 100644 index 6635d4db5d85..000000000000 --- a/test/python/algorithms/evolvers/trotterization/test_trotter_qrte.py +++ /dev/null @@ -1,259 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2021, 2023. -# -# 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 TrotterQRTE.""" - -import unittest -import warnings - -from test.python.opflow import QiskitOpflowTestCase -from ddt import ddt, data, unpack -import numpy as np -from numpy.testing import assert_raises - -from qiskit import BasicAer, QuantumCircuit -from qiskit.algorithms import EvolutionProblem -from qiskit.algorithms.evolvers.trotterization import ( - TrotterQRTE, -) -from qiskit.circuit.library import ZGate -from qiskit.quantum_info import Statevector -from qiskit.utils import algorithm_globals, QuantumInstance -from qiskit.circuit import Parameter -from qiskit.opflow import ( - X, - Z, - Zero, - VectorStateFn, - StateFn, - I, - Y, - SummedOp, - ExpectationFactory, -) -from qiskit.synthesis import SuzukiTrotter, QDrift - - -@ddt -class TestTrotterQRTE(QiskitOpflowTestCase): - """TrotterQRTE tests.""" - - def setUp(self): - super().setUp() - self.seed = 50 - with warnings.catch_warnings(): - warnings.filterwarnings("ignore", category=DeprecationWarning) - algorithm_globals.random_seed = self.seed - backend_statevector = BasicAer.get_backend("statevector_simulator") - backend_qasm = BasicAer.get_backend("qasm_simulator") - with self.assertWarns(DeprecationWarning): - self.quantum_instance = QuantumInstance( - backend=backend_statevector, - shots=1, - seed_simulator=self.seed, - seed_transpiler=self.seed, - ) - self.quantum_instance_qasm = QuantumInstance( - backend=backend_qasm, - shots=8000, - seed_simulator=self.seed, - seed_transpiler=self.seed, - ) - self.backends_dict = { - "qi_sv": self.quantum_instance, - "qi_qasm": self.quantum_instance_qasm, - "b_sv": backend_statevector, - "None": None, - } - - self.backends_names = ["qi_qasm", "b_sv", "None", "qi_sv"] - self.backends_names_not_none = ["qi_sv", "b_sv", "qi_qasm"] - - @data( - ( - None, - VectorStateFn( - Statevector([0.29192658 - 0.45464871j, 0.70807342 - 0.45464871j], dims=(2,)) - ), - ), - ( - SuzukiTrotter(), - VectorStateFn(Statevector([0.29192658 - 0.84147098j, 0.0 - 0.45464871j], dims=(2,))), - ), - ) - @unpack - def test_trotter_qrte_trotter_single_qubit(self, product_formula, expected_state): - """Test for default TrotterQRTE on a single qubit.""" - operator = SummedOp([X, Z]) - initial_state = StateFn([1, 0]) - time = 1 - with self.assertWarns(DeprecationWarning): - evolution_problem = EvolutionProblem(operator, time, initial_state) - trotter_qrte = TrotterQRTE(product_formula=product_formula) - evolution_result_state_circuit = trotter_qrte.evolve(evolution_problem).evolved_state - - np.testing.assert_equal(evolution_result_state_circuit.eval(), expected_state) - - def test_trotter_qrte_trotter_single_qubit_aux_ops(self): - """Test for default TrotterQRTE on a single qubit with auxiliary operators.""" - operator = SummedOp([X, Z]) - # LieTrotter with 1 rep - aux_ops = [X, Y] - - initial_state = Zero - time = 3 - with self.assertWarns(DeprecationWarning): - evolution_problem = EvolutionProblem(operator, time, initial_state, aux_ops) - - expected_evolved_state = VectorStateFn( - Statevector([0.98008514 + 0.13970775j, 0.01991486 + 0.13970775j], dims=(2,)) - ) - expected_aux_ops_evaluated = [(0.078073, 0.0), (0.268286, 0.0)] - expected_aux_ops_evaluated_qasm = [ - (0.05799999999999995, 0.011161518713866855), - (0.2495, 0.010826759383582883), - ] - - for backend_name in self.backends_names_not_none: - with self.subTest(msg=f"Test {backend_name} backend."): - with warnings.catch_warnings(): - warnings.filterwarnings("ignore", category=DeprecationWarning) - algorithm_globals.random_seed = 0 - backend = self.backends_dict[backend_name] - with self.assertWarns(DeprecationWarning): - expectation = ExpectationFactory.build( - operator=operator, - backend=backend, - ) - with self.assertWarns(DeprecationWarning): - trotter_qrte = TrotterQRTE(quantum_instance=backend, expectation=expectation) - evolution_result = trotter_qrte.evolve(evolution_problem) - - np.testing.assert_equal( - evolution_result.evolved_state.eval(), expected_evolved_state - ) - if backend_name == "qi_qasm": - expected_aux_ops_evaluated = expected_aux_ops_evaluated_qasm - np.testing.assert_array_almost_equal( - evolution_result.aux_ops_evaluated, expected_aux_ops_evaluated - ) - - @data( - ( - SummedOp([(X ^ Y), (Y ^ X)]), - VectorStateFn( - Statevector( - [-0.41614684 + 0.0j, 0.0 + 0.0j, 0.0 + 0.0j, 0.90929743 + 0.0j], dims=(2, 2) - ) - ), - ), - ( - (Z ^ Z) + (Z ^ I) + (I ^ Z), - VectorStateFn( - Statevector( - [-0.9899925 - 0.14112001j, 0.0 + 0.0j, 0.0 + 0.0j, 0.0 + 0.0j], dims=(2, 2) - ) - ), - ), - ( - Y ^ Y, - VectorStateFn( - Statevector( - [0.54030231 + 0.0j, 0.0 + 0.0j, 0.0 + 0.0j, 0.0 + 0.84147098j], dims=(2, 2) - ) - ), - ), - ) - @unpack - def test_trotter_qrte_trotter_two_qubits(self, operator, expected_state): - """Test for TrotterQRTE on two qubits with various types of a Hamiltonian.""" - # LieTrotter with 1 rep - initial_state = StateFn([1, 0, 0, 0]) - with self.assertWarns(DeprecationWarning): - evolution_problem = EvolutionProblem(operator, 1, initial_state) - trotter_qrte = TrotterQRTE() - evolution_result = trotter_qrte.evolve(evolution_problem) - np.testing.assert_equal(evolution_result.evolved_state.eval(), expected_state) - - def test_trotter_qrte_trotter_two_qubits_with_params(self): - """Test for TrotterQRTE on two qubits with a parametrized Hamiltonian.""" - # LieTrotter with 1 rep - initial_state = StateFn([1, 0, 0, 0]) - w_param = Parameter("w") - u_param = Parameter("u") - params_dict = {w_param: 2.0, u_param: 3.0} - operator = w_param * (Z ^ Z) / 2.0 + (Z ^ I) + u_param * (I ^ Z) / 3.0 - time = 1 - expected_state = VectorStateFn( - Statevector([-0.9899925 - 0.14112001j, 0.0 + 0.0j, 0.0 + 0.0j, 0.0 + 0.0j], dims=(2, 2)) - ) - with self.assertWarns(DeprecationWarning): - evolution_problem = EvolutionProblem( - operator, time, initial_state, param_value_dict=params_dict - ) - trotter_qrte = TrotterQRTE() - evolution_result = trotter_qrte.evolve(evolution_problem) - np.testing.assert_equal(evolution_result.evolved_state.eval(), expected_state) - - @data( - ( - Zero, - VectorStateFn( - Statevector([0.23071786 - 0.69436148j, 0.4646314 - 0.49874749j], dims=(2,)) - ), - ), - ( - QuantumCircuit(1).compose(ZGate(), [0]), - VectorStateFn( - Statevector([0.23071786 - 0.69436148j, 0.4646314 - 0.49874749j], dims=(2,)) - ), - ), - ) - @unpack - def test_trotter_qrte_qdrift(self, initial_state, expected_state): - """Test for TrotterQRTE with QDrift.""" - operator = SummedOp([X, Z]) - time = 1 - with warnings.catch_warnings(): - warnings.filterwarnings("ignore", category=DeprecationWarning) - algorithm_globals.random_seed = 0 - with self.assertWarns(DeprecationWarning): - evolution_problem = EvolutionProblem(operator, time, initial_state) - trotter_qrte = TrotterQRTE(product_formula=QDrift(seed=0)) - evolution_result = trotter_qrte.evolve(evolution_problem) - np.testing.assert_equal(evolution_result.evolved_state.eval(), expected_state) - - @data((Parameter("t"), {}), (None, {Parameter("x"): 2}), (None, None)) - @unpack - def test_trotter_qrte_trotter_errors(self, t_param, param_value_dict): - """Test TrotterQRTE with raising errors.""" - operator = X * Parameter("t") + Z - initial_state = Zero - time = 1 - with warnings.catch_warnings(): - warnings.filterwarnings("ignore", category=DeprecationWarning) - algorithm_globals.random_seed = 0 - with self.assertWarns(DeprecationWarning): - trotter_qrte = TrotterQRTE() - evolution_problem = EvolutionProblem( - operator, - time, - initial_state, - t_param=t_param, - param_value_dict=param_value_dict, - ) - with assert_raises(ValueError): - _ = trotter_qrte.evolve(evolution_problem) - - -if __name__ == "__main__": - unittest.main() diff --git a/test/python/algorithms/gradients/__init__.py b/test/python/algorithms/gradients/__init__.py deleted file mode 100644 index 756b2a26196b..000000000000 --- a/test/python/algorithms/gradients/__init__.py +++ /dev/null @@ -1,13 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2022. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. - -"""Tests for the primitive-based gradients""" diff --git a/test/python/algorithms/gradients/logging_primitives.py b/test/python/algorithms/gradients/logging_primitives.py deleted file mode 100644 index ef2cad9e2cc0..000000000000 --- a/test/python/algorithms/gradients/logging_primitives.py +++ /dev/null @@ -1,42 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2022. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. - -"""Test primitives that check what kind of operations are in the circuits they execute.""" - -from qiskit.primitives import Estimator, Sampler - - -class LoggingEstimator(Estimator): - """An estimator checking what operations were in the circuits it executed.""" - - def __init__(self, options=None, operations_callback=None): - super().__init__(options=options) - self.operations_callback = operations_callback - - def _run(self, circuits, observables, parameter_values, **run_options): - if self.operations_callback is not None: - ops = [circuit.count_ops() for circuit in circuits] - self.operations_callback(ops) - return super()._run(circuits, observables, parameter_values, **run_options) - - -class LoggingSampler(Sampler): - """A sampler checking what operations were in the circuits it executed.""" - - def __init__(self, operations_callback): - super().__init__() - self.operations_callback = operations_callback - - def _run(self, circuits, parameter_values, **run_options): - ops = [circuit.count_ops() for circuit in circuits] - self.operations_callback(ops) - return super()._run(circuits, parameter_values, **run_options) diff --git a/test/python/algorithms/gradients/test_estimator_gradient.py b/test/python/algorithms/gradients/test_estimator_gradient.py deleted file mode 100644 index 202727c5835a..000000000000 --- a/test/python/algorithms/gradients/test_estimator_gradient.py +++ /dev/null @@ -1,519 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2019, 2021. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. -# ============================================================================= - -"""Test Estimator Gradients""" - -import unittest - -import numpy as np -from ddt import ddt, data, unpack - -from qiskit import QuantumCircuit -from qiskit.algorithms.gradients import ( - FiniteDiffEstimatorGradient, - LinCombEstimatorGradient, - ParamShiftEstimatorGradient, - SPSAEstimatorGradient, - ReverseEstimatorGradient, - DerivativeType, -) -from qiskit.circuit import Parameter -from qiskit.circuit.library import EfficientSU2, RealAmplitudes -from qiskit.circuit.library.standard_gates import RXXGate, RYYGate, RZXGate, RZZGate -from qiskit.primitives import Estimator -from qiskit.quantum_info import Operator, SparsePauliOp, Pauli -from qiskit.quantum_info.random import random_pauli_list -from qiskit.test import QiskitTestCase - -from .logging_primitives import LoggingEstimator - -gradient_factories = [ - lambda estimator: FiniteDiffEstimatorGradient(estimator, epsilon=1e-6, method="central"), - lambda estimator: FiniteDiffEstimatorGradient(estimator, epsilon=1e-6, method="forward"), - lambda estimator: FiniteDiffEstimatorGradient(estimator, epsilon=1e-6, method="backward"), - ParamShiftEstimatorGradient, - LinCombEstimatorGradient, - lambda estimator: ReverseEstimatorGradient(), # does not take an estimator! -] - - -@ddt -class TestEstimatorGradient(QiskitTestCase): - """Test Estimator Gradient""" - - @data(*gradient_factories) - def test_gradient_operators(self, grad): - """Test the estimator gradient for different operators""" - estimator = Estimator() - a = Parameter("a") - qc = QuantumCircuit(1) - qc.h(0) - qc.p(a, 0) - qc.h(0) - gradient = grad(estimator) - op = SparsePauliOp.from_list([("Z", 1)]) - correct_result = -1 / np.sqrt(2) - param = [np.pi / 4] - value = gradient.run([qc], [op], [param]).result().gradients[0] - self.assertAlmostEqual(value[0], correct_result, 3) - op = SparsePauliOp.from_list([("Z", 1)]) - value = gradient.run([qc], [op], [param]).result().gradients[0] - self.assertAlmostEqual(value[0], correct_result, 3) - op = Operator.from_label("Z") - value = gradient.run([qc], [op], [param]).result().gradients[0] - self.assertAlmostEqual(value[0], correct_result, 3) - - @data(*gradient_factories) - def test_single_circuit_observable(self, grad): - """Test the estimator gradient for a single circuit and observable""" - estimator = Estimator() - a = Parameter("a") - qc = QuantumCircuit(1) - qc.h(0) - qc.p(a, 0) - qc.h(0) - gradient = grad(estimator) - op = SparsePauliOp.from_list([("Z", 1)]) - correct_result = -1 / np.sqrt(2) - param = [np.pi / 4] - value = gradient.run(qc, op, [param]).result().gradients[0] - self.assertAlmostEqual(value[0], correct_result, 3) - - @data(*gradient_factories) - def test_gradient_p(self, grad): - """Test the estimator gradient for p""" - estimator = Estimator() - a = Parameter("a") - qc = QuantumCircuit(1) - qc.h(0) - qc.p(a, 0) - qc.h(0) - gradient = grad(estimator) - op = SparsePauliOp.from_list([("Z", 1)]) - param_list = [[np.pi / 4], [0], [np.pi / 2]] - correct_results = [[-1 / np.sqrt(2)], [0], [-1]] - for i, param in enumerate(param_list): - gradients = gradient.run([qc], [op], [param]).result().gradients[0] - for j, value in enumerate(gradients): - self.assertAlmostEqual(value, correct_results[i][j], 3) - - @data(*gradient_factories) - def test_gradient_u(self, grad): - """Test the estimator gradient for u""" - estimator = Estimator() - a = Parameter("a") - b = Parameter("b") - c = Parameter("c") - qc = QuantumCircuit(1) - qc.h(0) - qc.u(a, b, c, 0) - qc.h(0) - gradient = grad(estimator) - op = SparsePauliOp.from_list([("Z", 1)]) - - param_list = [[np.pi / 4, 0, 0], [np.pi / 4, np.pi / 4, np.pi / 4]] - correct_results = [[-0.70710678, 0.0, 0.0], [-0.35355339, -0.85355339, -0.85355339]] - for i, param in enumerate(param_list): - gradients = gradient.run([qc], [op], [param]).result().gradients[0] - for j, value in enumerate(gradients): - self.assertAlmostEqual(value, correct_results[i][j], 3) - - @data(*gradient_factories) - def test_gradient_efficient_su2(self, grad): - """Test the estimator gradient for EfficientSU2""" - estimator = Estimator() - qc = EfficientSU2(2, reps=1) - op = SparsePauliOp.from_list([("ZI", 1)]) - gradient = grad(estimator) - param_list = [ - [np.pi / 4 for param in qc.parameters], - [np.pi / 2 for param in qc.parameters], - ] - correct_results = [ - [ - -0.35355339, - -0.70710678, - 0, - 0.35355339, - 0, - -0.70710678, - 0, - 0, - ], - [0, 0, 0, 1, 0, 0, 0, 0], - ] - for i, param in enumerate(param_list): - gradients = gradient.run([qc], [op], [param]).result().gradients[0] - np.testing.assert_allclose(gradients, correct_results[i], atol=1e-3) - - @data(*gradient_factories) - def test_gradient_2qubit_gate(self, grad): - """Test the estimator gradient for 2 qubit gates""" - estimator = Estimator() - for gate in [RXXGate, RYYGate, RZZGate, RZXGate]: - param_list = [[np.pi / 4], [np.pi / 2]] - correct_results = [ - [-0.70710678], - [-1], - ] - op = SparsePauliOp.from_list([("ZI", 1)]) - for i, param in enumerate(param_list): - a = Parameter("a") - qc = QuantumCircuit(2) - gradient = grad(estimator) - - if gate is RZZGate: - qc.h([0, 1]) - qc.append(gate(a), [qc.qubits[0], qc.qubits[1]], []) - qc.h([0, 1]) - else: - qc.append(gate(a), [qc.qubits[0], qc.qubits[1]], []) - gradients = gradient.run([qc], [op], [param]).result().gradients[0] - np.testing.assert_allclose(gradients, correct_results[i], atol=1e-3) - - @data(*gradient_factories) - def test_gradient_parameter_coefficient(self, grad): - """Test the estimator gradient for parameter variables with coefficients""" - estimator = Estimator() - qc = RealAmplitudes(num_qubits=2, reps=1) - qc.rz(qc.parameters[0].exp() + 2 * qc.parameters[1], 0) - qc.rx(3.0 * qc.parameters[0] + qc.parameters[1].sin(), 1) - qc.u(qc.parameters[0], qc.parameters[1], qc.parameters[3], 1) - qc.p(2 * qc.parameters[0] + 1, 0) - qc.rxx(qc.parameters[0] + 2, 0, 1) - gradient = grad(estimator) - param_list = [[np.pi / 4 for _ in qc.parameters], [np.pi / 2 for _ in qc.parameters]] - correct_results = [ - [-0.7266653, -0.4905135, -0.0068606, -0.9228880], - [-3.5972095, 0.10237173, -0.3117748, 0], - ] - op = SparsePauliOp.from_list([("ZI", 1)]) - for i, param in enumerate(param_list): - gradients = gradient.run([qc], [op], [param]).result().gradients[0] - np.testing.assert_allclose(gradients, correct_results[i], atol=1e-3) - - @data(*gradient_factories) - def test_gradient_parameters(self, grad): - """Test the estimator gradient for parameters""" - estimator = Estimator() - a = Parameter("a") - b = Parameter("b") - qc = QuantumCircuit(1) - qc.rx(a, 0) - qc.rx(b, 0) - gradient = grad(estimator) - param_list = [[np.pi / 4, np.pi / 2]] - correct_results = [ - [-0.70710678], - ] - op = SparsePauliOp.from_list([("Z", 1)]) - for i, param in enumerate(param_list): - gradients = gradient.run([qc], [op], [param], parameters=[[a]]).result().gradients[0] - np.testing.assert_allclose(gradients, correct_results[i], atol=1e-3) - - # parameter order - with self.subTest(msg="The order of gradients"): - c = Parameter("c") - qc = QuantumCircuit(1) - qc.rx(a, 0) - qc.rz(b, 0) - qc.rx(c, 0) - - param_list = [[np.pi / 4, np.pi / 2, np.pi / 3]] - correct_results = [ - [-0.35355339, 0.61237244, -0.61237244], - [-0.61237244, 0.61237244, -0.35355339], - [-0.35355339, -0.61237244], - [-0.61237244, -0.35355339], - ] - param = [[a, b, c], [c, b, a], [a, c], [c, a]] - op = SparsePauliOp.from_list([("Z", 1)]) - for i, p in enumerate(param): - gradient = grad(estimator) - gradients = ( - gradient.run([qc], [op], param_list, parameters=[p]).result().gradients[0] - ) - np.testing.assert_allclose(gradients, correct_results[i], atol=1e-3) - - @data(*gradient_factories) - def test_gradient_multi_arguments(self, grad): - """Test the estimator gradient for multiple arguments""" - estimator = Estimator() - a = Parameter("a") - b = Parameter("b") - qc = QuantumCircuit(1) - qc.rx(a, 0) - qc2 = QuantumCircuit(1) - qc2.rx(b, 0) - gradient = grad(estimator) - param_list = [[np.pi / 4], [np.pi / 2]] - correct_results = [ - [-0.70710678], - [-1], - ] - op = SparsePauliOp.from_list([("Z", 1)]) - gradients = gradient.run([qc, qc2], [op] * 2, param_list).result().gradients - np.testing.assert_allclose(gradients, correct_results, atol=1e-3) - - c = Parameter("c") - qc3 = QuantumCircuit(1) - qc3.rx(c, 0) - qc3.ry(a, 0) - param_list2 = [[np.pi / 4], [np.pi / 4, np.pi / 4], [np.pi / 4, np.pi / 4]] - correct_results2 = [ - [-0.70710678], - [-0.5], - [-0.5, -0.5], - ] - gradients2 = ( - gradient.run([qc, qc3, qc3], [op] * 3, param_list2, parameters=[[a], [c], None]) - .result() - .gradients - ) - np.testing.assert_allclose(gradients2[0], correct_results2[0], atol=1e-3) - np.testing.assert_allclose(gradients2[1], correct_results2[1], atol=1e-3) - np.testing.assert_allclose(gradients2[2], correct_results2[2], atol=1e-3) - - @data(*gradient_factories) - def test_gradient_validation(self, grad): - """Test estimator gradient's validation""" - estimator = Estimator() - a = Parameter("a") - qc = QuantumCircuit(1) - qc.rx(a, 0) - gradient = grad(estimator) - param_list = [[np.pi / 4], [np.pi / 2]] - op = SparsePauliOp.from_list([("Z", 1)]) - with self.assertRaises(ValueError): - gradient.run([qc], [op], param_list) - with self.assertRaises(ValueError): - gradient.run([qc, qc], [op, op], param_list, parameters=[[a]]) - with self.assertRaises(ValueError): - gradient.run([qc, qc], [op], param_list, parameters=[[a]]) - with self.assertRaises(ValueError): - gradient.run([qc], [op], [[np.pi / 4, np.pi / 4]]) - - def test_spsa_gradient(self): - """Test the SPSA estimator gradient""" - estimator = Estimator() - with self.assertRaises(ValueError): - _ = SPSAEstimatorGradient(estimator, epsilon=-0.1) - a = Parameter("a") - b = Parameter("b") - qc = QuantumCircuit(2) - qc.rx(b, 0) - qc.rx(a, 1) - param_list = [[1, 1]] - correct_results = [[-0.84147098, 0.84147098]] - op = SparsePauliOp.from_list([("ZI", 1)]) - gradient = SPSAEstimatorGradient(estimator, epsilon=1e-6, seed=123) - gradients = gradient.run([qc], [op], param_list).result().gradients - np.testing.assert_allclose(gradients, correct_results, atol=1e-3) - - # multi parameters - with self.subTest(msg="Multiple parameters"): - gradient = SPSAEstimatorGradient(estimator, epsilon=1e-6, seed=123) - param_list2 = [[1, 1], [1, 1], [3, 3]] - gradients2 = ( - gradient.run([qc] * 3, [op] * 3, param_list2, parameters=[None, [b], None]) - .result() - .gradients - ) - correct_results2 = [[-0.84147098, 0.84147098], [0.84147098], [-0.14112001, 0.14112001]] - for grad, correct in zip(gradients2, correct_results2): - np.testing.assert_allclose(grad, correct, atol=1e-3) - - # batch size - with self.subTest(msg="Batch size"): - correct_results = [[-0.84147098, 0.1682942]] - gradient = SPSAEstimatorGradient(estimator, epsilon=1e-6, batch_size=5, seed=123) - gradients = gradient.run([qc], [op], param_list).result().gradients - np.testing.assert_allclose(gradients, correct_results, atol=1e-3) - - # parameter order - with self.subTest(msg="The order of gradients"): - gradient = SPSAEstimatorGradient(estimator, epsilon=1e-6, seed=123) - c = Parameter("c") - qc = QuantumCircuit(1) - qc.rx(a, 0) - qc.rz(b, 0) - qc.rx(c, 0) - op = SparsePauliOp.from_list([("Z", 1)]) - param_list3 = [[np.pi / 4, np.pi / 2, np.pi / 3]] - param = [[a, b, c], [c, b, a], [a, c], [c, a]] - expected = [ - [-0.3535525, 0.3535525, 0.3535525], - [0.3535525, 0.3535525, -0.3535525], - [-0.3535525, 0.3535525], - [0.3535525, -0.3535525], - ] - for i, p in enumerate(param): - gradient = SPSAEstimatorGradient(estimator, epsilon=1e-6, seed=123) - gradients = ( - gradient.run([qc], [op], param_list3, parameters=[p]).result().gradients[0] - ) - np.testing.assert_allclose(gradients, expected[i], atol=1e-3) - - @data(ParamShiftEstimatorGradient, LinCombEstimatorGradient) - def test_gradient_random_parameters(self, grad): - """Test param shift and lin comb w/ random parameters""" - rng = np.random.default_rng(123) - qc = RealAmplitudes(num_qubits=3, reps=1) - params = qc.parameters - qc.rx(3.0 * params[0] + params[1].sin(), 0) - qc.ry(params[0].exp() + 2 * params[1], 1) - qc.rz(params[0] * params[1] - params[2], 2) - qc.p(2 * params[0] + 1, 0) - qc.u(params[0].sin(), params[1] - 2, params[2] * params[3], 1) - qc.sx(2) - qc.rxx(params[0].sin(), 1, 2) - qc.ryy(params[1].cos(), 2, 0) - qc.rzz(params[2] * 2, 0, 1) - qc.crx(params[0].exp(), 1, 2) - qc.cry(params[1].arctan(), 2, 0) - qc.crz(params[2] * -2, 0, 1) - qc.dcx(0, 1) - qc.csdg(0, 1) - qc.ccx(0, 1, 2) - qc.iswap(0, 2) - qc.swap(1, 2) - qc.global_phase = params[0] * params[1] + params[2].cos().exp() - - size = 10 - op = SparsePauliOp(random_pauli_list(num_qubits=qc.num_qubits, size=size, seed=rng)) - op.coeffs = rng.normal(0, 10, size) - - estimator = Estimator() - findiff = FiniteDiffEstimatorGradient(estimator, 1e-6) - gradient = grad(estimator) - - num_tries = 10 - param_values = rng.normal(0, 2, (num_tries, qc.num_parameters)).tolist() - np.testing.assert_allclose( - findiff.run([qc] * num_tries, [op] * num_tries, param_values).result().gradients, - gradient.run([qc] * num_tries, [op] * num_tries, param_values).result().gradients, - rtol=1e-4, - ) - - @data((DerivativeType.IMAG, -1.0), (DerivativeType.COMPLEX, -1.0j)) - @unpack - def test_complex_gradient(self, derivative_type, expected_gradient_value): - """Tests if the ``LinCombEstimatorGradient`` has the correct value.""" - estimator = Estimator() - lcu = LinCombEstimatorGradient(estimator, derivative_type=derivative_type) - reverse = ReverseEstimatorGradient(derivative_type=derivative_type) - - for gradient in [lcu, reverse]: - with self.subTest(gradient=gradient): - c = QuantumCircuit(1) - c.rz(Parameter("p"), 0) - result = gradient.run([c], [Pauli("I")], [[0.0]]).result() - self.assertAlmostEqual(result.gradients[0][0], expected_gradient_value) - - @data( - FiniteDiffEstimatorGradient, - ParamShiftEstimatorGradient, - LinCombEstimatorGradient, - SPSAEstimatorGradient, - ) - def test_options(self, grad): - """Test estimator gradient's run options""" - a = Parameter("a") - qc = QuantumCircuit(1) - qc.rx(a, 0) - op = SparsePauliOp.from_list([("Z", 1)]) - estimator = Estimator(options={"shots": 100}) - with self.subTest("estimator"): - if grad is FiniteDiffEstimatorGradient or grad is SPSAEstimatorGradient: - gradient = grad(estimator, epsilon=1e-6) - else: - gradient = grad(estimator) - options = gradient.options - result = gradient.run([qc], [op], [[1]]).result() - self.assertEqual(result.options.get("shots"), 100) - self.assertEqual(options.get("shots"), 100) - - with self.subTest("gradient init"): - if grad is FiniteDiffEstimatorGradient or grad is SPSAEstimatorGradient: - gradient = grad(estimator, epsilon=1e-6, options={"shots": 200}) - else: - gradient = grad(estimator, options={"shots": 200}) - options = gradient.options - result = gradient.run([qc], [op], [[1]]).result() - self.assertEqual(result.options.get("shots"), 200) - self.assertEqual(options.get("shots"), 200) - - with self.subTest("gradient update"): - if grad is FiniteDiffEstimatorGradient or grad is SPSAEstimatorGradient: - gradient = grad(estimator, epsilon=1e-6, options={"shots": 200}) - else: - gradient = grad(estimator, options={"shots": 200}) - gradient.update_default_options(shots=100) - options = gradient.options - result = gradient.run([qc], [op], [[1]]).result() - self.assertEqual(result.options.get("shots"), 100) - self.assertEqual(options.get("shots"), 100) - - with self.subTest("gradient run"): - if grad is FiniteDiffEstimatorGradient or grad is SPSAEstimatorGradient: - gradient = grad(estimator, epsilon=1e-6, options={"shots": 200}) - else: - gradient = grad(estimator, options={"shots": 200}) - options = gradient.options - result = gradient.run([qc], [op], [[1]], shots=300).result() - self.assertEqual(result.options.get("shots"), 300) - # Only default + estimator options. Not run. - self.assertEqual(options.get("shots"), 200) - - @data( - FiniteDiffEstimatorGradient, - ParamShiftEstimatorGradient, - LinCombEstimatorGradient, - SPSAEstimatorGradient, - ) - def test_operations_preserved(self, gradient_cls): - """Test non-parameterized instructions are preserved and not unrolled.""" - x = Parameter("x") - circuit = QuantumCircuit(2) - circuit.initialize([0.5, 0.5, 0.5, 0.5]) # this should remain as initialize - circuit.crx(x, 0, 1) # this should get unrolled - - values = [np.pi / 2] - expect = -1 / (2 * np.sqrt(2)) - - observable = SparsePauliOp(["XX"]) - - ops = [] - - def operations_callback(op): - ops.append(op) - - estimator = LoggingEstimator(operations_callback=operations_callback) - - if gradient_cls in [SPSAEstimatorGradient, FiniteDiffEstimatorGradient]: - gradient = gradient_cls(estimator, epsilon=0.01) - else: - gradient = gradient_cls(estimator) - - job = gradient.run([circuit], [observable], [values]) - result = job.result() - - with self.subTest(msg="assert initialize is preserved"): - self.assertTrue(all("initialize" in ops_i[0].keys() for ops_i in ops)) - - with self.subTest(msg="assert result is correct"): - self.assertAlmostEqual(result.gradients[0].item(), expect, places=5) - - -if __name__ == "__main__": - unittest.main() diff --git a/test/python/algorithms/gradients/test_qfi.py b/test/python/algorithms/gradients/test_qfi.py deleted file mode 100644 index b0d05ac7030f..000000000000 --- a/test/python/algorithms/gradients/test_qfi.py +++ /dev/null @@ -1,151 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2022, 2023. -# -# 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 QFI.""" - -import unittest -from ddt import ddt, data - -import numpy as np - -from qiskit import QuantumCircuit -from qiskit.algorithms.gradients import LinCombQGT, ReverseQGT, QFI, DerivativeType -from qiskit.circuit import Parameter -from qiskit.circuit.parametervector import ParameterVector -from qiskit.primitives import Estimator -from qiskit.test import QiskitTestCase - - -@ddt -class TestQFI(QiskitTestCase): - """Test QFI""" - - def setUp(self): - super().setUp() - self.estimator = Estimator() - self.lcu_qgt = LinCombQGT(self.estimator, derivative_type=DerivativeType.REAL) - self.reverse_qgt = ReverseQGT(derivative_type=DerivativeType.REAL) - - def test_qfi(self): - """Test if the quantum fisher information calculation is correct for a simple test case. - QFI = [[1, 0], [0, 1]] - [[0, 0], [0, cos^2(a)]] - """ - # create the circuit - a, b = Parameter("a"), Parameter("b") - qc = QuantumCircuit(1) - qc.h(0) - qc.rz(a, 0) - qc.rx(b, 0) - - param_list = [[np.pi / 4, 0.1], [np.pi, 0.1], [np.pi / 2, 0.1]] - correct_values = [[[1, 0], [0, 0.5]], [[1, 0], [0, 0]], [[1, 0], [0, 1]]] - - qfi = QFI(self.lcu_qgt) - for i, param in enumerate(param_list): - qfis = qfi.run([qc], [param]).result().qfis - np.testing.assert_allclose(qfis[0], correct_values[i], atol=1e-3) - - def test_qfi_phase_fix(self): - """Test the phase-fix argument in the QFI calculation""" - # create the circuit - a, b = Parameter("a"), Parameter("b") - qc = QuantumCircuit(1) - qc.h(0) - qc.rz(a, 0) - qc.rx(b, 0) - - param = [np.pi / 4, 0.1] - # test for different values - correct_values = [[1, 0], [0, 1]] - qgt = LinCombQGT(self.estimator, phase_fix=False) - qfi = QFI(qgt) - qfis = qfi.run([qc], [param]).result().qfis - np.testing.assert_allclose(qfis[0], correct_values, atol=1e-3) - - @data("lcu", "reverse") - def test_qfi_maxcut(self, qgt_kind): - """Test the QFI for a simple MaxCut problem. - - This is interesting because it contains the same parameters in different gates. - """ - # create maxcut circuit for the hamiltonian - # H = (I ^ I ^ Z ^ Z) + (I ^ Z ^ I ^ Z) + (Z ^ I ^ I ^ Z) + (I ^ Z ^ Z ^ I) - - x = ParameterVector("x", 2) - ansatz = QuantumCircuit(4) - - # initial hadamard layer - ansatz.h(ansatz.qubits) - - # e^{iZZ} layers - def expiz(qubit0, qubit1): - ansatz.cx(qubit0, qubit1) - ansatz.rz(2 * x[0], qubit1) - ansatz.cx(qubit0, qubit1) - - expiz(2, 1) - expiz(3, 0) - expiz(2, 0) - expiz(1, 0) - - # mixer layer with RX gates - for i in range(ansatz.num_qubits): - ansatz.rx(2 * x[1], i) - - reference = np.array([[16.0, -5.551], [-5.551, 18.497]]) - param = [0.4, 0.69] - - qgt = self.lcu_qgt if qgt_kind == "lcu" else self.reverse_qgt - qfi = QFI(qgt) - qfi_result = qfi.run([ansatz], [param]).result().qfis - np.testing.assert_array_almost_equal(qfi_result[0], reference, decimal=3) - - def test_options(self): - """Test QFI's options""" - a = Parameter("a") - qc = QuantumCircuit(1) - qc.rx(a, 0) - qgt = LinCombQGT(estimator=self.estimator, options={"shots": 100}) - - with self.subTest("QGT"): - qfi = QFI(qgt=qgt) - options = qfi.options - result = qfi.run([qc], [[1]]).result() - self.assertEqual(result.options.get("shots"), 100) - self.assertEqual(options.get("shots"), 100) - - with self.subTest("QFI init"): - qfi = QFI(qgt=qgt, options={"shots": 200}) - result = qfi.run([qc], [[1]]).result() - options = qfi.options - self.assertEqual(result.options.get("shots"), 200) - self.assertEqual(options.get("shots"), 200) - - with self.subTest("QFI update"): - qfi = QFI(qgt, options={"shots": 200}) - qfi.update_default_options(shots=100) - options = qfi.options - result = qfi.run([qc], [[1]]).result() - self.assertEqual(result.options.get("shots"), 100) - self.assertEqual(options.get("shots"), 100) - - with self.subTest("QFI run"): - qfi = QFI(qgt=qgt, options={"shots": 200}) - result = qfi.run([qc], [[0]], shots=300).result() - options = qfi.options - self.assertEqual(result.options.get("shots"), 300) - self.assertEqual(options.get("shots"), 200) - - -if __name__ == "__main__": - unittest.main() diff --git a/test/python/algorithms/gradients/test_qgt.py b/test/python/algorithms/gradients/test_qgt.py deleted file mode 100644 index 74f22d462d7c..000000000000 --- a/test/python/algorithms/gradients/test_qgt.py +++ /dev/null @@ -1,309 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2022, 2023. -# -# 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 QGT.""" - -import unittest -from ddt import ddt, data - -import numpy as np - -from qiskit import QuantumCircuit -from qiskit.algorithms.gradients import DerivativeType, LinCombQGT, ReverseQGT -from qiskit.circuit import Parameter -from qiskit.circuit.library import RealAmplitudes -from qiskit.primitives import Estimator -from qiskit.test import QiskitTestCase - -from .logging_primitives import LoggingEstimator - - -@ddt -class TestQGT(QiskitTestCase): - """Test QGT""" - - def setUp(self): - super().setUp() - self.estimator = Estimator() - - @data(LinCombQGT, ReverseQGT) - def test_qgt_derivative_type(self, qgt_type): - """Test QGT derivative_type""" - args = () if qgt_type == ReverseQGT else (self.estimator,) - qgt = qgt_type(*args, derivative_type=DerivativeType.REAL) - - a, b = Parameter("a"), Parameter("b") - qc = QuantumCircuit(1) - qc.h(0) - qc.rz(a, 0) - qc.rx(b, 0) - - param_list = [[np.pi / 4, 0], [np.pi / 2, np.pi / 4]] - correct_values = [ - np.array([[1, 0.707106781j], [-0.707106781j, 0.5]]) / 4, - np.array([[1, 1j], [-1j, 1]]) / 4, - ] - - # test real derivative - with self.subTest("Test with DerivativeType.REAL"): - qgt.derivative_type = DerivativeType.REAL - for i, param in enumerate(param_list): - qgt_result = qgt.run([qc], [param]).result().qgts - np.testing.assert_allclose(qgt_result[0], correct_values[i].real, atol=1e-3) - - # test imaginary derivative - with self.subTest("Test with DerivativeType.IMAG"): - qgt.derivative_type = DerivativeType.IMAG - for i, param in enumerate(param_list): - qgt_result = qgt.run([qc], [param]).result().qgts - np.testing.assert_allclose(qgt_result[0], correct_values[i].imag, atol=1e-3) - - # test real + imaginary derivative - with self.subTest("Test with DerivativeType.COMPLEX"): - qgt.derivative_type = DerivativeType.COMPLEX - for i, param in enumerate(param_list): - qgt_result = qgt.run([qc], [param]).result().qgts - np.testing.assert_allclose(qgt_result[0], correct_values[i], atol=1e-3) - - @data(LinCombQGT, ReverseQGT) - def test_qgt_phase_fix(self, qgt_type): - """Test the phase-fix argument in a QGT calculation""" - args = () if qgt_type == ReverseQGT else (self.estimator,) - qgt = qgt_type(*args, phase_fix=False) - - # create the circuit - a, b = Parameter("a"), Parameter("b") - qc = QuantumCircuit(1) - qc.h(0) - qc.rz(a, 0) - qc.rx(b, 0) - - param_list = [[np.pi / 4, 0], [np.pi / 2, np.pi / 4]] - correct_values = [ - np.array([[1, 0.707106781j], [-0.707106781j, 1]]) / 4, - np.array([[1, 1j], [-1j, 1]]) / 4, - ] - - # test real derivative - with self.subTest("Test phase fix with DerivativeType.REAL"): - qgt.derivative_type = DerivativeType.REAL - for i, param in enumerate(param_list): - qgt_result = qgt.run([qc], [param]).result().qgts - np.testing.assert_allclose(qgt_result[0], correct_values[i].real, atol=1e-3) - - # test imaginary derivative - with self.subTest("Test phase fix with DerivativeType.IMAG"): - qgt.derivative_type = DerivativeType.IMAG - for i, param in enumerate(param_list): - qgt_result = qgt.run([qc], [param]).result().qgts - np.testing.assert_allclose(qgt_result[0], correct_values[i].imag, atol=1e-3) - - # test real + imaginary derivative - with self.subTest("Test phase fix with DerivativeType.COMPLEX"): - qgt.derivative_type = DerivativeType.COMPLEX - for i, param in enumerate(param_list): - qgt_result = qgt.run([qc], [param]).result().qgts - np.testing.assert_allclose(qgt_result[0], correct_values[i], atol=1e-3) - - @data(LinCombQGT, ReverseQGT) - def test_qgt_coefficients(self, qgt_type): - """Test the derivative option of QGT""" - args = () if qgt_type == ReverseQGT else (self.estimator,) - qgt = qgt_type(*args, derivative_type=DerivativeType.REAL) - - qc = RealAmplitudes(num_qubits=2, reps=1) - qc.rz(qc.parameters[0].exp() + 2 * qc.parameters[1], 0) - qc.rx(3.0 * qc.parameters[2] + qc.parameters[3].sin(), 1) - - # test imaginary derivative - param_list = [ - [np.pi / 4 for param in qc.parameters], - [np.pi / 2 for param in qc.parameters], - ] - correct_values = ( - np.array( - [ - [ - [5.707309, 4.2924833, 1.5295868, 0.1938604], - [4.2924833, 4.9142136, 0.75, 0.8838835], - [1.5295868, 0.75, 3.4430195, 0.0758252], - [0.1938604, 0.8838835, 0.0758252, 1.1357233], - ], - [ - [1.0, 0.0, 1.0, 0.0], - [0.0, 1.0, 0.0, 0.0], - [1.0, 0.0, 10.0, -0.0], - [0.0, 0.0, -0.0, 1.0], - ], - ] - ) - / 4 - ) - for i, param in enumerate(param_list): - qgt_result = qgt.run([qc], [param]).result().qgts - np.testing.assert_allclose(qgt_result[0], correct_values[i], atol=1e-3) - - @data(LinCombQGT, ReverseQGT) - def test_qgt_parameters(self, qgt_type): - """Test the QGT with specified parameters""" - args = () if qgt_type == ReverseQGT else (self.estimator,) - qgt = qgt_type(*args, derivative_type=DerivativeType.REAL) - - a = Parameter("a") - b = Parameter("b") - qc = QuantumCircuit(1) - qc.rx(a, 0) - qc.ry(b, 0) - param_values = [np.pi / 4, np.pi / 4] - qgt_result = qgt.run([qc], [param_values], [[a]]).result().qgts - np.testing.assert_allclose(qgt_result[0], [[1 / 4]], atol=1e-3) - - with self.subTest("Test with different parameter orders"): - c = Parameter("c") - qc2 = QuantumCircuit(1) - qc2.rx(a, 0) - qc2.rz(b, 0) - qc2.rx(c, 0) - param_values = [np.pi / 4, np.pi / 4, np.pi / 4] - params = [[a, b, c], [c, b, a], [a, c], [b, a]] - expected = [ - np.array( - [ - [0.25, 0.0, 0.1767767], - [0.0, 0.125, -0.08838835], - [0.1767767, -0.08838835, 0.1875], - ] - ), - np.array( - [ - [0.1875, -0.08838835, 0.1767767], - [-0.08838835, 0.125, 0.0], - [0.1767767, 0.0, 0.25], - ] - ), - np.array([[0.25, 0.1767767], [0.1767767, 0.1875]]), - np.array([[0.125, 0.0], [0.0, 0.25]]), - ] - for i, param in enumerate(params): - qgt_result = qgt.run([qc2], [param_values], [param]).result().qgts - np.testing.assert_allclose(qgt_result[0], expected[i], atol=1e-3) - - @data(LinCombQGT, ReverseQGT) - def test_qgt_multi_arguments(self, qgt_type): - """Test the QGT for multiple arguments""" - args = () if qgt_type == ReverseQGT else (self.estimator,) - qgt = qgt_type(*args, derivative_type=DerivativeType.REAL) - - a = Parameter("a") - b = Parameter("b") - qc = QuantumCircuit(1) - qc.rx(a, 0) - qc.ry(b, 0) - qc2 = QuantumCircuit(1) - qc2.rx(a, 0) - qc2.ry(b, 0) - - param_list = [[np.pi / 4], [np.pi / 2]] - correct_values = [[[1 / 4]], [[1 / 4, 0], [0, 0]]] - param_list = [[np.pi / 4, np.pi / 4], [np.pi / 2, np.pi / 2]] - qgt_results = qgt.run([qc, qc2], param_list, [[a], None]).result().qgts - for i, _ in enumerate(param_list): - np.testing.assert_allclose(qgt_results[i], correct_values[i], atol=1e-3) - - @data(LinCombQGT, ReverseQGT) - def test_qgt_validation(self, qgt_type): - """Test estimator QGT's validation""" - args = () if qgt_type == ReverseQGT else (self.estimator,) - qgt = qgt_type(*args) - - a = Parameter("a") - qc = QuantumCircuit(1) - qc.rx(a, 0) - parameter_values = [[np.pi / 4]] - with self.subTest("assert number of circuits does not match"): - with self.assertRaises(ValueError): - qgt.run([qc, qc], parameter_values) - with self.subTest("assert number of parameter values does not match"): - with self.assertRaises(ValueError): - qgt.run([qc], [[np.pi / 4], [np.pi / 2]]) - with self.subTest("assert number of parameters does not match"): - with self.assertRaises(ValueError): - qgt.run([qc], parameter_values, parameters=[[a], [a]]) - - def test_options(self): - """Test QGT's options""" - a = Parameter("a") - qc = QuantumCircuit(1) - qc.rx(a, 0) - estimator = Estimator(options={"shots": 100}) - - with self.subTest("estimator"): - qgt = LinCombQGT(estimator) - options = qgt.options - result = qgt.run([qc], [[1]]).result() - self.assertEqual(result.options.get("shots"), 100) - self.assertEqual(options.get("shots"), 100) - - with self.subTest("QGT init"): - qgt = LinCombQGT(estimator, options={"shots": 200}) - result = qgt.run([qc], [[1]]).result() - options = qgt.options - self.assertEqual(result.options.get("shots"), 200) - self.assertEqual(options.get("shots"), 200) - - with self.subTest("QGT update"): - qgt = LinCombQGT(estimator, options={"shots": 200}) - qgt.update_default_options(shots=100) - options = qgt.options - result = qgt.run([qc], [[1]]).result() - self.assertEqual(result.options.get("shots"), 100) - self.assertEqual(options.get("shots"), 100) - - with self.subTest("QGT run"): - qgt = LinCombQGT(estimator, options={"shots": 200}) - result = qgt.run([qc], [[0]], shots=300).result() - options = qgt.options - self.assertEqual(result.options.get("shots"), 300) - self.assertEqual(options.get("shots"), 200) - - def test_operations_preserved(self): - """Test non-parameterized instructions are preserved and not unrolled.""" - x, y = Parameter("x"), Parameter("y") - circuit = QuantumCircuit(2) - circuit.initialize([0.5, 0.5, 0.5, 0.5]) # this should remain as initialize - circuit.crx(x, 0, 1) # this should get unrolled - circuit.ry(y, 0) - - values = [np.pi / 2, np.pi] - expect = np.diag([0.25, 0.5]) / 4 - - ops = [] - - def operations_callback(op): - ops.append(op) - - estimator = LoggingEstimator(operations_callback=operations_callback) - qgt = LinCombQGT(estimator, derivative_type=DerivativeType.REAL) - - job = qgt.run([circuit], [values]) - result = job.result() - - with self.subTest(msg="assert initialize is preserved"): - self.assertTrue(all("initialize" in ops_i[0].keys() for ops_i in ops)) - - with self.subTest(msg="assert result is correct"): - np.testing.assert_allclose(result.qgts[0], expect, atol=1e-5) - - -if __name__ == "__main__": - unittest.main() diff --git a/test/python/algorithms/gradients/test_sampler_gradient.py b/test/python/algorithms/gradients/test_sampler_gradient.py deleted file mode 100644 index 2faf44df6466..000000000000 --- a/test/python/algorithms/gradients/test_sampler_gradient.py +++ /dev/null @@ -1,690 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2019, 2021. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. -# ============================================================================= - -"""Test Sampler Gradients""" - -import unittest -from typing import List - -import numpy as np -from ddt import ddt, data - -from qiskit import QuantumCircuit -from qiskit.algorithms.gradients import ( - FiniteDiffSamplerGradient, - LinCombSamplerGradient, - ParamShiftSamplerGradient, - SPSASamplerGradient, -) -from qiskit.circuit import Parameter -from qiskit.circuit.library import EfficientSU2, RealAmplitudes -from qiskit.circuit.library.standard_gates import RXXGate -from qiskit.primitives import Sampler -from qiskit.result import QuasiDistribution -from qiskit.test import QiskitTestCase - -from .logging_primitives import LoggingSampler - -gradient_factories = [ - lambda sampler: FiniteDiffSamplerGradient(sampler, epsilon=1e-6, method="central"), - lambda sampler: FiniteDiffSamplerGradient(sampler, epsilon=1e-6, method="forward"), - lambda sampler: FiniteDiffSamplerGradient(sampler, epsilon=1e-6, method="backward"), - ParamShiftSamplerGradient, - LinCombSamplerGradient, -] - - -@ddt -class TestSamplerGradient(QiskitTestCase): - """Test Sampler Gradient""" - - @data(*gradient_factories) - def test_single_circuit(self, grad): - """Test the sampler gradient for a single circuit""" - sampler = Sampler() - a = Parameter("a") - qc = QuantumCircuit(1) - qc.h(0) - qc.p(a, 0) - qc.h(0) - qc.measure_all() - gradient = grad(sampler) - param_list = [[np.pi / 4], [0], [np.pi / 2]] - expected = [ - [{0: -0.5 / np.sqrt(2), 1: 0.5 / np.sqrt(2)}], - [{0: 0, 1: 0}], - [{0: -0.499999, 1: 0.499999}], - ] - for i, param in enumerate(param_list): - gradients = gradient.run([qc], [param]).result().gradients[0] - array1 = _quasi2array(gradients, num_qubits=1) - array2 = _quasi2array(expected[i], num_qubits=1) - np.testing.assert_allclose(array1, array2, atol=1e-3) - - @data(*gradient_factories) - def test_gradient_p(self, grad): - """Test the sampler gradient for p""" - sampler = Sampler() - a = Parameter("a") - qc = QuantumCircuit(1) - qc.h(0) - qc.p(a, 0) - qc.h(0) - qc.measure_all() - gradient = grad(sampler) - param_list = [[np.pi / 4], [0], [np.pi / 2]] - expected = [ - [{0: -0.5 / np.sqrt(2), 1: 0.5 / np.sqrt(2)}], - [{0: 0, 1: 0}], - [{0: -0.499999, 1: 0.499999}], - ] - for i, param in enumerate(param_list): - gradients = gradient.run([qc], [param]).result().gradients[0] - array1 = _quasi2array(gradients, num_qubits=1) - array2 = _quasi2array(expected[i], num_qubits=1) - np.testing.assert_allclose(array1, array2, atol=1e-3) - - @data(*gradient_factories) - def test_gradient_u(self, grad): - """Test the sampler gradient for u""" - sampler = Sampler() - a = Parameter("a") - b = Parameter("b") - c = Parameter("c") - qc = QuantumCircuit(1) - qc.h(0) - qc.u(a, b, c, 0) - qc.h(0) - qc.measure_all() - gradient = grad(sampler) - param_list = [[np.pi / 4, 0, 0], [np.pi / 4, np.pi / 4, np.pi / 4]] - expected = [ - [{0: -0.5 / np.sqrt(2), 1: 0.5 / np.sqrt(2)}, {0: 0, 1: 0}, {0: 0, 1: 0}], - [{0: -0.176777, 1: 0.176777}, {0: -0.426777, 1: 0.426777}, {0: -0.426777, 1: 0.426777}], - ] - for i, param in enumerate(param_list): - gradients = gradient.run([qc], [param]).result().gradients[0] - array1 = _quasi2array(gradients, num_qubits=1) - array2 = _quasi2array(expected[i], num_qubits=1) - np.testing.assert_allclose(array1, array2, atol=1e-3) - - @data(*gradient_factories) - def test_gradient_efficient_su2(self, grad): - """Test the sampler gradient for EfficientSU2""" - sampler = Sampler() - qc = EfficientSU2(2, reps=1) - qc.measure_all() - gradient = grad(sampler) - param_list = [ - [np.pi / 4 for param in qc.parameters], - [np.pi / 2 for param in qc.parameters], - ] - expected = [ - [ - { - 0: -0.11963834764831836, - 1: -0.05713834764831845, - 2: -0.21875000000000003, - 3: 0.39552669529663675, - }, - { - 0: -0.32230339059327373, - 1: -0.031250000000000014, - 2: 0.2339150429449554, - 3: 0.11963834764831843, - }, - { - 0: 0.012944173824159189, - 1: -0.01294417382415923, - 2: 0.07544417382415919, - 3: -0.07544417382415919, - }, - { - 0: 0.2080266952966367, - 1: -0.03125000000000002, - 2: -0.11963834764831842, - 3: -0.057138347648318405, - }, - { - 0: -0.11963834764831838, - 1: 0.11963834764831838, - 2: -0.21875000000000003, - 3: 0.21875, - }, - { - 0: -0.2781092167691146, - 1: -0.0754441738241592, - 2: 0.27810921676911443, - 3: 0.07544417382415924, - }, - {0: 0.0, 1: 0.0, 2: 0.0, 3: 0.0}, - {0: 0.0, 1: 0.0, 2: 0.0, 3: 0.0}, - ], - [ - { - 0: -4.163336342344337e-17, - 1: 2.7755575615628914e-17, - 2: -4.163336342344337e-17, - 3: 0.0, - }, - {0: 0.0, 1: -1.3877787807814457e-17, 2: 4.163336342344337e-17, 3: 0.0}, - { - 0: -0.24999999999999994, - 1: 0.24999999999999994, - 2: 0.24999999999999994, - 3: -0.24999999999999994, - }, - { - 0: 0.24999999999999994, - 1: 0.24999999999999994, - 2: -0.24999999999999994, - 3: -0.24999999999999994, - }, - { - 0: -4.163336342344337e-17, - 1: 4.163336342344337e-17, - 2: -4.163336342344337e-17, - 3: 5.551115123125783e-17, - }, - { - 0: -0.24999999999999994, - 1: 0.24999999999999994, - 2: 0.24999999999999994, - 3: -0.24999999999999994, - }, - {0: 0.0, 1: 2.7755575615628914e-17, 2: 0.0, 3: 2.7755575615628914e-17}, - {0: 0.0, 1: 0.0, 2: 0.0, 3: 0.0}, - ], - ] - for i, param in enumerate(param_list): - gradients = gradient.run([qc], [param]).result().gradients[0] - array1 = _quasi2array(gradients, num_qubits=2) - array2 = _quasi2array(expected[i], num_qubits=2) - np.testing.assert_allclose(array1, array2, atol=1e-3) - - @data(*gradient_factories) - def test_gradient_2qubit_gate(self, grad): - """Test the sampler gradient for 2 qubit gates""" - sampler = Sampler() - for gate in [RXXGate]: - param_list = [[np.pi / 4], [np.pi / 2]] - correct_results = [ - [{0: -0.5 / np.sqrt(2), 1: 0, 2: 0, 3: 0.5 / np.sqrt(2)}], - [{0: -0.5, 1: 0, 2: 0, 3: 0.5}], - ] - for i, param in enumerate(param_list): - a = Parameter("a") - qc = QuantumCircuit(2) - qc.append(gate(a), [qc.qubits[0], qc.qubits[1]], []) - qc.measure_all() - gradient = grad(sampler) - gradients = gradient.run([qc], [param]).result().gradients[0] - array1 = _quasi2array(gradients, num_qubits=2) - array2 = _quasi2array(correct_results[i], num_qubits=2) - np.testing.assert_allclose(array1, array2, atol=1e-3) - - @data(*gradient_factories) - def test_gradient_parameter_coefficient(self, grad): - """Test the sampler gradient for parameter variables with coefficients""" - sampler = Sampler() - qc = RealAmplitudes(num_qubits=2, reps=1) - qc.rz(qc.parameters[0].exp() + 2 * qc.parameters[1], 0) - qc.rx(3.0 * qc.parameters[0] + qc.parameters[1].sin(), 1) - qc.u(qc.parameters[0], qc.parameters[1], qc.parameters[3], 1) - qc.p(2 * qc.parameters[0] + 1, 0) - qc.rxx(qc.parameters[0] + 2, 0, 1) - qc.measure_all() - gradient = grad(sampler) - param_list = [[np.pi / 4 for _ in qc.parameters], [np.pi / 2 for _ in qc.parameters]] - correct_results = [ - [ - { - 0: 0.30014831912265927, - 1: -0.6634809704357856, - 2: 0.343589357193753, - 3: 0.019743294119373426, - }, - { - 0: 0.16470607453981906, - 1: -0.40996282450610577, - 2: 0.08791803062881773, - 3: 0.15733871933746948, - }, - { - 0: 0.27036068339663866, - 1: -0.273790986018701, - 2: 0.12752010079553433, - 3: -0.12408979817347202, - }, - { - 0: -0.2098616294167757, - 1: -0.2515823946449894, - 2: 0.21929102305386305, - 3: 0.24215300100790207, - }, - ], - [ - { - 0: -1.844810060881004, - 1: 0.04620532700836027, - 2: 1.6367366426074323, - 3: 0.16186809126521057, - }, - { - 0: 0.07296073407769421, - 1: -0.021774869186331716, - 2: 0.02177486918633173, - 3: -0.07296073407769456, - }, - { - 0: -0.07794369186049102, - 1: -0.07794369186049122, - 2: 0.07794369186049117, - 3: 0.07794369186049112, - }, - { - 0: 0.0, - 1: 0.0, - 2: 0.0, - 3: 0.0, - }, - ], - ] - - for i, param in enumerate(param_list): - gradients = gradient.run([qc], [param]).result().gradients[0] - array1 = _quasi2array(gradients, num_qubits=2) - array2 = _quasi2array(correct_results[i], num_qubits=2) - np.testing.assert_allclose(array1, array2, atol=1e-3) - - @data(*gradient_factories) - def test_gradient_parameters(self, grad): - """Test the sampler gradient for parameters""" - sampler = Sampler() - a = Parameter("a") - b = Parameter("b") - qc = QuantumCircuit(1) - qc.rx(a, 0) - qc.rz(b, 0) - qc.measure_all() - gradient = grad(sampler) - param_list = [[np.pi / 4, np.pi / 2]] - expected = [ - [{0: -0.5 / np.sqrt(2), 1: 0.5 / np.sqrt(2)}], - ] - for i, param in enumerate(param_list): - gradients = gradient.run([qc], [param], parameters=[[a]]).result().gradients[0] - array1 = _quasi2array(gradients, num_qubits=1) - array2 = _quasi2array(expected[i], num_qubits=1) - np.testing.assert_allclose(array1, array2, atol=1e-3) - - # parameter order - with self.subTest(msg="The order of gradients"): - c = Parameter("c") - qc = QuantumCircuit(1) - qc.rx(a, 0) - qc.rz(b, 0) - qc.rx(c, 0) - qc.measure_all() - param_values = [[np.pi / 4, np.pi / 2, np.pi / 3]] - params = [[a, b, c], [c, b, a], [a, c], [c, a]] - expected = [ - [ - {0: -0.17677666583387008, 1: 0.17677666583378482}, - {0: 0.3061861668168149, 1: -0.3061861668167012}, - {0: -0.3061861668168149, 1: 0.30618616681678645}, - ], - [ - {0: -0.3061861668168149, 1: 0.30618616681678645}, - {0: 0.3061861668168149, 1: -0.3061861668167012}, - {0: -0.17677666583387008, 1: 0.17677666583378482}, - ], - [ - {0: -0.17677666583387008, 1: 0.17677666583378482}, - {0: -0.3061861668168149, 1: 0.30618616681678645}, - ], - [ - {0: -0.3061861668168149, 1: 0.30618616681678645}, - {0: -0.17677666583387008, 1: 0.17677666583378482}, - ], - ] - for i, p in enumerate(params): - gradients = gradient.run([qc], param_values, parameters=[p]).result().gradients[0] - array1 = _quasi2array(gradients, num_qubits=1) - array2 = _quasi2array(expected[i], num_qubits=1) - np.testing.assert_allclose(array1, array2, atol=1e-3) - - @data(*gradient_factories) - def test_gradient_multi_arguments(self, grad): - """Test the sampler gradient for multiple arguments""" - sampler = Sampler() - a = Parameter("a") - b = Parameter("b") - qc = QuantumCircuit(1) - qc.rx(a, 0) - qc.measure_all() - qc2 = QuantumCircuit(1) - qc2.rx(b, 0) - qc2.measure_all() - gradient = grad(sampler) - param_list = [[np.pi / 4], [np.pi / 2]] - correct_results = [ - [{0: -0.5 / np.sqrt(2), 1: 0.5 / np.sqrt(2)}], - [{0: -0.499999, 1: 0.499999}], - ] - gradients = gradient.run([qc, qc2], param_list).result().gradients - for i, q_dists in enumerate(gradients): - array1 = _quasi2array(q_dists, num_qubits=1) - array2 = _quasi2array(correct_results[i], num_qubits=1) - np.testing.assert_allclose(array1, array2, atol=1e-3) - - # parameters - with self.subTest(msg="Different parameters"): - c = Parameter("c") - qc3 = QuantumCircuit(1) - qc3.rx(c, 0) - qc3.ry(a, 0) - qc3.measure_all() - param_list2 = [[np.pi / 4], [np.pi / 4, np.pi / 4], [np.pi / 4, np.pi / 4]] - gradients = ( - gradient.run([qc, qc3, qc3], param_list2, parameters=[[a], [c], None]) - .result() - .gradients - ) - correct_results = [ - [{0: -0.5 / np.sqrt(2), 1: 0.5 / np.sqrt(2)}], - [{0: -0.25, 1: 0.25}], - [{0: -0.25, 1: 0.25}, {0: -0.25, 1: 0.25}], - ] - for i, q_dists in enumerate(gradients): - array1 = _quasi2array(q_dists, num_qubits=1) - array2 = _quasi2array(correct_results[i], num_qubits=1) - np.testing.assert_allclose(array1, array2, atol=1e-3) - - @data(*gradient_factories) - def test_gradient_validation(self, grad): - """Test sampler gradient's validation""" - sampler = Sampler() - a = Parameter("a") - qc = QuantumCircuit(1) - qc.rx(a, 0) - qc.measure_all() - gradient = grad(sampler) - param_list = [[np.pi / 4], [np.pi / 2]] - with self.assertRaises(ValueError): - gradient.run([qc], param_list) - with self.assertRaises(ValueError): - gradient.run([qc, qc], param_list, parameters=[[a]]) - with self.assertRaises(ValueError): - gradient.run([qc], [[np.pi / 4, np.pi / 4]]) - - def test_spsa_gradient(self): - """Test the SPSA sampler gradient""" - sampler = Sampler() - with self.assertRaises(ValueError): - _ = SPSASamplerGradient(sampler, epsilon=-0.1) - - a = Parameter("a") - b = Parameter("b") - c = Parameter("c") - qc = QuantumCircuit(2) - qc.rx(b, 0) - qc.rx(a, 1) - qc.measure_all() - param_list = [[1, 2]] - correct_results = [ - [ - {0: 0.2273244, 1: -0.6480598, 2: 0.2273244, 3: 0.1934111}, - {0: -0.2273244, 1: 0.6480598, 2: -0.2273244, 3: -0.1934111}, - ], - ] - gradient = SPSASamplerGradient(sampler, epsilon=1e-6, seed=123) - for i, param in enumerate(param_list): - gradients = gradient.run([qc], [param]).result().gradients[0] - array1 = _quasi2array(gradients, num_qubits=2) - array2 = _quasi2array(correct_results[i], num_qubits=2) - np.testing.assert_allclose(array1, array2, atol=1e-3) - - # multi parameters - with self.subTest(msg="Multiple parameters"): - param_list2 = [[1, 2], [1, 2], [3, 4]] - correct_results2 = [ - [ - {0: 0.2273244, 1: -0.6480598, 2: 0.2273244, 3: 0.1934111}, - {0: -0.2273244, 1: 0.6480598, 2: -0.2273244, 3: -0.1934111}, - ], - [ - {0: -0.2273244, 1: 0.6480598, 2: -0.2273244, 3: -0.1934111}, - ], - [ - {0: -0.0141129, 1: -0.0564471, 2: -0.3642884, 3: 0.4348484}, - {0: 0.0141129, 1: 0.0564471, 2: 0.3642884, 3: -0.4348484}, - ], - ] - gradient = SPSASamplerGradient(sampler, epsilon=1e-6, seed=123) - gradients = ( - gradient.run([qc] * 3, param_list2, parameters=[None, [b], None]).result().gradients - ) - for i, result in enumerate(gradients): - array1 = _quasi2array(result, num_qubits=2) - array2 = _quasi2array(correct_results2[i], num_qubits=2) - np.testing.assert_allclose(array1, array2, atol=1e-3) - - # batch size - with self.subTest(msg="Batch size"): - param_list = [[1, 1]] - gradient = SPSASamplerGradient(sampler, epsilon=1e-6, batch_size=4, seed=123) - gradients = gradient.run([qc], param_list).result().gradients - correct_results3 = [ - [ - { - 0: -0.1620149622932887, - 1: -0.25872053011771756, - 2: 0.3723827084675668, - 3: 0.04835278392088804, - }, - { - 0: -0.1620149622932887, - 1: 0.3723827084675668, - 2: -0.25872053011771756, - 3: 0.04835278392088804, - }, - ] - ] - for i, q_dists in enumerate(gradients): - array1 = _quasi2array(q_dists, num_qubits=2) - array2 = _quasi2array(correct_results3[i], num_qubits=2) - np.testing.assert_allclose(array1, array2, atol=1e-3) - - # parameter order - with self.subTest(msg="The order of gradients"): - qc = QuantumCircuit(1) - qc.rx(a, 0) - qc.rz(b, 0) - qc.rx(c, 0) - qc.measure_all() - param_list = [[np.pi / 4, np.pi / 2, np.pi / 3]] - param = [[a, b, c], [c, b, a], [a, c], [c, a]] - correct_results = [ - [ - {0: -0.17677624757590138, 1: 0.17677624757590138}, - {0: 0.17677624757590138, 1: -0.17677624757590138}, - {0: 0.17677624757590138, 1: -0.17677624757590138}, - ], - [ - {0: 0.17677624757590138, 1: -0.17677624757590138}, - {0: 0.17677624757590138, 1: -0.17677624757590138}, - {0: -0.17677624757590138, 1: 0.17677624757590138}, - ], - [ - {0: -0.17677624757590138, 1: 0.17677624757590138}, - {0: 0.17677624757590138, 1: -0.17677624757590138}, - ], - [ - {0: 0.17677624757590138, 1: -0.17677624757590138}, - {0: -0.17677624757590138, 1: 0.17677624757590138}, - ], - ] - for i, p in enumerate(param): - gradient = SPSASamplerGradient(sampler, epsilon=1e-6, seed=123) - gradients = gradient.run([qc], param_list, parameters=[p]).result().gradients[0] - array1 = _quasi2array(gradients, num_qubits=1) - array2 = _quasi2array(correct_results[i], num_qubits=1) - np.testing.assert_allclose(array1, array2, atol=1e-3) - - @data(ParamShiftSamplerGradient, LinCombSamplerGradient) - def test_gradient_random_parameters(self, grad): - """Test param shift and lin comb w/ random parameters""" - rng = np.random.default_rng(123) - qc = RealAmplitudes(num_qubits=3, reps=1) - params = qc.parameters - qc.rx(3.0 * params[0] + params[1].sin(), 0) - qc.ry(params[0].exp() + 2 * params[1], 1) - qc.rz(params[0] * params[1] - params[2], 2) - qc.p(2 * params[0] + 1, 0) - qc.u(params[0].sin(), params[1] - 2, params[2] * params[3], 1) - qc.sx(2) - qc.rxx(params[0].sin(), 1, 2) - qc.ryy(params[1].cos(), 2, 0) - qc.rzz(params[2] * 2, 0, 1) - qc.crx(params[0].exp(), 1, 2) - qc.cry(params[1].arctan(), 2, 0) - qc.crz(params[2] * -2, 0, 1) - qc.dcx(0, 1) - qc.csdg(0, 1) - qc.ccx(0, 1, 2) - qc.iswap(0, 2) - qc.swap(1, 2) - qc.global_phase = params[0] * params[1] + params[2].cos().exp() - qc.measure_all() - - sampler = Sampler() - findiff = FiniteDiffSamplerGradient(sampler, 1e-6) - gradient = grad(sampler) - - num_qubits = qc.num_qubits - num_tries = 10 - param_values = rng.normal(0, 2, (num_tries, qc.num_parameters)).tolist() - result1 = findiff.run([qc] * num_tries, param_values).result().gradients - result2 = gradient.run([qc] * num_tries, param_values).result().gradients - self.assertEqual(len(result1), len(result2)) - for res1, res2 in zip(result1, result2): - array1 = _quasi2array(res1, num_qubits) - array2 = _quasi2array(res2, num_qubits) - np.testing.assert_allclose(array1, array2, rtol=1e-4) - - @data( - FiniteDiffSamplerGradient, - ParamShiftSamplerGradient, - LinCombSamplerGradient, - SPSASamplerGradient, - ) - def test_options(self, grad): - """Test sampler gradient's run options""" - a = Parameter("a") - qc = QuantumCircuit(1) - qc.rx(a, 0) - qc.measure_all() - sampler = Sampler(options={"shots": 100}) - with self.subTest("sampler"): - if grad is FiniteDiffSamplerGradient or grad is SPSASamplerGradient: - gradient = grad(sampler, epsilon=1e-6) - else: - gradient = grad(sampler) - options = gradient.options - result = gradient.run([qc], [[1]]).result() - self.assertEqual(result.options.get("shots"), 100) - self.assertEqual(options.get("shots"), 100) - - with self.subTest("gradient init"): - if grad is FiniteDiffSamplerGradient or grad is SPSASamplerGradient: - gradient = grad(sampler, epsilon=1e-6, options={"shots": 200}) - else: - gradient = grad(sampler, options={"shots": 200}) - options = gradient.options - result = gradient.run([qc], [[1]]).result() - self.assertEqual(result.options.get("shots"), 200) - self.assertEqual(options.get("shots"), 200) - - with self.subTest("gradient update"): - if grad is FiniteDiffSamplerGradient or grad is SPSASamplerGradient: - gradient = grad(sampler, epsilon=1e-6, options={"shots": 200}) - else: - gradient = grad(sampler, options={"shots": 200}) - gradient.update_default_options(shots=100) - options = gradient.options - result = gradient.run([qc], [[1]]).result() - self.assertEqual(result.options.get("shots"), 100) - self.assertEqual(options.get("shots"), 100) - - with self.subTest("gradient run"): - if grad is FiniteDiffSamplerGradient or grad is SPSASamplerGradient: - gradient = grad(sampler, epsilon=1e-6, options={"shots": 200}) - else: - gradient = grad(sampler, options={"shots": 200}) - options = gradient.options - result = gradient.run([qc], [[1]], shots=300).result() - self.assertEqual(result.options.get("shots"), 300) - # Only default + sampler options. Not run. - self.assertEqual(options.get("shots"), 200) - - @data( - FiniteDiffSamplerGradient, - ParamShiftSamplerGradient, - LinCombSamplerGradient, - SPSASamplerGradient, - ) - def test_operations_preserved(self, gradient_cls): - """Test non-parameterized instructions are preserved and not unrolled.""" - x = Parameter("x") - circuit = QuantumCircuit(2) - circuit.initialize(np.array([1, 1, 0, 0]) / np.sqrt(2)) # this should remain as initialize - circuit.crx(x, 0, 1) # this should get unrolled - circuit.measure_all() - - values = [np.pi / 2] - expect = [{0: 0, 1: -0.25, 2: 0, 3: 0.25}] - - ops = [] - - def operations_callback(op): - ops.append(op) - - sampler = LoggingSampler(operations_callback=operations_callback) - - if gradient_cls in [SPSASamplerGradient, FiniteDiffSamplerGradient]: - gradient = gradient_cls(sampler, epsilon=0.01) - else: - gradient = gradient_cls(sampler) - - job = gradient.run([circuit], [values]) - result = job.result() - - with self.subTest(msg="assert initialize is preserved"): - self.assertTrue(all("initialize" in ops_i[0].keys() for ops_i in ops)) - - with self.subTest(msg="assert result is correct"): - array1 = _quasi2array(result.gradients[0], num_qubits=2) - array2 = _quasi2array(expect, num_qubits=2) - np.testing.assert_allclose(array1, array2, atol=1e-5) - - -def _quasi2array(quasis: List[QuasiDistribution], num_qubits: int) -> np.ndarray: - ret = np.zeros((len(quasis), 2**num_qubits)) - for i, quasi in enumerate(quasis): - ret[i, list(quasi.keys())] = list(quasi.values()) - return ret - - -if __name__ == "__main__": - unittest.main() diff --git a/test/python/algorithms/minimum_eigensolvers/__init__.py b/test/python/algorithms/minimum_eigensolvers/__init__.py deleted file mode 100644 index fdb172d367f0..000000000000 --- a/test/python/algorithms/minimum_eigensolvers/__init__.py +++ /dev/null @@ -1,11 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2022. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. diff --git a/test/python/algorithms/minimum_eigensolvers/test_adapt_vqe.py b/test/python/algorithms/minimum_eigensolvers/test_adapt_vqe.py deleted file mode 100644 index 9509abf422f3..000000000000 --- a/test/python/algorithms/minimum_eigensolvers/test_adapt_vqe.py +++ /dev/null @@ -1,245 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2022, 2023. -# -# 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 of the AdaptVQE minimum eigensolver""" - -import unittest -import warnings - -from test.python.algorithms import QiskitAlgorithmsTestCase - -from ddt import ddt, data, unpack -import numpy as np - -from qiskit.algorithms.minimum_eigensolvers import VQE -from qiskit.algorithms.minimum_eigensolvers.adapt_vqe import AdaptVQE, TerminationCriterion -from qiskit.algorithms.optimizers import SLSQP -from qiskit.circuit import QuantumCircuit, QuantumRegister -from qiskit.circuit.library import EvolvedOperatorAnsatz -from qiskit.opflow import PauliSumOp -from qiskit.primitives import Estimator -from qiskit.quantum_info import SparsePauliOp -from qiskit.utils import algorithm_globals - - -@ddt -class TestAdaptVQE(QiskitAlgorithmsTestCase): - """Test of the AdaptVQE minimum eigensolver""" - - def setUp(self): - super().setUp() - with warnings.catch_warnings(): - warnings.filterwarnings("ignore", category=DeprecationWarning) - algorithm_globals.random_seed = 42 - - with self.assertWarns(DeprecationWarning): - self.h2_op = PauliSumOp.from_list( - [ - ("IIII", -0.8105479805373266), - ("ZZII", -0.2257534922240251), - ("IIZI", +0.12091263261776641), - ("ZIZI", +0.12091263261776641), - ("IZZI", +0.17218393261915543), - ("IIIZ", +0.17218393261915546), - ("IZIZ", +0.1661454325638243), - ("ZZIZ", +0.1661454325638243), - ("IIZZ", -0.2257534922240251), - ("IZZZ", +0.16892753870087926), - ("ZZZZ", +0.17464343068300464), - ("IXIX", +0.04523279994605788), - ("ZXIX", +0.04523279994605788), - ("IXZX", -0.04523279994605788), - ("ZXZX", -0.04523279994605788), - ] - ) - self.excitation_pool = [ - PauliSumOp( - SparsePauliOp(["IIIY", "IIZY"], coeffs=[0.5 + 0.0j, -0.5 + 0.0j]), coeff=1.0 - ), - PauliSumOp( - SparsePauliOp(["ZYII", "IYZI"], coeffs=[-0.5 + 0.0j, 0.5 + 0.0j]), coeff=1.0 - ), - PauliSumOp( - SparsePauliOp( - ["ZXZY", "IXIY", "IYIX", "ZYZX", "IYZX", "ZYIX", "ZXIY", "IXZY"], - coeffs=[ - -0.125 + 0.0j, - 0.125 + 0.0j, - -0.125 + 0.0j, - 0.125 + 0.0j, - 0.125 + 0.0j, - -0.125 + 0.0j, - 0.125 + 0.0j, - -0.125 + 0.0j, - ], - ), - coeff=1.0, - ), - ] - self.initial_state = QuantumCircuit(QuantumRegister(4)) - self.initial_state.x(0) - self.initial_state.x(1) - self.ansatz = EvolvedOperatorAnsatz( - self.excitation_pool, initial_state=self.initial_state - ) - self.optimizer = SLSQP() - - def test_default(self): - """Default execution""" - calc = AdaptVQE(VQE(Estimator(), self.ansatz, self.optimizer)) - - with self.assertWarns(DeprecationWarning): - res = calc.compute_minimum_eigenvalue(operator=self.h2_op) - - expected_eigenvalue = -1.85727503 - - self.assertAlmostEqual(res.eigenvalue, expected_eigenvalue, places=6) - np.testing.assert_allclose(res.eigenvalue_history, [expected_eigenvalue], rtol=1e-6) - - def test_with_quantum_info(self): - """Test behavior with quantum_info-based operators.""" - ansatz = EvolvedOperatorAnsatz( - [op.primitive for op in self.excitation_pool], - initial_state=self.initial_state, - ) - - calc = AdaptVQE(VQE(Estimator(), ansatz, self.optimizer)) - res = calc.compute_minimum_eigenvalue(operator=self.h2_op.primitive) - - expected_eigenvalue = -1.85727503 - - self.assertAlmostEqual(res.eigenvalue, expected_eigenvalue, places=6) - np.testing.assert_allclose(res.eigenvalue_history, [expected_eigenvalue], rtol=1e-6) - - def test_converged(self): - """Test to check termination criteria""" - calc = AdaptVQE( - VQE(Estimator(), self.ansatz, self.optimizer), - gradient_threshold=1e-3, - ) - with self.assertWarns(DeprecationWarning): - res = calc.compute_minimum_eigenvalue(operator=self.h2_op) - - self.assertEqual(res.termination_criterion, TerminationCriterion.CONVERGED) - - def test_maximum(self): - """Test to check termination criteria""" - calc = AdaptVQE( - VQE(Estimator(), self.ansatz, self.optimizer), - max_iterations=1, - ) - with self.assertWarns(DeprecationWarning): - res = calc.compute_minimum_eigenvalue(operator=self.h2_op) - - self.assertEqual(res.termination_criterion, TerminationCriterion.MAXIMUM) - - def test_eigenvalue_threshold(self): - """Test for the eigenvalue_threshold attribute.""" - operator = SparsePauliOp.from_list( - [ - ("XX", 1.0), - ("ZX", -0.5), - ("XZ", -0.5), - ] - ) - ansatz = EvolvedOperatorAnsatz( - [ - SparsePauliOp.from_list([("YZ", 0.4)]), - SparsePauliOp.from_list([("ZY", 0.5)]), - ], - initial_state=QuantumCircuit(2), - ) - - calc = AdaptVQE( - VQE(Estimator(), ansatz, self.optimizer), - eigenvalue_threshold=1, - ) - res = calc.compute_minimum_eigenvalue(operator) - - self.assertEqual(res.termination_criterion, TerminationCriterion.CONVERGED) - - def test_threshold_attribute(self): - """Test the (pending deprecated) threshold attribute""" - with self.assertWarns(PendingDeprecationWarning): - calc = AdaptVQE( - VQE(Estimator(), self.ansatz, self.optimizer), - threshold=1e-3, - ) - with self.assertWarns(DeprecationWarning): - res = calc.compute_minimum_eigenvalue(operator=self.h2_op) - - self.assertEqual(res.termination_criterion, TerminationCriterion.CONVERGED) - - @data( - ([1, 1], True), - ([1, 11], False), - ([11, 1], False), - ([1, 12], False), - ([12, 2], False), - ([1, 1, 1], True), - ([1, 2, 1], False), - ([1, 2, 2], True), - ([1, 2, 21], False), - ([1, 12, 2], False), - ([11, 1, 2], False), - ([1, 2, 1, 1], True), - ([1, 2, 1, 2], True), - ([1, 2, 1, 21], False), - ([11, 2, 1, 2], False), - ([1, 11, 1, 111], False), - ([11, 1, 111, 1], False), - ([1, 2, 3, 1, 2, 3], True), - ([1, 2, 3, 4, 1, 2, 3], False), - ([11, 2, 3, 1, 2, 3], False), - ([1, 2, 3, 1, 2, 31], False), - ([1, 2, 3, 4, 1, 2, 3, 4], True), - ([11, 2, 3, 4, 1, 2, 3, 4], False), - ([1, 2, 3, 4, 1, 2, 3, 41], False), - ([1, 2, 3, 4, 5, 1, 2, 3, 4], False), - ) - @unpack - def test_cyclicity(self, seq, is_cycle): - """Test AdaptVQE index cycle detection""" - self.assertEqual(is_cycle, AdaptVQE._check_cyclicity(seq)) - - def test_vqe_solver(self): - """Test to check if the VQE solver remains the same or not""" - solver = VQE(Estimator(), self.ansatz, self.optimizer) - calc = AdaptVQE(solver) - with self.assertWarns(DeprecationWarning): - _ = calc.compute_minimum_eigenvalue(operator=self.h2_op) - self.assertEqual(solver.ansatz, calc.solver.ansatz) - - def test_gradient_calculation(self): - """Test to check if the gradient calculation""" - solver = VQE(Estimator(), QuantumCircuit(1), self.optimizer) - calc = AdaptVQE(solver) - calc._excitation_pool = [SparsePauliOp("X")] - res = calc._compute_gradients(operator=SparsePauliOp("Y"), theta=[]) - # compare with manually computed reference value - self.assertAlmostEqual(res[0][0], 2.0) - - def test_supports_aux_operators(self): - """Test that auxiliary operators are supported""" - calc = AdaptVQE(VQE(Estimator(), self.ansatz, self.optimizer)) - with self.assertWarns(DeprecationWarning): - res = calc.compute_minimum_eigenvalue(operator=self.h2_op, aux_operators=[self.h2_op]) - - expected_eigenvalue = -1.85727503 - - self.assertAlmostEqual(res.eigenvalue, expected_eigenvalue, places=6) - self.assertAlmostEqual(res.aux_operators_evaluated[0][0], expected_eigenvalue, places=6) - np.testing.assert_allclose(res.eigenvalue_history, [expected_eigenvalue], rtol=1e-6) - - -if __name__ == "__main__": - unittest.main() diff --git a/test/python/algorithms/minimum_eigensolvers/test_numpy_minimum_eigensolver.py b/test/python/algorithms/minimum_eigensolvers/test_numpy_minimum_eigensolver.py deleted file mode 100644 index f5724f77c152..000000000000 --- a/test/python/algorithms/minimum_eigensolvers/test_numpy_minimum_eigensolver.py +++ /dev/null @@ -1,240 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2022. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. - -"""Test NumPy minimum eigensolver""" - -import unittest -from test.python.algorithms import QiskitAlgorithmsTestCase - -import numpy as np -from ddt import ddt, data - -from qiskit.algorithms.minimum_eigensolvers import NumPyMinimumEigensolver -from qiskit.opflow import PauliSumOp -from qiskit.quantum_info import Operator, SparsePauliOp - -H2_SPARSE_PAULI = SparsePauliOp( - ["II", "ZI", "IZ", "ZZ", "XX"], - coeffs=[ - -1.052373245772859, - 0.39793742484318045, - -0.39793742484318045, - -0.01128010425623538, - 0.18093119978423156, - ], -) - -H2_OP = Operator(H2_SPARSE_PAULI.to_matrix()) - -H2_PAULI = PauliSumOp(H2_SPARSE_PAULI) - - -@ddt -class TestNumPyMinimumEigensolver(QiskitAlgorithmsTestCase): - """Test NumPy minimum eigensolver""" - - def setUp(self): - super().setUp() - aux_op1 = Operator(SparsePauliOp(["II"], coeffs=[2.0]).to_matrix()) - aux_op2 = SparsePauliOp(["II", "ZZ", "YY", "XX"], coeffs=[0.5, 0.5, 0.5, -0.5]) - self.aux_ops_list = [aux_op1, aux_op2] - self.aux_ops_dict = {"aux_op1": aux_op1, "aux_op2": aux_op2} - - @data(H2_SPARSE_PAULI, H2_PAULI, H2_OP) - def test_cme(self, op): - """Basic test""" - algo = NumPyMinimumEigensolver() - result = algo.compute_minimum_eigenvalue(operator=op, aux_operators=self.aux_ops_list) - self.assertAlmostEqual(result.eigenvalue, -1.85727503 + 0j) - self.assertEqual(len(result.aux_operators_evaluated), 2) - self.assertAlmostEqual(result.aux_operators_evaluated[0][0], 2) - self.assertAlmostEqual(result.aux_operators_evaluated[1][0], 0) - - @data(H2_SPARSE_PAULI, H2_PAULI, H2_OP) - def test_cme_reuse(self, op): - """Test reuse""" - algo = NumPyMinimumEigensolver() - - with self.subTest("Test with no operator or aux_operators, give via compute method"): - result = algo.compute_minimum_eigenvalue(operator=op) - self.assertEqual(result.eigenvalue.dtype, np.float64) - self.assertAlmostEqual(result.eigenvalue, -1.85727503) - self.assertIsNone(result.aux_operators_evaluated) - - with self.subTest("Test with added aux_operators"): - result = algo.compute_minimum_eigenvalue(operator=op, aux_operators=self.aux_ops_list) - self.assertAlmostEqual(result.eigenvalue, -1.85727503 + 0j) - self.assertEqual(len(result.aux_operators_evaluated), 2) - self.assertAlmostEqual(result.aux_operators_evaluated[0][0], 2) - self.assertAlmostEqual(result.aux_operators_evaluated[1][0], 0) - - with self.subTest("Test with aux_operators removed"): - result = algo.compute_minimum_eigenvalue(operator=op, aux_operators=[]) - self.assertEqual(result.eigenvalue.dtype, np.float64) - self.assertAlmostEqual(result.eigenvalue, -1.85727503) - self.assertIsNone(result.aux_operators_evaluated) - - with self.subTest("Test with aux_operators set again"): - result = algo.compute_minimum_eigenvalue(operator=op, aux_operators=self.aux_ops_list) - self.assertAlmostEqual(result.eigenvalue, -1.85727503 + 0j) - self.assertEqual(len(result.aux_operators_evaluated), 2) - self.assertAlmostEqual(result.aux_operators_evaluated[0][0], 2) - self.assertAlmostEqual(result.aux_operators_evaluated[1][0], 0) - - with self.subTest("Test after setting first aux_operators as main operator"): - result = algo.compute_minimum_eigenvalue( - operator=self.aux_ops_list[0], aux_operators=[] - ) - self.assertAlmostEqual(result.eigenvalue, 2 + 0j) - self.assertIsNone(result.aux_operators_evaluated) - - @data(H2_SPARSE_PAULI, H2_PAULI, H2_OP) - def test_cme_filter(self, op): - """Basic test""" - - # define filter criterion - # pylint: disable=unused-argument - def criterion(x, v, a_v): - return v >= -0.5 - - algo = NumPyMinimumEigensolver(filter_criterion=criterion) - result = algo.compute_minimum_eigenvalue(operator=op, aux_operators=self.aux_ops_list) - self.assertAlmostEqual(result.eigenvalue, -0.22491125 + 0j) - self.assertEqual(len(result.aux_operators_evaluated), 2) - self.assertAlmostEqual(result.aux_operators_evaluated[0][0], 2) - self.assertAlmostEqual(result.aux_operators_evaluated[1][0], 0) - - @data(H2_SPARSE_PAULI, H2_PAULI, H2_OP) - def test_cme_filter_empty(self, op): - """Test with filter always returning False""" - - # define filter criterion - # pylint: disable=unused-argument - def criterion(x, v, a_v): - return False - - algo = NumPyMinimumEigensolver(filter_criterion=criterion) - result = algo.compute_minimum_eigenvalue(operator=op, aux_operators=self.aux_ops_list) - self.assertEqual(result.eigenvalue, None) - self.assertEqual(result.eigenstate, None) - self.assertEqual(result.aux_operators_evaluated, None) - - @data("X", "Y", "Z") - def test_cme_1q(self, op): - """Test for 1 qubit operator""" - algo = NumPyMinimumEigensolver() - operator = SparsePauliOp([op], coeffs=1.0) - result = algo.compute_minimum_eigenvalue(operator=operator) - self.assertAlmostEqual(result.eigenvalue, -1) - - @data(H2_SPARSE_PAULI, H2_PAULI, H2_OP) - def test_cme_aux_ops_dict(self, op): - """Test dictionary compatibility of aux_operators""" - # Start with an empty dictionary - algo = NumPyMinimumEigensolver() - - with self.subTest("Test with an empty dictionary."): - result = algo.compute_minimum_eigenvalue(operator=op, aux_operators={}) - self.assertAlmostEqual(result.eigenvalue, -1.85727503 + 0j) - self.assertIsNone(result.aux_operators_evaluated) - - with self.subTest("Test with two auxiliary operators."): - result = algo.compute_minimum_eigenvalue(operator=op, aux_operators=self.aux_ops_dict) - self.assertAlmostEqual(result.eigenvalue, -1.85727503 + 0j) - self.assertEqual(len(result.aux_operators_evaluated), 2) - self.assertAlmostEqual(result.aux_operators_evaluated["aux_op1"][0], 2) - self.assertAlmostEqual(result.aux_operators_evaluated["aux_op2"][0], 0) - - with self.subTest("Test with additional zero and None operators."): - extra_ops = {"None_op": None, "zero_op": 0, **self.aux_ops_dict} - result = algo.compute_minimum_eigenvalue(operator=op, aux_operators=extra_ops) - self.assertAlmostEqual(result.eigenvalue, -1.85727503 + 0j) - self.assertEqual(len(result.aux_operators_evaluated), 3) - self.assertAlmostEqual(result.aux_operators_evaluated["aux_op1"][0], 2) - self.assertAlmostEqual(result.aux_operators_evaluated["aux_op2"][0], 0) - self.assertEqual(result.aux_operators_evaluated["zero_op"], (0.0, {"variance": 0})) - - @data(H2_SPARSE_PAULI, H2_PAULI, H2_OP) - def test_aux_operators_list(self, op): - """Test list-based aux_operators.""" - algo = NumPyMinimumEigensolver() - - with self.subTest("Test with two auxiliary operators."): - result = algo.compute_minimum_eigenvalue(operator=op, aux_operators=self.aux_ops_list) - self.assertAlmostEqual(result.eigenvalue, -1.85727503 + 0j) - self.assertEqual(len(result.aux_operators_evaluated), 2) - # expectation values - self.assertAlmostEqual(result.aux_operators_evaluated[0][0], 2, places=6) - self.assertAlmostEqual(result.aux_operators_evaluated[1][0], 0, places=6) - # standard deviations - self.assertAlmostEqual(result.aux_operators_evaluated[0][1].pop("variance"), 0.0) - self.assertAlmostEqual(result.aux_operators_evaluated[1][1].pop("variance"), 0.0) - - with self.subTest("Test with additional zero and None operators."): - extra_ops = [*self.aux_ops_list, None, 0] - result = algo.compute_minimum_eigenvalue(operator=op, aux_operators=extra_ops) - self.assertAlmostEqual(result.eigenvalue, -1.85727503 + 0j) - self.assertEqual(len(result.aux_operators_evaluated), 4) - # expectation values - self.assertAlmostEqual(result.aux_operators_evaluated[0][0], 2, places=6) - self.assertAlmostEqual(result.aux_operators_evaluated[1][0], 0, places=6) - self.assertIsNone(result.aux_operators_evaluated[2], None) - self.assertEqual(result.aux_operators_evaluated[3][0], 0.0) - # standard deviations - self.assertAlmostEqual(result.aux_operators_evaluated[0][1].pop("variance"), 0.0) - self.assertAlmostEqual(result.aux_operators_evaluated[1][1].pop("variance"), 0.0) - self.assertEqual(result.aux_operators_evaluated[3][1].pop("variance"), 0.0) - - @data(H2_SPARSE_PAULI, H2_PAULI, H2_OP) - def test_aux_operators_dict(self, op): - """Test dict-based aux_operators.""" - algo = NumPyMinimumEigensolver() - - with self.subTest("Test with two auxiliary operators."): - result = algo.compute_minimum_eigenvalue(operator=op, aux_operators=self.aux_ops_dict) - self.assertAlmostEqual(result.eigenvalue, -1.85727503 + 0j) - self.assertEqual(len(result.aux_operators_evaluated), 2) - # expectation values - self.assertAlmostEqual(result.aux_operators_evaluated["aux_op1"][0], 2, places=6) - self.assertAlmostEqual(result.aux_operators_evaluated["aux_op2"][0], 0, places=6) - # standard deviations - self.assertAlmostEqual( - result.aux_operators_evaluated["aux_op1"][1].pop("variance"), 0.0 - ) - self.assertAlmostEqual( - result.aux_operators_evaluated["aux_op2"][1].pop("variance"), 0.0 - ) - - with self.subTest("Test with additional zero and None operators."): - extra_ops = {**self.aux_ops_dict, "None_operator": None, "zero_operator": 0} - result = algo.compute_minimum_eigenvalue(operator=op, aux_operators=extra_ops) - self.assertAlmostEqual(result.eigenvalue, -1.85727503 + 0j) - self.assertEqual(len(result.aux_operators_evaluated), 3) - # expectation values - self.assertAlmostEqual(result.aux_operators_evaluated["aux_op1"][0], 2, places=6) - self.assertAlmostEqual(result.aux_operators_evaluated["aux_op2"][0], 0, places=6) - self.assertEqual(result.aux_operators_evaluated["zero_operator"][0], 0.0) - self.assertTrue("None_operator" not in result.aux_operators_evaluated.keys()) - # standard deviations - self.assertAlmostEqual( - result.aux_operators_evaluated["aux_op1"][1].pop("variance"), 0.0 - ) - self.assertAlmostEqual( - result.aux_operators_evaluated["aux_op2"][1].pop("variance"), 0.0 - ) - self.assertAlmostEqual( - result.aux_operators_evaluated["zero_operator"][1].pop("variance"), 0.0 - ) - - -if __name__ == "__main__": - unittest.main() diff --git a/test/python/algorithms/minimum_eigensolvers/test_qaoa.py b/test/python/algorithms/minimum_eigensolvers/test_qaoa.py deleted file mode 100644 index c269f2aa0769..000000000000 --- a/test/python/algorithms/minimum_eigensolvers/test_qaoa.py +++ /dev/null @@ -1,304 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2022, 2023. -# -# 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 QAOA algorithm.""" - -import unittest -import warnings - -from functools import partial -from test.python.algorithms import QiskitAlgorithmsTestCase - -import numpy as np -import rustworkx as rx -from ddt import ddt, idata, unpack -from scipy.optimize import minimize as scipy_minimize - -from qiskit import QuantumCircuit -from qiskit.algorithms.minimum_eigensolvers import QAOA -from qiskit.algorithms.optimizers import COBYLA, NELDER_MEAD -from qiskit.circuit import Parameter -from qiskit.primitives import Sampler -from qiskit.quantum_info import Pauli, SparsePauliOp -from qiskit.result import QuasiDistribution -from qiskit.utils import algorithm_globals - - -W1 = np.array([[0, 1, 0, 1], [1, 0, 1, 0], [0, 1, 0, 1], [1, 0, 1, 0]]) -P1 = 1 -M1 = SparsePauliOp.from_list( - [ - ("IIIX", 1), - ("IIXI", 1), - ("IXII", 1), - ("XIII", 1), - ] -) -S1 = {"0101", "1010"} - - -W2 = np.array( - [ - [0.0, 8.0, -9.0, 0.0], - [8.0, 0.0, 7.0, 9.0], - [-9.0, 7.0, 0.0, -8.0], - [0.0, 9.0, -8.0, 0.0], - ] -) -P2 = 1 -M2 = None -S2 = {"1011", "0100"} - -CUSTOM_SUPERPOSITION = [1 / np.sqrt(15)] * 15 + [0] - - -@ddt -class TestQAOA(QiskitAlgorithmsTestCase): - """Test QAOA with MaxCut.""" - - def setUp(self): - super().setUp() - self.seed = 10598 - with warnings.catch_warnings(): - warnings.filterwarnings("ignore", category=DeprecationWarning) - algorithm_globals.random_seed = self.seed - self.sampler = Sampler() - - @idata( - [ - [W1, P1, M1, S1], - [W2, P2, M2, S2], - ] - ) - @unpack - def test_qaoa(self, w, reps, mixer, solutions): - """QAOA test""" - self.log.debug("Testing %s-step QAOA with MaxCut on graph\n%s", reps, w) - - qubit_op, _ = self._get_operator(w) - - qaoa = QAOA(self.sampler, COBYLA(), reps=reps, mixer=mixer) - result = qaoa.compute_minimum_eigenvalue(operator=qubit_op) - x = self._sample_most_likely(result.eigenstate) - graph_solution = self._get_graph_solution(x) - self.assertIn(graph_solution, solutions) - - @idata( - [ - [W1, P1, S1], - [W2, P2, S2], - ] - ) - @unpack - def test_qaoa_qc_mixer(self, w, prob, solutions): - """QAOA test with a mixer as a parameterized circuit""" - self.log.debug( - "Testing %s-step QAOA with MaxCut on graph with a mixer as a parameterized circuit\n%s", - prob, - w, - ) - - optimizer = COBYLA() - qubit_op, _ = self._get_operator(w) - - num_qubits = qubit_op.num_qubits - mixer = QuantumCircuit(num_qubits) - theta = Parameter("θ") - mixer.rx(theta, range(num_qubits)) - - qaoa = QAOA(self.sampler, optimizer, reps=prob, mixer=mixer) - result = qaoa.compute_minimum_eigenvalue(operator=qubit_op) - x = self._sample_most_likely(result.eigenstate) - graph_solution = self._get_graph_solution(x) - self.assertIn(graph_solution, solutions) - - def test_qaoa_qc_mixer_many_parameters(self): - """QAOA test with a mixer as a parameterized circuit with the num of parameters > 1.""" - optimizer = COBYLA() - qubit_op, _ = self._get_operator(W1) - - num_qubits = qubit_op.num_qubits - mixer = QuantumCircuit(num_qubits) - for i in range(num_qubits): - theta = Parameter("θ" + str(i)) - mixer.rx(theta, range(num_qubits)) - - qaoa = QAOA(self.sampler, optimizer, reps=2, mixer=mixer) - result = qaoa.compute_minimum_eigenvalue(operator=qubit_op) - x = self._sample_most_likely(result.eigenstate) - self.log.debug(x) - graph_solution = self._get_graph_solution(x) - self.assertIn(graph_solution, S1) - - def test_qaoa_qc_mixer_no_parameters(self): - """QAOA test with a mixer as a parameterized circuit with zero parameters.""" - qubit_op, _ = self._get_operator(W1) - - num_qubits = qubit_op.num_qubits - mixer = QuantumCircuit(num_qubits) - # just arbitrary circuit - mixer.rx(np.pi / 2, range(num_qubits)) - - qaoa = QAOA(self.sampler, COBYLA(), reps=1, mixer=mixer) - result = qaoa.compute_minimum_eigenvalue(operator=qubit_op) - # we just assert that we get a result, it is not meaningful. - self.assertIsNotNone(result.eigenstate) - - def test_change_operator_size(self): - """QAOA change operator size test""" - qubit_op, _ = self._get_operator( - np.array([[0, 1, 0, 1], [1, 0, 1, 0], [0, 1, 0, 1], [1, 0, 1, 0]]) - ) - qaoa = QAOA(self.sampler, COBYLA(), reps=1) - result = qaoa.compute_minimum_eigenvalue(operator=qubit_op) - x = self._sample_most_likely(result.eigenstate) - graph_solution = self._get_graph_solution(x) - with self.subTest(msg="QAOA 4x4"): - self.assertIn(graph_solution, {"0101", "1010"}) - - qubit_op, _ = self._get_operator( - np.array( - [ - [0, 1, 0, 1, 0, 1], - [1, 0, 1, 0, 1, 0], - [0, 1, 0, 1, 0, 1], - [1, 0, 1, 0, 1, 0], - [0, 1, 0, 1, 0, 1], - [1, 0, 1, 0, 1, 0], - ] - ) - ) - result = qaoa.compute_minimum_eigenvalue(operator=qubit_op) - x = self._sample_most_likely(result.eigenstate) - graph_solution = self._get_graph_solution(x) - with self.subTest(msg="QAOA 6x6"): - self.assertIn(graph_solution, {"010101", "101010"}) - - @idata([[W2, S2, None], [W2, S2, [0.0, 0.0]], [W2, S2, [1.0, 0.8]]]) - @unpack - def test_qaoa_initial_point(self, w, solutions, init_pt): - """Check first parameter value used is initial point as expected""" - qubit_op, _ = self._get_operator(w) - - first_pt = [] - - def cb_callback(eval_count, parameters, mean, metadata): - nonlocal first_pt - if eval_count == 1: - first_pt = list(parameters) - - qaoa = QAOA( - self.sampler, - COBYLA(), - initial_point=init_pt, - callback=cb_callback, - ) - result = qaoa.compute_minimum_eigenvalue(operator=qubit_op) - x = self._sample_most_likely(result.eigenstate) - graph_solution = self._get_graph_solution(x) - - with self.subTest("Initial Point"): - # If None the preferred random initial point of QAOA variational form - if init_pt is None: - self.assertLess(result.eigenvalue, -0.97) - else: - self.assertListEqual(init_pt, first_pt) - - with self.subTest("Solution"): - self.assertIn(graph_solution, solutions) - - def test_qaoa_random_initial_point(self): - """QAOA random initial point""" - with warnings.catch_warnings(): - warnings.filterwarnings("ignore", category=DeprecationWarning) - w = rx.adjacency_matrix( - rx.undirected_gnp_random_graph(5, 0.5, seed=algorithm_globals.random_seed) - ) - qubit_op, _ = self._get_operator(w) - qaoa = QAOA(self.sampler, NELDER_MEAD(disp=True), reps=2) - result = qaoa.compute_minimum_eigenvalue(operator=qubit_op) - - self.assertLess(result.eigenvalue, -0.97) - - def test_optimizer_scipy_callable(self): - """Test passing a SciPy optimizer directly as callable.""" - with warnings.catch_warnings(): - warnings.filterwarnings("ignore", category=DeprecationWarning) - w = rx.adjacency_matrix( - rx.undirected_gnp_random_graph(5, 0.5, seed=algorithm_globals.random_seed) - ) - qubit_op, _ = self._get_operator(w) - qaoa = QAOA( - self.sampler, - partial(scipy_minimize, method="Nelder-Mead", options={"maxiter": 2}), - ) - result = qaoa.compute_minimum_eigenvalue(qubit_op) - self.assertEqual(result.cost_function_evals, 5) - - def _get_operator(self, weight_matrix): - """Generate Hamiltonian for the max-cut problem of a graph. - - Args: - weight_matrix (numpy.ndarray) : adjacency matrix. - - Returns: - PauliSumOp: operator for the Hamiltonian - float: a constant shift for the obj function. - - """ - num_nodes = weight_matrix.shape[0] - pauli_list = [] - shift = 0 - for i in range(num_nodes): - for j in range(i): - if weight_matrix[i, j] != 0: - x_p = np.zeros(num_nodes, dtype=bool) - z_p = np.zeros(num_nodes, dtype=bool) - z_p[i] = True - z_p[j] = True - pauli_list.append([0.5 * weight_matrix[i, j], Pauli((z_p, x_p))]) - shift -= 0.5 * weight_matrix[i, j] - lst = [(pauli[1].to_label(), pauli[0]) for pauli in pauli_list] - return SparsePauliOp.from_list(lst), shift - - def _get_graph_solution(self, x: np.ndarray) -> str: - """Get graph solution from binary string. - - Args: - x : binary string as numpy array. - - Returns: - a graph solution as string. - """ - - return "".join([str(int(i)) for i in 1 - x]) - - def _sample_most_likely(self, state_vector: QuasiDistribution) -> np.ndarray: - """Compute the most likely binary string from state vector. - Args: - state_vector: Quasi-distribution. - - Returns: - Binary string as numpy.ndarray of ints. - """ - values = list(state_vector.values()) - n = int(np.log2(len(values))) - k = np.argmax(np.abs(values)) - x = np.zeros(n) - for i in range(n): - x[i] = k % 2 - k >>= 1 - return x - - -if __name__ == "__main__": - unittest.main() diff --git a/test/python/algorithms/minimum_eigensolvers/test_qaoa_opflow.py b/test/python/algorithms/minimum_eigensolvers/test_qaoa_opflow.py deleted file mode 100644 index 18527c85046e..000000000000 --- a/test/python/algorithms/minimum_eigensolvers/test_qaoa_opflow.py +++ /dev/null @@ -1,312 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2022, 2023. -# -# 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 QAOA algorithm with opflow.""" - -import unittest -import warnings - -from test.python.algorithms import QiskitAlgorithmsTestCase - -from functools import partial -import numpy as np - -from scipy.optimize import minimize as scipy_minimize -from ddt import ddt, idata, unpack - -import rustworkx as rx - -from qiskit import QuantumCircuit -from qiskit.algorithms.minimum_eigensolvers import QAOA -from qiskit.algorithms.optimizers import COBYLA, NELDER_MEAD -from qiskit.circuit import Parameter -from qiskit.opflow import PauliSumOp -from qiskit.quantum_info import Pauli -from qiskit.result import QuasiDistribution -from qiskit.primitives import Sampler -from qiskit.utils import algorithm_globals - -I = PauliSumOp.from_list([("I", 1)]) -X = PauliSumOp.from_list([("X", 1)]) - -W1 = np.array([[0, 1, 0, 1], [1, 0, 1, 0], [0, 1, 0, 1], [1, 0, 1, 0]]) -P1 = 1 -M1 = (I ^ I ^ I ^ X) + (I ^ I ^ X ^ I) + (I ^ X ^ I ^ I) + (X ^ I ^ I ^ I) -S1 = {"0101", "1010"} - - -W2 = np.array( - [ - [0.0, 8.0, -9.0, 0.0], - [8.0, 0.0, 7.0, 9.0], - [-9.0, 7.0, 0.0, -8.0], - [0.0, 9.0, -8.0, 0.0], - ] -) -P2 = 1 -M2 = None -S2 = {"1011", "0100"} - -CUSTOM_SUPERPOSITION = [1 / np.sqrt(15)] * 15 + [0] - - -@ddt -class TestQAOA(QiskitAlgorithmsTestCase): - """Test QAOA with MaxCut.""" - - def setUp(self): - super().setUp() - self.seed = 10598 - with warnings.catch_warnings(): - warnings.filterwarnings("ignore", category=DeprecationWarning) - algorithm_globals.random_seed = self.seed - self.sampler = Sampler() - - @idata( - [ - [W1, P1, M1, S1], - [W2, P2, M2, S2], - ] - ) - @unpack - def test_qaoa(self, w, reps, mixer, solutions): - """QAOA test""" - self.log.debug("Testing %s-step QAOA with MaxCut on graph\n%s", reps, w) - - qubit_op, _ = self._get_operator(w) - - qaoa = QAOA(self.sampler, COBYLA(), reps=reps, mixer=mixer) - with self.assertWarns(DeprecationWarning): - result = qaoa.compute_minimum_eigenvalue(operator=qubit_op) - x = self._sample_most_likely(result.eigenstate) - graph_solution = self._get_graph_solution(x) - self.assertIn(graph_solution, solutions) - - @idata( - [ - [W1, P1, S1], - [W2, P2, S2], - ] - ) - @unpack - def test_qaoa_qc_mixer(self, w, prob, solutions): - """QAOA test with a mixer as a parameterized circuit""" - self.log.debug( - "Testing %s-step QAOA with MaxCut on graph with a mixer as a parameterized circuit\n%s", - prob, - w, - ) - - optimizer = COBYLA() - qubit_op, _ = self._get_operator(w) - - num_qubits = qubit_op.num_qubits - mixer = QuantumCircuit(num_qubits) - theta = Parameter("θ") - mixer.rx(theta, range(num_qubits)) - - qaoa = QAOA(self.sampler, optimizer, reps=prob, mixer=mixer) - with self.assertWarns(DeprecationWarning): - result = qaoa.compute_minimum_eigenvalue(operator=qubit_op) - x = self._sample_most_likely(result.eigenstate) - graph_solution = self._get_graph_solution(x) - self.assertIn(graph_solution, solutions) - - def test_qaoa_qc_mixer_many_parameters(self): - """QAOA test with a mixer as a parameterized circuit with the num of parameters > 1.""" - optimizer = COBYLA() - qubit_op, _ = self._get_operator(W1) - - num_qubits = qubit_op.num_qubits - mixer = QuantumCircuit(num_qubits) - for i in range(num_qubits): - theta = Parameter("θ" + str(i)) - mixer.rx(theta, range(num_qubits)) - - qaoa = QAOA(self.sampler, optimizer, reps=2, mixer=mixer) - with self.assertWarns(DeprecationWarning): - result = qaoa.compute_minimum_eigenvalue(operator=qubit_op) - x = self._sample_most_likely(result.eigenstate) - self.log.debug(x) - graph_solution = self._get_graph_solution(x) - self.assertIn(graph_solution, S1) - - def test_qaoa_qc_mixer_no_parameters(self): - """QAOA test with a mixer as a parameterized circuit with zero parameters.""" - qubit_op, _ = self._get_operator(W1) - - num_qubits = qubit_op.num_qubits - mixer = QuantumCircuit(num_qubits) - # just arbitrary circuit - mixer.rx(np.pi / 2, range(num_qubits)) - - qaoa = QAOA(self.sampler, COBYLA(), reps=1, mixer=mixer) - with self.assertWarns(DeprecationWarning): - result = qaoa.compute_minimum_eigenvalue(operator=qubit_op) - # we just assert that we get a result, it is not meaningful. - self.assertIsNotNone(result.eigenstate) - - def test_change_operator_size(self): - """QAOA change operator size test""" - qubit_op, _ = self._get_operator( - np.array([[0, 1, 0, 1], [1, 0, 1, 0], [0, 1, 0, 1], [1, 0, 1, 0]]) - ) - qaoa = QAOA(self.sampler, COBYLA(), reps=1) - with self.assertWarns(DeprecationWarning): - result = qaoa.compute_minimum_eigenvalue(operator=qubit_op) - x = self._sample_most_likely(result.eigenstate) - graph_solution = self._get_graph_solution(x) - with self.subTest(msg="QAOA 4x4"): - self.assertIn(graph_solution, {"0101", "1010"}) - - qubit_op, _ = self._get_operator( - np.array( - [ - [0, 1, 0, 1, 0, 1], - [1, 0, 1, 0, 1, 0], - [0, 1, 0, 1, 0, 1], - [1, 0, 1, 0, 1, 0], - [0, 1, 0, 1, 0, 1], - [1, 0, 1, 0, 1, 0], - ] - ) - ) - with self.assertWarns(DeprecationWarning): - result = qaoa.compute_minimum_eigenvalue(operator=qubit_op) - x = self._sample_most_likely(result.eigenstate) - graph_solution = self._get_graph_solution(x) - with self.subTest(msg="QAOA 6x6"): - self.assertIn(graph_solution, {"010101", "101010"}) - - @idata([[W2, S2, None], [W2, S2, [0.0, 0.0]], [W2, S2, [1.0, 0.8]]]) - @unpack - def test_qaoa_initial_point(self, w, solutions, init_pt): - """Check first parameter value used is initial point as expected""" - qubit_op, _ = self._get_operator(w) - - first_pt = [] - - def cb_callback(eval_count, parameters, mean, metadata): - nonlocal first_pt - if eval_count == 1: - first_pt = list(parameters) - - qaoa = QAOA( - self.sampler, - COBYLA(), - initial_point=init_pt, - callback=cb_callback, - ) - with self.assertWarns(DeprecationWarning): - result = qaoa.compute_minimum_eigenvalue(operator=qubit_op) - x = self._sample_most_likely(result.eigenstate) - graph_solution = self._get_graph_solution(x) - - with self.subTest("Initial Point"): - # If None the preferred random initial point of QAOA variational form - if init_pt is None: - self.assertLess(result.eigenvalue, -0.97) - else: - self.assertListEqual(init_pt, first_pt) - - with self.subTest("Solution"): - self.assertIn(graph_solution, solutions) - - def test_qaoa_random_initial_point(self): - """QAOA random initial point""" - with warnings.catch_warnings(): - warnings.filterwarnings("ignore", category=DeprecationWarning) - w = rx.adjacency_matrix( - rx.undirected_gnp_random_graph(5, 0.5, seed=algorithm_globals.random_seed) - ) - qubit_op, _ = self._get_operator(w) - qaoa = QAOA(self.sampler, NELDER_MEAD(disp=True), reps=2) - with self.assertWarns(DeprecationWarning): - result = qaoa.compute_minimum_eigenvalue(operator=qubit_op) - - self.assertLess(result.eigenvalue, -0.97) - - def test_optimizer_scipy_callable(self): - """Test passing a SciPy optimizer directly as callable.""" - with warnings.catch_warnings(): - warnings.filterwarnings("ignore", category=DeprecationWarning) - w = rx.adjacency_matrix( - rx.undirected_gnp_random_graph(5, 0.5, seed=algorithm_globals.random_seed) - ) - qubit_op, _ = self._get_operator(w) - qaoa = QAOA( - self.sampler, - partial(scipy_minimize, method="Nelder-Mead", options={"maxiter": 2}), - ) - with self.assertWarns(DeprecationWarning): - result = qaoa.compute_minimum_eigenvalue(qubit_op) - self.assertEqual(result.cost_function_evals, 5) - - def _get_operator(self, weight_matrix): - """Generate Hamiltonian for the max-cut problem of a graph. - - Args: - weight_matrix (numpy.ndarray) : adjacency matrix. - - Returns: - PauliSumOp: operator for the Hamiltonian - float: a constant shift for the obj function. - - """ - num_nodes = weight_matrix.shape[0] - pauli_list = [] - shift = 0 - for i in range(num_nodes): - for j in range(i): - if weight_matrix[i, j] != 0: - x_p = np.zeros(num_nodes, dtype=bool) - z_p = np.zeros(num_nodes, dtype=bool) - z_p[i] = True - z_p[j] = True - pauli_list.append([0.5 * weight_matrix[i, j], Pauli((z_p, x_p))]) - shift -= 0.5 * weight_matrix[i, j] - opflow_list = [(pauli[1].to_label(), pauli[0]) for pauli in pauli_list] - with self.assertWarns(DeprecationWarning): - return PauliSumOp.from_list(opflow_list), shift - - def _get_graph_solution(self, x: np.ndarray) -> str: - """Get graph solution from binary string. - - Args: - x : binary string as numpy array. - - Returns: - a graph solution as string. - """ - - return "".join([str(int(i)) for i in 1 - x]) - - def _sample_most_likely(self, state_vector: QuasiDistribution) -> np.ndarray: - """Compute the most likely binary string from state vector. - Args: - state_vector: Quasi-distribution. - - Returns: - Binary string as numpy.ndarray of ints. - """ - values = list(state_vector.values()) - n = int(np.log2(len(values))) - k = np.argmax(np.abs(values)) - x = np.zeros(n) - for i in range(n): - x[i] = k % 2 - k >>= 1 - return x - - -if __name__ == "__main__": - unittest.main() diff --git a/test/python/algorithms/minimum_eigensolvers/test_sampling_vqe.py b/test/python/algorithms/minimum_eigensolvers/test_sampling_vqe.py deleted file mode 100644 index 4a7eb1929ef9..000000000000 --- a/test/python/algorithms/minimum_eigensolvers/test_sampling_vqe.py +++ /dev/null @@ -1,287 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2018, 2021. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. - -"""Test the Sampler VQE.""" - - -import unittest -import warnings -from functools import partial -from test.python.algorithms import QiskitAlgorithmsTestCase - -import numpy as np -from ddt import data, ddt -from scipy.optimize import minimize as scipy_minimize - -from qiskit.algorithms import AlgorithmError -from qiskit.algorithms.minimum_eigensolvers import SamplingVQE -from qiskit.algorithms.optimizers import L_BFGS_B, QNSPSA, SLSQP, OptimizerResult -from qiskit.algorithms.state_fidelities import ComputeUncompute -from qiskit.circuit import ParameterVector, QuantumCircuit -from qiskit.circuit.library import RealAmplitudes, TwoLocal -from qiskit.opflow import PauliSumOp -from qiskit.primitives import Sampler -from qiskit.quantum_info import Operator, Pauli, SparsePauliOp -from qiskit.utils import algorithm_globals - - -# pylint: disable=invalid-name -def _mock_optimizer(fun, x0, jac=None, bounds=None, inputs=None): - """A mock of a callable that can be used as minimizer in the VQE. - - If ``inputs`` is given as a dictionary, stores the inputs in that dictionary. - """ - 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 - - -PAULI_OP = PauliSumOp(SparsePauliOp(["ZZ", "IZ", "II"], coeffs=[1, -0.5, 0.12])) -OP = Operator(PAULI_OP.to_matrix()) - - -@ddt -class TestSamplerVQE(QiskitAlgorithmsTestCase): - """Test VQE""" - - def setUp(self): - super().setUp() - self.optimal_value = -1.38 - self.optimal_bitstring = "10" - with warnings.catch_warnings(): - warnings.filterwarnings("ignore", category=DeprecationWarning) - algorithm_globals.random_seed = 42 - - @data(PAULI_OP, OP) - def test_exact_sampler(self, op): - """Test the VQE on BasicAer's statevector simulator.""" - thetas = ParameterVector("th", 4) - ansatz = QuantumCircuit(2) - ansatz.rx(thetas[0], 0) - ansatz.rx(thetas[1], 1) - ansatz.cz(0, 1) - ansatz.ry(thetas[2], 0) - ansatz.ry(thetas[3], 1) - - optimizer = L_BFGS_B() - - # start in maximal superposition - initial_point = np.zeros(ansatz.num_parameters) - initial_point[-ansatz.num_qubits :] = np.pi / 2 - - vqe = SamplingVQE(Sampler(), ansatz, optimizer, initial_point=initial_point) - result = vqe.compute_minimum_eigenvalue(operator=op) - - with self.subTest(msg="test eigenvalue"): - self.assertAlmostEqual(result.eigenvalue, self.optimal_value, places=5) - - with self.subTest(msg="test optimal_value"): - self.assertAlmostEqual(result.optimal_value, self.optimal_value, places=5) - - with self.subTest(msg="test dimension of optimal point"): - self.assertEqual(len(result.optimal_point), ansatz.num_parameters) - - 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="check best measurement"): - self.assertEqual(result.best_measurement["bitstring"], self.optimal_bitstring) - self.assertEqual(result.best_measurement["value"], self.optimal_value) - - @data(PAULI_OP, OP) - def test_invalid_initial_point(self, op): - """Test the proper error is raised when the initial point has the wrong size.""" - ansatz = RealAmplitudes(2, reps=1) - initial_point = np.array([1]) - - vqe = SamplingVQE(Sampler(), ansatz, SLSQP(), initial_point=initial_point) - - with self.assertRaises(ValueError): - _ = vqe.compute_minimum_eigenvalue(operator=op) - - @data(PAULI_OP, OP) - def test_ansatz_resize(self, op): - """Test the ansatz is properly resized if it's a blueprint circuit.""" - ansatz = RealAmplitudes(1, reps=1) - vqe = SamplingVQE(Sampler(), ansatz, SLSQP()) - result = vqe.compute_minimum_eigenvalue(operator=op) - self.assertAlmostEqual(result.eigenvalue, self.optimal_value, places=5) - - @data(PAULI_OP, OP) - def test_invalid_ansatz_size(self, op): - """Test an error is raised if the ansatz has the wrong number of qubits.""" - ansatz = QuantumCircuit(1) - ansatz.compose(RealAmplitudes(1, reps=2)) - vqe = SamplingVQE(Sampler(), ansatz, SLSQP()) - - with self.assertRaises(AlgorithmError): - _ = vqe.compute_minimum_eigenvalue(operator=op) - - @data(PAULI_OP, OP) - def test_missing_varform_params(self, op): - """Test specifying a variational form with no parameters raises an error.""" - circuit = QuantumCircuit(op.num_qubits) - vqe = SamplingVQE(Sampler(), circuit, SLSQP()) - with self.assertRaises(AlgorithmError): - vqe.compute_minimum_eigenvalue(operator=op) - - @data(PAULI_OP, OP) - def test_batch_evaluate_slsqp(self, op): - """Test batching with SLSQP (as representative of SciPyOptimizer).""" - optimizer = SLSQP(max_evals_grouped=10) - vqe = SamplingVQE(Sampler(), RealAmplitudes(), optimizer) - result = vqe.compute_minimum_eigenvalue(operator=op) - self.assertAlmostEqual(result.eigenvalue, self.optimal_value, places=5) - - def test_batch_evaluate_with_qnspsa(self): - """Test batch evaluating with QNSPSA works.""" - ansatz = TwoLocal(2, rotation_blocks=["ry", "rz"], entanglement_blocks="cz") - - wrapped_sampler = Sampler() - inner_sampler = Sampler() - - callcount = {"count": 0} - - def wrapped_run(*args, **kwargs): - kwargs["callcount"]["count"] += 1 - return inner_sampler.run(*args, **kwargs) - - wrapped_sampler.run = partial(wrapped_run, callcount=callcount) - - fidelity = ComputeUncompute(wrapped_sampler) - - def fidelity_callable(left, right): - batchsize = np.asarray(left).shape[0] - job = fidelity.run(batchsize * [ansatz], batchsize * [ansatz], left, right) - return job.result().fidelities - - qnspsa = QNSPSA(fidelity_callable, maxiter=5) - qnspsa.set_max_evals_grouped(100) - - vqe = SamplingVQE(wrapped_sampler, ansatz, qnspsa) - _ = vqe.compute_minimum_eigenvalue(Pauli("ZZ")) - - # 1 calibration + 1 stddev estimation + 1 initial blocking - # + 5 (1 loss + 1 fidelity + 1 blocking) + 1 return loss + 1 VQE eval - expected = 1 + 1 + 1 + 5 * 3 + 1 + 1 - - self.assertEqual(callcount["count"], expected) - - def test_optimizer_scipy_callable(self): - """Test passing a SciPy optimizer directly as callable.""" - vqe = SamplingVQE( - Sampler(), - RealAmplitudes(), - partial(scipy_minimize, method="COBYLA", options={"maxiter": 2}), - ) - result = vqe.compute_minimum_eigenvalue(Pauli("Z")) - self.assertEqual(result.cost_function_evals, 2) - - def test_optimizer_callable(self): - """Test passing a optimizer directly as callable.""" - ansatz = RealAmplitudes(1, reps=1) - vqe = SamplingVQE(Sampler(), ansatz, _mock_optimizer) - result = vqe.compute_minimum_eigenvalue(Pauli("Z")) - self.assertTrue(np.all(result.optimal_point == np.zeros(ansatz.num_parameters))) - - @data(PAULI_OP, OP) - def test_auxops(self, op): - """Test passing auxiliary operators.""" - ansatz = RealAmplitudes(2, reps=1) - vqe = SamplingVQE(Sampler(), ansatz, SLSQP()) - - as_list = [Pauli("ZZ"), Pauli("II")] - with self.subTest(auxops=as_list): - result = vqe.compute_minimum_eigenvalue(op, aux_operators=as_list) - self.assertIsInstance(result.aux_operators_evaluated, list) - self.assertEqual(len(result.aux_operators_evaluated), 2) - self.assertAlmostEqual(result.aux_operators_evaluated[0][0], -1 + 0j, places=5) - self.assertAlmostEqual(result.aux_operators_evaluated[1][0], 1 + 0j, places=5) - - as_dict = {"magnetization": SparsePauliOp(["ZI", "IZ"])} - with self.subTest(auxops=as_dict): - result = vqe.compute_minimum_eigenvalue(op, aux_operators=as_dict) - self.assertIsInstance(result.aux_operators_evaluated, dict) - self.assertEqual(len(result.aux_operators_evaluated.keys()), 1) - self.assertAlmostEqual(result.aux_operators_evaluated["magnetization"][0], 0j, places=5) - - def test_nondiag_observable_raises(self): - """Test passing a non-diagonal observable raises an error.""" - vqe = SamplingVQE(Sampler(), RealAmplitudes(), SLSQP()) - - with self.assertRaises(ValueError): - _ = vqe.compute_minimum_eigenvalue(Pauli("X")) - - @data(PAULI_OP, OP) - def test_callback(self, op): - """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) - - sampling_vqe = SamplingVQE( - Sampler(), - RealAmplitudes(2, reps=1), - SLSQP(), - callback=store_intermediate_result, - ) - sampling_vqe.compute_minimum_eigenvalue(operator=op) - - self.assertTrue(all(isinstance(count, int) for count in history["eval_count"])) - self.assertTrue(all(isinstance(mean, complex) 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_aggregation(self): - """Test the aggregation works.""" - - # test a custom aggregration that just uses the best measurement - def best_measurement(measurements): - res = min(measurements, key=lambda meas: meas[1])[1] - return res - - # test CVaR with alpha of 0.4 (i.e. 40% of the best measurements) - alpha = 0.4 - - ansatz = RealAmplitudes(1, reps=0) - ansatz.h(0) - - for aggregation in [alpha, best_measurement]: - with self.subTest(aggregation=aggregation): - vqe = SamplingVQE(Sampler(), ansatz, _mock_optimizer, aggregation=best_measurement) - result = vqe.compute_minimum_eigenvalue(Pauli("Z")) - - # evaluation at x0=0 samples -1 and 1 with 50% probability, and our aggregation - # takes the smallest value - self.assertAlmostEqual(result.optimal_value, -1) - - -if __name__ == "__main__": - unittest.main() diff --git a/test/python/algorithms/minimum_eigensolvers/test_vqe.py b/test/python/algorithms/minimum_eigensolvers/test_vqe.py deleted file mode 100644 index 210c622ca817..000000000000 --- a/test/python/algorithms/minimum_eigensolvers/test_vqe.py +++ /dev/null @@ -1,488 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2022, 2023. -# -# 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 -import warnings -from test.python.algorithms import QiskitAlgorithmsTestCase - -from functools import partial -import numpy as np -from scipy.optimize import minimize as scipy_minimize -from ddt import data, ddt - -from qiskit import QuantumCircuit -from qiskit.algorithms import AlgorithmError -from qiskit.algorithms.gradients import ParamShiftEstimatorGradient -from qiskit.algorithms.minimum_eigensolvers import VQE -from qiskit.algorithms.optimizers import ( - CG, - COBYLA, - GradientDescent, - L_BFGS_B, - OptimizerResult, - P_BFGS, - QNSPSA, - SLSQP, - SPSA, - TNC, -) -from qiskit.algorithms.state_fidelities import ComputeUncompute -from qiskit.circuit.library import RealAmplitudes, TwoLocal -from qiskit.opflow import PauliSumOp, TwoQubitReduction -from qiskit.quantum_info import SparsePauliOp, Operator, Pauli -from qiskit.primitives import Estimator, Sampler -from qiskit.utils import algorithm_globals - -# pylint: disable=invalid-name -def _mock_optimizer(fun, x0, jac=None, bounds=None, inputs=None) -> OptimizerResult: - """A mock of a callable that can be used as minimizer in the VQE.""" - result = OptimizerResult() - 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 - with warnings.catch_warnings(): - warnings.filterwarnings("ignore", category=DeprecationWarning) - 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(L_BFGS_B(), COBYLA()) - def test_basic_aer_statevector(self, estimator): - """Test VQE using reference Estimator.""" - vqe = VQE(Estimator(), self.ryrz_wavefunction, estimator) - - result = vqe.compute_minimum_eigenvalue(operator=self.h2_op) - - with self.subTest(msg="test eigenvalue"): - self.assertAlmostEqual(result.eigenvalue.real, self.h2_energy, 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, - SLSQP(), - 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, SLSQP()) - 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, SLSQP()) - - 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, SLSQP()) - with self.assertRaises(AlgorithmError): - vqe.compute_minimum_eigenvalue(operator=self.h2_op) - - def test_max_evals_grouped(self): - """Test with SLSQP with max_evals_grouped.""" - optimizer = SLSQP(maxiter=50, 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) - - @data( - CG(), - L_BFGS_B(), - P_BFGS(), - SLSQP(), - TNC(), - ) - def test_with_gradient(self, optimizer): - """Test VQE using gradient primitive.""" - estimator = Estimator() - vqe = VQE( - estimator, - self.ry_wavefunction, - optimizer, - gradient=ParamShiftEstimatorGradient(estimator), - ) - result = vqe.compute_minimum_eigenvalue(operator=self.h2_op) - self.assertAlmostEqual(result.eigenvalue.real, self.h2_energy, places=5) - - def test_gradient_passed(self): - """Test the gradient is properly passed into the optimizer.""" - inputs = {} - estimator = Estimator() - vqe = VQE( - estimator, - RealAmplitudes(), - partial(_mock_optimizer, inputs=inputs), - gradient=ParamShiftEstimatorGradient(estimator), - ) - _ = vqe.compute_minimum_eigenvalue(operator=self.h2_op) - - self.assertIsNotNone(inputs["jac"]) - - def test_gradient_run(self): - """Test using the gradient to calculate the minimum.""" - estimator = Estimator() - vqe = VQE( - estimator, - RealAmplitudes(), - GradientDescent(maxiter=200, learning_rate=0.1), - gradient=ParamShiftEstimatorGradient(estimator), - ) - result = vqe.compute_minimum_eigenvalue(operator=self.h2_op) - self.assertAlmostEqual(result.eigenvalue.real, self.h2_energy, places=5) - - def test_with_two_qubit_reduction(self): - """Test the VQE using TwoQubitReduction.""" - with self.assertWarns(DeprecationWarning): - qubit_op = PauliSumOp.from_list( - [ - ("IIII", -0.8105479805373266), - ("IIIZ", 0.17218393261915552), - ("IIZZ", -0.22575349222402472), - ("IZZI", 0.1721839326191556), - ("ZZII", -0.22575349222402466), - ("IIZI", 0.1209126326177663), - ("IZZZ", 0.16892753870087912), - ("IXZX", -0.045232799946057854), - ("ZXIX", 0.045232799946057854), - ("IXIX", 0.045232799946057854), - ("ZXZX", -0.045232799946057854), - ("ZZIZ", 0.16614543256382414), - ("IZIZ", 0.16614543256382414), - ("ZZZZ", 0.17464343068300453), - ("ZIZI", 0.1209126326177663), - ] - ) - tapered_qubit_op = TwoQubitReduction(num_particles=2).convert(qubit_op) - vqe = VQE( - Estimator(), - self.ry_wavefunction, - SPSA(maxiter=300, last_avg=5), - ) - result = vqe.compute_minimum_eigenvalue(tapered_qubit_op) - self.assertAlmostEqual(result.eigenvalue.real, self.h2_energy, places=2) - - def test_callback(self): - """Test the callback on VQE.""" - history = {"eval_count": [], "parameters": [], "mean": [], "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, SLSQP(maxiter=300)) - with self.subTest(msg="assert VQE works once all info is available"): - result = vqe.compute_minimum_eigenvalue(operator=self.h2_op) - self.assertAlmostEqual(result.eigenvalue.real, self.h2_energy, places=5) - - operator = Operator(np.array([[1, 0, 0, 0], [0, -1, 0, 0], [0, 0, 2, 0], [0, 0, 0, 3]])) - - with self.subTest(msg="assert vqe works on re-use."): - result = vqe.compute_minimum_eigenvalue(operator=operator) - self.assertAlmostEqual(result.eigenvalue.real, -1.0, places=5) - - def test_vqe_optimizer_reuse(self): - """Test running same VQE twice to re-use optimizer, then switch optimizer""" - vqe = VQE( - Estimator(), - self.ryrz_wavefunction, - SLSQP(), - ) - - def run_check(): - result = vqe.compute_minimum_eigenvalue(operator=self.h2_op) - self.assertAlmostEqual(result.eigenvalue.real, self.h2_energy, places=5) - - run_check() - - with self.subTest("Optimizer re-use."): - run_check() - - with self.subTest("Optimizer replace."): - vqe.optimizer = L_BFGS_B() - run_check() - - def test_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_batch_evaluate_with_qnspsa(self): - """Test batch evaluating with QNSPSA works.""" - ansatz = TwoLocal(2, rotation_blocks=["ry", "rz"], entanglement_blocks="cz") - - wrapped_sampler = Sampler() - inner_sampler = Sampler() - - wrapped_estimator = Estimator() - inner_estimator = Estimator() - - callcount = {"sampler": 0, "estimator": 0} - - def wrapped_estimator_run(*args, **kwargs): - kwargs["callcount"]["estimator"] += 1 - return inner_estimator.run(*args, **kwargs) - - def wrapped_sampler_run(*args, **kwargs): - kwargs["callcount"]["sampler"] += 1 - return inner_sampler.run(*args, **kwargs) - - wrapped_estimator.run = partial(wrapped_estimator_run, callcount=callcount) - wrapped_sampler.run = partial(wrapped_sampler_run, callcount=callcount) - - fidelity = ComputeUncompute(wrapped_sampler) - - def fidelity_callable(left, right): - batchsize = np.asarray(left).shape[0] - job = fidelity.run(batchsize * [ansatz], batchsize * [ansatz], left, right) - return job.result().fidelities - - qnspsa = QNSPSA(fidelity_callable, maxiter=5) - qnspsa.set_max_evals_grouped(100) - - vqe = VQE( - wrapped_estimator, - ansatz, - qnspsa, - ) - _ = vqe.compute_minimum_eigenvalue(Pauli("ZZ")) - - # 5 (fidelity) - expected_sampler_runs = 5 - # 1 calibration + 1 stddev estimation + 1 initial blocking - # + 5 (1 loss + 1 blocking) + 1 return loss - expected_estimator_runs = 1 + 1 + 1 + 5 * 2 + 1 - - self.assertEqual(callcount["sampler"], expected_sampler_runs) - self.assertEqual(callcount["estimator"], expected_estimator_runs) - - def test_optimizer_scipy_callable(self): - """Test passing a SciPy optimizer directly as callable.""" - 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, SLSQP(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."): - with self.assertWarns(DeprecationWarning): - aux_op1 = PauliSumOp.from_list([("II", 2.0)]) - aux_op2 = PauliSumOp.from_list( - [("II", 0.5), ("ZZ", 0.5), ("YY", 0.5), ("XX", -0.5)] - ) - aux_ops = [aux_op1, aux_op2] - result = vqe.compute_minimum_eigenvalue(self.h2_op, aux_operators=aux_ops) - - self.assertAlmostEqual(result.eigenvalue.real, self.h2_energy, places=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, SLSQP(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."): - with self.assertWarns(DeprecationWarning): - aux_op1 = PauliSumOp.from_list([("II", 2.0)]) - aux_op2 = PauliSumOp.from_list( - [("II", 0.5), ("ZZ", 0.5), ("YY", 0.5), ("XX", -0.5)] - ) - aux_ops = {"aux_op1": aux_op1, "aux_op2": aux_op2} - result = vqe.compute_minimum_eigenvalue(self.h2_op, aux_operators=aux_ops) - self.assertAlmostEqual(result.eigenvalue.real, self.h2_energy, places=6) - self.assertEqual(len(result.aux_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() diff --git a/test/python/algorithms/optimizers/__init__.py b/test/python/algorithms/optimizers/__init__.py deleted file mode 100644 index c6268f028739..000000000000 --- a/test/python/algorithms/optimizers/__init__.py +++ /dev/null @@ -1,13 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2021. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. - -"""Qiskit's algorithm optimizer tests.""" diff --git a/test/python/algorithms/optimizers/test_gradient_descent.py b/test/python/algorithms/optimizers/test_gradient_descent.py deleted file mode 100644 index 8d2396650176..000000000000 --- a/test/python/algorithms/optimizers/test_gradient_descent.py +++ /dev/null @@ -1,196 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2021, 2023. -# -# 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. - -"""Tests for the Gradient Descent optimizer.""" - -from test.python.algorithms import QiskitAlgorithmsTestCase -import numpy as np -from qiskit.algorithms.optimizers import GradientDescent, GradientDescentState -from qiskit.algorithms.optimizers.steppable_optimizer import TellData, AskData -from qiskit.circuit.library import PauliTwoDesign -from qiskit.opflow import I, Z, StateFn - - -class TestGradientDescent(QiskitAlgorithmsTestCase): - """Tests for the gradient descent optimizer.""" - - def setUp(self): - super().setUp() - np.random.seed(12) - self.initial_point = np.array([1, 1, 1, 1, 0]) - - def objective(self, x): - """Objective Function for the tests""" - return (np.linalg.norm(x) - 1) ** 2 - - def grad(self, x): - """Gradient of the objective function""" - return 2 * (np.linalg.norm(x) - 1) * x / np.linalg.norm(x) - - def test_pauli_two_design(self): - """Test standard gradient descent on the Pauli two-design example.""" - circuit = PauliTwoDesign(3, reps=3, seed=2) - parameters = list(circuit.parameters) - with self.assertWarns(DeprecationWarning): - obs = Z ^ Z ^ I - expr = ~StateFn(obs) @ StateFn(circuit) - - initial_point = np.array( - [ - 0.1822308, - -0.27254251, - 0.83684425, - 0.86153976, - -0.7111668, - 0.82766631, - 0.97867993, - 0.46136964, - 2.27079901, - 0.13382699, - 0.29589915, - 0.64883193, - ] - ) - - def objective_pauli(x): - return expr.bind_parameters(dict(zip(parameters, x))).eval().real - - optimizer = GradientDescent(maxiter=100, learning_rate=0.1, perturbation=0.1) - - with self.assertWarns(DeprecationWarning): - result = optimizer.minimize(objective_pauli, x0=initial_point) - self.assertLess(result.fun, -0.95) # final loss - self.assertEqual(result.nfev, 1300) # function evaluations - - def test_callback(self): - """Test the callback.""" - - history = [] - - def callback(*args): - history.append(args) - - optimizer = GradientDescent(maxiter=1, callback=callback) - - _ = optimizer.minimize(self.objective, np.array([1, -1])) - - self.assertEqual(len(history), 1) - self.assertIsInstance(history[0][0], int) # nfevs - self.assertIsInstance(history[0][1], np.ndarray) # parameters - self.assertIsInstance(history[0][2], float) # function value - self.assertIsInstance(history[0][3], float) # norm of the gradient - - def test_minimize(self): - """Test setting the learning rate as iterator and minimizing the funciton.""" - - def learning_rate(): - power = 0.6 - constant_coeff = 0.1 - - def powerlaw(): - n = 0 - while True: - yield constant_coeff * (n**power) - n += 1 - - return powerlaw() - - optimizer = GradientDescent(maxiter=20, learning_rate=learning_rate) - result = optimizer.minimize(self.objective, self.initial_point, self.grad) - - self.assertLess(result.fun, 1e-5) - - def test_no_start(self): - """Tests that making a step without having started the optimizer raises an error.""" - optimizer = GradientDescent() - with self.assertRaises(AttributeError): - optimizer.step() - - def test_start(self): - """Tests if the start method initializes the state properly.""" - optimizer = GradientDescent() - self.assertIsNone(optimizer.state) - self.assertIsNone(optimizer.perturbation) - optimizer.start(x0=self.initial_point, fun=self.objective) - - test_state = GradientDescentState( - x=self.initial_point, - fun=self.objective, - jac=None, - nfev=0, - njev=0, - nit=0, - learning_rate=1, - stepsize=None, - ) - - self.assertEqual(test_state, optimizer.state) - - def test_ask(self): - """Test the ask method.""" - optimizer = GradientDescent() - optimizer.start(fun=self.objective, x0=self.initial_point) - - ask_data = optimizer.ask() - np.testing.assert_equal(ask_data.x_jac, self.initial_point) - self.assertIsNone(ask_data.x_fun) - - def test_evaluate(self): - """Test the evaluate method.""" - optimizer = GradientDescent(perturbation=1e-10) - optimizer.start(fun=self.objective, x0=self.initial_point) - ask_data = AskData(x_jac=self.initial_point) - tell_data = optimizer.evaluate(ask_data=ask_data) - np.testing.assert_almost_equal(tell_data.eval_jac, self.grad(self.initial_point), decimal=2) - - def test_tell(self): - """Test the tell method.""" - optimizer = GradientDescent(learning_rate=1.0) - optimizer.start(fun=self.objective, x0=self.initial_point) - ask_data = AskData(x_jac=self.initial_point) - tell_data = TellData(eval_jac=self.initial_point) - optimizer.tell(ask_data=ask_data, tell_data=tell_data) - np.testing.assert_equal(optimizer.state.x, np.zeros(optimizer.state.x.shape)) - - def test_continue_condition(self): - """Test if the continue condition is working properly.""" - optimizer = GradientDescent(tol=1) - optimizer.start(fun=self.objective, x0=self.initial_point) - self.assertTrue(optimizer.continue_condition()) - optimizer.state.stepsize = 0.1 - self.assertFalse(optimizer.continue_condition()) - optimizer.state.stepsize = 10 - optimizer.state.nit = 1000 - self.assertFalse(optimizer.continue_condition()) - - def test_step(self): - """Tests if performing one step yields the desired result.""" - optimizer = GradientDescent(learning_rate=1.0) - optimizer.start(fun=self.objective, jac=self.grad, x0=self.initial_point) - optimizer.step() - np.testing.assert_almost_equal( - optimizer.state.x, self.initial_point - self.grad(self.initial_point), 6 - ) - - def test_wrong_dimension_gradient(self): - """Tests if an error is raised when a gradient of the wrong dimension is passed.""" - - optimizer = GradientDescent(learning_rate=1.0) - optimizer.start(fun=self.objective, x0=self.initial_point) - ask_data = AskData(x_jac=self.initial_point) - tell_data = TellData(eval_jac=np.array([1.0, 5])) - with self.assertRaises(ValueError): - optimizer.tell(ask_data=ask_data, tell_data=tell_data) - - tell_data = TellData(eval_jac=np.array(1)) - with self.assertRaises(ValueError): - optimizer.tell(ask_data=ask_data, tell_data=tell_data) diff --git a/test/python/algorithms/optimizers/test_optimizer_aqgd.py b/test/python/algorithms/optimizers/test_optimizer_aqgd.py deleted file mode 100644 index 9bef92b48cd4..000000000000 --- a/test/python/algorithms/optimizers/test_optimizer_aqgd.py +++ /dev/null @@ -1,122 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2019, 2023 -# -# 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 of AQGD optimizer""" - -import unittest -import warnings - -from test.python.algorithms import QiskitAlgorithmsTestCase -from qiskit.circuit.library import RealAmplitudes -from qiskit.utils import QuantumInstance, algorithm_globals, optionals -from qiskit.opflow import PauliSumOp -from qiskit.algorithms.optimizers import AQGD -from qiskit.algorithms import VQE, AlgorithmError -from qiskit.opflow.gradients import Gradient -from qiskit.test import slow_test - - -class TestOptimizerAQGD(QiskitAlgorithmsTestCase): - """Test AQGD optimizer using RY for analytic gradient with VQE""" - - def setUp(self): - super().setUp() - with warnings.catch_warnings(): - warnings.filterwarnings("ignore", category=DeprecationWarning) - algorithm_globals.random_seed = 50 - with self.assertWarns(DeprecationWarning): - self.qubit_op = PauliSumOp.from_list( - [ - ("II", -1.052373245772859), - ("IZ", 0.39793742484318045), - ("ZI", -0.39793742484318045), - ("ZZ", -0.01128010425623538), - ("XX", 0.18093119978423156), - ] - ) - - @slow_test - @unittest.skipUnless(optionals.HAS_AER, "qiskit-aer is required to run this test") - def test_simple(self): - """test AQGD optimizer with the parameters as single values.""" - from qiskit_aer import Aer - - with self.assertWarns(DeprecationWarning): - q_instance = QuantumInstance( - Aer.get_backend("aer_simulator_statevector"), - seed_simulator=algorithm_globals.random_seed, - seed_transpiler=algorithm_globals.random_seed, - ) - aqgd = AQGD(momentum=0.0) - - with self.assertWarns(DeprecationWarning): - vqe = VQE( - ansatz=RealAmplitudes(), - optimizer=aqgd, - gradient=Gradient("lin_comb"), - quantum_instance=q_instance, - ) - result = vqe.compute_minimum_eigenvalue(operator=self.qubit_op) - - self.assertAlmostEqual(result.eigenvalue.real, -1.857, places=3) - - @unittest.skipUnless(optionals.HAS_AER, "qiskit-aer is required to run this test") - def test_list(self): - """test AQGD optimizer with the parameters as lists.""" - from qiskit_aer import Aer - - with self.assertWarns(DeprecationWarning): - q_instance = QuantumInstance( - Aer.get_backend("aer_simulator_statevector"), - seed_simulator=algorithm_globals.random_seed, - seed_transpiler=algorithm_globals.random_seed, - ) - aqgd = AQGD(maxiter=[1000, 1000, 1000], eta=[1.0, 0.5, 0.3], momentum=[0.0, 0.5, 0.75]) - - with self.assertWarns(DeprecationWarning): - vqe = VQE(ansatz=RealAmplitudes(), optimizer=aqgd, quantum_instance=q_instance) - result = vqe.compute_minimum_eigenvalue(operator=self.qubit_op) - - self.assertAlmostEqual(result.eigenvalue.real, -1.857, places=3) - - def test_raises_exception(self): - """tests that AQGD raises an exception when incorrect values are passed.""" - self.assertRaises(AlgorithmError, AQGD, maxiter=[1000], eta=[1.0, 0.5], momentum=[0.0, 0.5]) - - @slow_test - @unittest.skipUnless(optionals.HAS_AER, "qiskit-aer is required to run this test") - def test_int_values(self): - """test AQGD with int values passed as eta and momentum.""" - from qiskit_aer import Aer - - with self.assertWarns(DeprecationWarning): - q_instance = QuantumInstance( - Aer.get_backend("aer_simulator_statevector"), - seed_simulator=algorithm_globals.random_seed, - seed_transpiler=algorithm_globals.random_seed, - ) - aqgd = AQGD(maxiter=1000, eta=1, momentum=0) - - with self.assertWarns(DeprecationWarning): - vqe = VQE( - ansatz=RealAmplitudes(), - optimizer=aqgd, - gradient=Gradient("lin_comb"), - quantum_instance=q_instance, - ) - result = vqe.compute_minimum_eigenvalue(operator=self.qubit_op) - - self.assertAlmostEqual(result.eigenvalue.real, -1.857, places=3) - - -if __name__ == "__main__": - unittest.main() diff --git a/test/python/algorithms/optimizers/test_optimizer_nft.py b/test/python/algorithms/optimizers/test_optimizer_nft.py deleted file mode 100644 index e904c3bffd91..000000000000 --- a/test/python/algorithms/optimizers/test_optimizer_nft.py +++ /dev/null @@ -1,64 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2020, 2023. -# -# 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 of NFT optimizer""" - -import unittest -import warnings - -from test.python.algorithms import QiskitAlgorithmsTestCase -from qiskit import BasicAer -from qiskit.circuit.library import RealAmplitudes -from qiskit.utils import QuantumInstance, algorithm_globals -from qiskit.opflow import PauliSumOp -from qiskit.algorithms.optimizers import NFT -from qiskit.algorithms import VQE - - -class TestOptimizerNFT(QiskitAlgorithmsTestCase): - """Test NFT optimizer using RY with VQE""" - - def setUp(self): - super().setUp() - with warnings.catch_warnings(): - warnings.filterwarnings("ignore", category=DeprecationWarning) - algorithm_globals.random_seed = 50 - with self.assertWarns(DeprecationWarning): - self.qubit_op = PauliSumOp.from_list( - [ - ("II", -1.052373245772859), - ("IZ", 0.39793742484318045), - ("ZI", -0.39793742484318045), - ("ZZ", -0.01128010425623538), - ("XX", 0.18093119978423156), - ] - ) - - def test_nft(self): - """Test NFT optimizer by using it""" - with self.assertWarns(DeprecationWarning): - vqe = VQE( - ansatz=RealAmplitudes(), - optimizer=NFT(), - quantum_instance=QuantumInstance( - BasicAer.get_backend("statevector_simulator"), - seed_simulator=algorithm_globals.random_seed, - seed_transpiler=algorithm_globals.random_seed, - ), - ) - result = vqe.compute_minimum_eigenvalue(operator=self.qubit_op) - - self.assertAlmostEqual(result.eigenvalue.real, -1.857275, places=6) - - -if __name__ == "__main__": - unittest.main() diff --git a/test/python/algorithms/optimizers/test_optimizers.py b/test/python/algorithms/optimizers/test_optimizers.py deleted file mode 100644 index f0e759ce4e4e..000000000000 --- a/test/python/algorithms/optimizers/test_optimizers.py +++ /dev/null @@ -1,404 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2018, 2021. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. - -"""Test Optimizers""" - -import unittest -import warnings - -from test.python.algorithms import QiskitAlgorithmsTestCase - -from typing import Optional, List, Tuple -from ddt import ddt, data, unpack -import numpy as np -from scipy.optimize import rosen, rosen_der - -from qiskit.algorithms.optimizers import ( - ADAM, - AQGD, - BOBYQA, - IMFIL, - CG, - CRS, - COBYLA, - DIRECT_L, - DIRECT_L_RAND, - GSLS, - GradientDescent, - L_BFGS_B, - NELDER_MEAD, - Optimizer, - P_BFGS, - POWELL, - SLSQP, - SPSA, - QNSPSA, - TNC, - SciPyOptimizer, -) -from qiskit.circuit.library import RealAmplitudes -from qiskit.exceptions import MissingOptionalLibraryError -from qiskit.utils import algorithm_globals, optionals - - -@ddt -class TestOptimizers(QiskitAlgorithmsTestCase): - """Test Optimizers""" - - def setUp(self): - super().setUp() - with warnings.catch_warnings(): - warnings.filterwarnings("ignore", category=DeprecationWarning) - algorithm_globals.random_seed = 52 - - def run_optimizer( - self, - optimizer: Optimizer, - max_nfev: int, - grad: bool = False, - bounds: Optional[List[Tuple[float, float]]] = None, - ): - """Test the optimizer. - - Args: - optimizer: The optimizer instance to test. - max_nfev: The maximal allowed number of function evaluations. - grad: Whether to pass the gradient function as input. - bounds: Optimizer bounds. - """ - x_0 = [1.3, 0.7, 0.8, 1.9, 1.2] - jac = rosen_der if grad else None - - res = optimizer.minimize(rosen, x_0, jac, bounds) - x_opt = res.x - nfev = res.nfev - - np.testing.assert_array_almost_equal(x_opt, [1.0] * len(x_0), decimal=2) - self.assertLessEqual(nfev, max_nfev) - - def test_adam(self): - """adam test""" - optimizer = ADAM(maxiter=10000, tol=1e-06) - self.run_optimizer(optimizer, max_nfev=10000) - - def test_cg(self): - """cg test""" - optimizer = CG(maxiter=1000, tol=1e-06) - self.run_optimizer(optimizer, max_nfev=10000) - - def test_gradient_descent(self): - """cg test""" - optimizer = GradientDescent(maxiter=100000, tol=1e-06, learning_rate=1e-3) - self.run_optimizer(optimizer, grad=True, max_nfev=100000) - - def test_cobyla(self): - """cobyla test""" - optimizer = COBYLA(maxiter=100000, tol=1e-06) - self.run_optimizer(optimizer, max_nfev=100000) - - def test_l_bfgs_b(self): - """l_bfgs_b test""" - optimizer = L_BFGS_B(maxfun=1000) - self.run_optimizer(optimizer, max_nfev=10000) - - def test_p_bfgs(self): - """parallel l_bfgs_b test""" - optimizer = P_BFGS(maxfun=1000, max_processes=4) - self.run_optimizer(optimizer, max_nfev=10000) - - def test_nelder_mead(self): - """nelder mead test""" - optimizer = NELDER_MEAD(maxfev=10000, tol=1e-06) - self.run_optimizer(optimizer, max_nfev=10000) - - def test_powell(self): - """powell test""" - optimizer = POWELL(maxfev=10000, tol=1e-06) - self.run_optimizer(optimizer, max_nfev=10000) - - def test_slsqp(self): - """slsqp test""" - optimizer = SLSQP(maxiter=1000, tol=1e-06) - self.run_optimizer(optimizer, max_nfev=10000) - - @unittest.skip("Skipping SPSA as it does not do well on non-convex rozen") - def test_spsa(self): - """spsa test""" - optimizer = SPSA(maxiter=10000) - self.run_optimizer(optimizer, max_nfev=100000) - - def test_tnc(self): - """tnc test""" - optimizer = TNC(maxiter=1000, tol=1e-06) - self.run_optimizer(optimizer, max_nfev=10000) - - def test_gsls(self): - """gsls test""" - optimizer = GSLS( - sample_size_factor=40, - sampling_radius=1.0e-12, - maxiter=10000, - max_eval=10000, - min_step_size=1.0e-12, - ) - x_0 = [1.3, 0.7, 0.8, 1.9, 1.2] - - with warnings.catch_warnings(): - warnings.filterwarnings("ignore", category=DeprecationWarning) - algorithm_globals.random_seed = 1 - res = optimizer.minimize(rosen, x_0) - x_value = res.fun - n_evals = res.nfev - - # Ensure value is near-optimal - self.assertLessEqual(x_value, 0.01) - self.assertLessEqual(n_evals, 10000) - - def test_scipy_optimizer(self): - """scipy_optimizer test""" - optimizer = SciPyOptimizer("BFGS", options={"maxiter": 1000}) - self.run_optimizer(optimizer, max_nfev=10000) - - def test_scipy_optimizer_callback(self): - """scipy_optimizer callback test""" - values = [] - - def callback(x): - values.append(x) - - optimizer = SciPyOptimizer("BFGS", options={"maxiter": 1000}, callback=callback) - self.run_optimizer(optimizer, max_nfev=10000) - self.assertTrue(values) # Check the list is nonempty. - - # ESCH and ISRES do not do well with rosen - @data( - (CRS, True), - (DIRECT_L, True), - (DIRECT_L_RAND, True), - (CRS, False), - (DIRECT_L, False), - (DIRECT_L_RAND, False), - ) - @unpack - def test_nlopt(self, optimizer_cls, use_bound): - """NLopt test""" - bounds = [(-6, 6)] * 5 if use_bound else None - try: - optimizer = optimizer_cls() - optimizer.set_options(**{"max_evals": 50000}) - self.run_optimizer(optimizer, max_nfev=50000, bounds=bounds) - except MissingOptionalLibraryError as ex: - self.skipTest(str(ex)) - - -@ddt -class TestOptimizerSerialization(QiskitAlgorithmsTestCase): - """Tests concerning the serialization of optimizers.""" - - @data( - ("BFGS", {"maxiter": 100, "eps": np.array([0.1])}), - ("CG", {"maxiter": 200, "gtol": 1e-8}), - ("COBYLA", {"maxiter": 10}), - ("L_BFGS_B", {"maxiter": 30}), - ("NELDER_MEAD", {"maxiter": 0}), - ("NFT", {"maxiter": 100}), - ("P_BFGS", {"maxiter": 5}), - ("POWELL", {"maxiter": 1}), - ("SLSQP", {"maxiter": 400}), - ("TNC", {"maxiter": 20}), - ("dogleg", {"maxiter": 100}), - ("trust-constr", {"maxiter": 10}), - ("trust-ncg", {"maxiter": 100}), - ("trust-exact", {"maxiter": 120}), - ("trust-krylov", {"maxiter": 150}), - ) - @unpack - def test_scipy(self, method, options): - """Test the SciPyOptimizer is serializable.""" - - optimizer = SciPyOptimizer(method, options=options) - serialized = optimizer.settings - from_dict = SciPyOptimizer(**serialized) - - self.assertEqual(from_dict._method, method.lower()) - self.assertEqual(from_dict._options, options) - - def test_independent_reconstruction(self): - """Test the SciPyOptimizers don't reset all settings upon creating a new instance. - - COBYLA is used as representative example here.""" - - kwargs = {"coffee": "without sugar"} - options = {"tea": "with milk"} - optimizer = COBYLA(maxiter=1, options=options, **kwargs) - serialized = optimizer.settings - from_dict = COBYLA(**serialized) - - with self.subTest(msg="test attributes"): - self.assertEqual(from_dict.settings["maxiter"], 1) - - with self.subTest(msg="test options"): - # options should only contain values that are *not* already in the initializer - # (e.g. should not contain maxiter) - self.assertEqual(from_dict.settings["options"], {"tea": "with milk"}) - - with self.subTest(msg="test kwargs"): - self.assertEqual(from_dict.settings["coffee"], "without sugar") - - with self.subTest(msg="option ids differ"): - self.assertNotEqual(id(serialized["options"]), id(from_dict.settings["options"])) - - def test_adam(self): - """Test ADAM is serializable.""" - - adam = ADAM(maxiter=100, amsgrad=True) - settings = adam.settings - - self.assertEqual(settings["maxiter"], 100) - self.assertTrue(settings["amsgrad"]) - - def test_aqgd(self): - """Test AQGD is serializable.""" - - opt = AQGD(maxiter=[200, 100], eta=[0.2, 0.1], momentum=[0.25, 0.1]) - settings = opt.settings - - self.assertListEqual(settings["maxiter"], [200, 100]) - self.assertListEqual(settings["eta"], [0.2, 0.1]) - self.assertListEqual(settings["momentum"], [0.25, 0.1]) - - @unittest.skipIf(not optionals.HAS_SKQUANT, "Install scikit-quant to run this test.") - def test_bobyqa(self): - """Test BOBYQA is serializable.""" - - opt = BOBYQA(maxiter=200) - settings = opt.settings - - self.assertEqual(settings["maxiter"], 200) - - @unittest.skipIf(not optionals.HAS_SKQUANT, "Install scikit-quant to run this test.") - def test_imfil(self): - """Test IMFIL is serializable.""" - - opt = IMFIL(maxiter=200) - settings = opt.settings - - self.assertEqual(settings["maxiter"], 200) - - def test_gradient_descent(self): - """Test GradientDescent is serializable.""" - - opt = GradientDescent(maxiter=10, learning_rate=0.01) - settings = opt.settings - - self.assertEqual(settings["maxiter"], 10) - self.assertEqual(settings["learning_rate"], 0.01) - - def test_gsls(self): - """Test GSLS is serializable.""" - - opt = GSLS(maxiter=100, sampling_radius=1e-3) - settings = opt.settings - - self.assertEqual(settings["maxiter"], 100) - self.assertEqual(settings["sampling_radius"], 1e-3) - - def test_spsa(self): - """Test SPSA optimizer is serializable.""" - options = { - "maxiter": 100, - "blocking": True, - "allowed_increase": 0.1, - "second_order": True, - "learning_rate": 0.02, - "perturbation": 0.05, - "regularization": 0.1, - "resamplings": 2, - "perturbation_dims": 5, - "trust_region": False, - "initial_hessian": None, - "lse_solver": None, - "hessian_delay": 0, - "callback": None, - "termination_checker": None, - } - spsa = SPSA(**options) - - self.assertDictEqual(spsa.settings, options) - - def test_spsa_custom_iterators(self): - """Test serialization works with custom iterators for learning rate and perturbation.""" - rate = 0.99 - - def powerlaw(): - n = 0 - while True: - yield rate**n - n += 1 - - def steps(): - n = 1 - divide_after = 20 - epsilon = 0.5 - while True: - yield epsilon - n += 1 - if n % divide_after == 0: - epsilon /= 2 - - learning_rate = powerlaw() - expected_learning_rate = np.array([next(learning_rate) for _ in range(200)]) - - perturbation = steps() - expected_perturbation = np.array([next(perturbation) for _ in range(200)]) - - spsa = SPSA(maxiter=200, learning_rate=powerlaw, perturbation=steps) - settings = spsa.settings - - self.assertTrue(np.allclose(settings["learning_rate"], expected_learning_rate)) - self.assertTrue(np.allclose(settings["perturbation"], expected_perturbation)) - - def test_qnspsa(self): - """Test QN-SPSA optimizer is serializable.""" - ansatz = RealAmplitudes(1) - fidelity = QNSPSA.get_fidelity(ansatz) - options = { - "fidelity": fidelity, - "maxiter": 100, - "blocking": True, - "allowed_increase": 0.1, - "learning_rate": 0.02, - "perturbation": 0.05, - "regularization": 0.1, - "resamplings": 2, - "perturbation_dims": 5, - "lse_solver": None, - "initial_hessian": None, - "callback": None, - "termination_checker": None, - "hessian_delay": 0, - } - spsa = QNSPSA(**options) - - settings = spsa.settings - expected = options.copy() - - with self.subTest(msg="check constructed dictionary"): - self.assertDictEqual(settings, expected) - - reconstructed = QNSPSA(**settings) # pylint: disable=unexpected-keyword-arg - with self.subTest(msg="test reconstructed optimizer"): - self.assertDictEqual(reconstructed.settings, expected) - - -if __name__ == "__main__": - unittest.main() diff --git a/test/python/algorithms/optimizers/test_optimizers_scikitquant.py b/test/python/algorithms/optimizers/test_optimizers_scikitquant.py deleted file mode 100644 index f3d3f645d895..000000000000 --- a/test/python/algorithms/optimizers/test_optimizers_scikitquant.py +++ /dev/null @@ -1,118 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2020, 2023. -# -# 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 of scikit-quant optimizers.""" - -import unittest -import warnings - -from test.python.algorithms import QiskitAlgorithmsTestCase - -from ddt import ddt, data, unpack - -import numpy -from qiskit import BasicAer -from qiskit.circuit.library import RealAmplitudes -from qiskit.utils import QuantumInstance, algorithm_globals -from qiskit.exceptions import MissingOptionalLibraryError -from qiskit.opflow import PauliSumOp -from qiskit.algorithms import VQE -from qiskit.algorithms.optimizers import BOBYQA, SNOBFIT, IMFIL - - -@ddt -class TestOptimizers(QiskitAlgorithmsTestCase): - """Test scikit-quant optimizers.""" - - def setUp(self): - """Set the problem.""" - super().setUp() - with warnings.catch_warnings(): - warnings.filterwarnings("ignore", category=DeprecationWarning) - algorithm_globals.random_seed = 50 - with self.assertWarns(DeprecationWarning): - self.qubit_op = PauliSumOp.from_list( - [ - ("II", -1.052373245772859), - ("IZ", 0.39793742484318045), - ("ZI", -0.39793742484318045), - ("ZZ", -0.01128010425623538), - ("XX", 0.18093119978423156), - ] - ) - - def _optimize(self, optimizer): - """launch vqe""" - with self.assertWarns(DeprecationWarning): - qe = QuantumInstance( - BasicAer.get_backend("statevector_simulator"), - seed_simulator=algorithm_globals.random_seed, - seed_transpiler=algorithm_globals.random_seed, - ) - with self.assertWarns(DeprecationWarning): - vqe = VQE(ansatz=RealAmplitudes(), optimizer=optimizer, quantum_instance=qe) - result = vqe.compute_minimum_eigenvalue(operator=self.qubit_op) - - self.assertAlmostEqual(result.eigenvalue.real, -1.857, places=1) - - def test_bobyqa(self): - """BOBYQA optimizer test.""" - try: - optimizer = BOBYQA(maxiter=150) - self._optimize(optimizer) - except MissingOptionalLibraryError as ex: - self.skipTest(str(ex)) - - @unittest.skipIf( - # NB: numpy.__version__ may contain letters, e.g. "1.26.0b1" - tuple(map(int, numpy.__version__.split(".")[:2])) >= (1, 24), - "scikit's SnobFit currently incompatible with NumPy 1.24.0.", - ) - def test_snobfit(self): - """SNOBFIT optimizer test.""" - try: - optimizer = SNOBFIT(maxiter=100, maxfail=100, maxmp=20) - self._optimize(optimizer) - except MissingOptionalLibraryError as ex: - self.skipTest(str(ex)) - - @unittest.skipIf( - # NB: numpy.__version__ may contain letters, e.g. "1.26.0b1" - tuple(map(int, numpy.__version__.split(".")[:2])) >= (1, 24), - "scikit's SnobFit currently incompatible with NumPy 1.24.0.", - ) - @data((None,), ([(-1, 1), (None, None)],)) - @unpack - def test_snobfit_missing_bounds(self, bounds): - """SNOBFIT optimizer test with missing bounds.""" - try: - optimizer = SNOBFIT() - with self.assertRaises(ValueError): - optimizer.minimize( - fun=lambda _: 1, # using dummy function (never called) - x0=[0.1, 0.1], # dummy initial point - bounds=bounds, - ) - except MissingOptionalLibraryError as ex: - self.skipTest(str(ex)) - - def test_imfil(self): - """IMFIL test.""" - try: - optimizer = IMFIL(maxiter=100) - self._optimize(optimizer) - except MissingOptionalLibraryError as ex: - self.skipTest(str(ex)) - - -if __name__ == "__main__": - unittest.main() diff --git a/test/python/algorithms/optimizers/test_spsa.py b/test/python/algorithms/optimizers/test_spsa.py deleted file mode 100644 index dafa8c44b435..000000000000 --- a/test/python/algorithms/optimizers/test_spsa.py +++ /dev/null @@ -1,293 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2021, 2023. -# -# 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. - -"""Tests for the SPSA optimizer.""" - -import warnings - -from test.python.algorithms import QiskitAlgorithmsTestCase -from ddt import ddt, data - -import numpy as np - -from qiskit.algorithms.optimizers import SPSA, QNSPSA -from qiskit.circuit.library import PauliTwoDesign -from qiskit.primitives import Estimator, Sampler -from qiskit.providers.basicaer import StatevectorSimulatorPy -from qiskit.opflow import I, Z, StateFn, MatrixExpectation -from qiskit.utils import algorithm_globals - - -@ddt -class TestSPSA(QiskitAlgorithmsTestCase): - """Tests for the SPSA optimizer.""" - - def setUp(self): - super().setUp() - np.random.seed(12) - with warnings.catch_warnings(): - warnings.filterwarnings("ignore", category=DeprecationWarning) - algorithm_globals.random_seed = 12 - - # @slow_test - @data("spsa", "2spsa", "qnspsa") - def test_pauli_two_design(self, method): - """Test SPSA on the Pauli two-design example.""" - circuit = PauliTwoDesign(3, reps=1, seed=1) - parameters = list(circuit.parameters) - with self.assertWarns(DeprecationWarning): - obs = Z ^ Z ^ I - expr = ~StateFn(obs) @ StateFn(circuit) - - initial_point = np.array( - [0.82311034, 0.02611798, 0.21077064, 0.61842177, 0.09828447, 0.62013131] - ) - - def objective(x): - return expr.bind_parameters(dict(zip(parameters, x))).eval().real - - settings = {"maxiter": 100, "blocking": True, "allowed_increase": 0} - - if method == "2spsa": - settings["second_order"] = True - settings["regularization"] = 0.01 - expected_nfev = settings["maxiter"] * 5 + 1 - elif method == "qnspsa": - settings["fidelity"] = QNSPSA.get_fidelity(circuit) - settings["regularization"] = 0.001 - settings["learning_rate"] = 0.05 - settings["perturbation"] = 0.05 - - expected_nfev = settings["maxiter"] * 7 + 1 - else: - expected_nfev = settings["maxiter"] * 3 + 1 - - if method == "qnspsa": - spsa = QNSPSA(**settings) - else: - spsa = SPSA(**settings) - - with self.assertWarns(DeprecationWarning): - result = spsa.optimize(circuit.num_parameters, objective, initial_point=initial_point) - - with self.subTest("check final accuracy"): - self.assertLess(result[1], -0.95) # final loss - - with self.subTest("check number of function calls"): - self.assertEqual(result[2], expected_nfev) # function evaluations - - def test_recalibrate_at_optimize(self): - """Test SPSA calibrates anew upon each optimization run, if no autocalibration is set.""" - - def objective(x): - return -(x**2) - - spsa = SPSA(maxiter=1) - _ = spsa.minimize(objective, x0=np.array([0.5])) - - self.assertIsNone(spsa.learning_rate) - self.assertIsNone(spsa.perturbation) - - def test_learning_rate_perturbation_as_iterators(self): - """Test the learning rate and perturbation can be callables returning iterators.""" - - def get_learning_rate(): - def learning_rate(): - x = 0.99 - while True: - x *= x - yield x - - return learning_rate - - def get_perturbation(): - def perturbation(): - x = 0.99 - while True: - x *= x - yield max(x, 0.01) - - return perturbation - - def objective(x): - return (np.linalg.norm(x) - 2) ** 2 - - spsa = SPSA(learning_rate=get_learning_rate(), perturbation=get_perturbation()) - result = spsa.minimize(objective, np.array([0.5, 0.5])) - - self.assertAlmostEqual(np.linalg.norm(result.x), 2, places=2) - - def test_learning_rate_perturbation_as_arrays(self): - """Test the learning rate and perturbation can be arrays.""" - - learning_rate = np.linspace(1, 0, num=100, endpoint=False) ** 2 - perturbation = 0.01 * np.ones(100) - - def objective(x): - return (np.linalg.norm(x) - 2) ** 2 - - spsa = SPSA(learning_rate=learning_rate, perturbation=perturbation) - result = spsa.minimize(objective, x0=np.array([0.5, 0.5])) - - self.assertAlmostEqual(np.linalg.norm(result.x), 2, places=2) - - def test_termination_checker(self): - """Test the termination_callback""" - - def objective(x): - return np.linalg.norm(x) + np.random.rand(1) - - class TerminationChecker: - """Example termination checker""" - - def __init__(self): - self.values = [] - - def __call__(self, nfev, point, fvalue, stepsize, accepted) -> bool: - self.values.append(fvalue) - - if len(self.values) > 10: - return True - return False - - maxiter = 400 - spsa = SPSA(maxiter=maxiter, termination_checker=TerminationChecker()) - result = spsa.minimize(objective, x0=[0.5, 0.5]) - - self.assertLess(result.nit, maxiter) - - def test_callback(self): - """Test using the callback.""" - - def objective(x): - return (np.linalg.norm(x) - 2) ** 2 - - history = {"nfevs": [], "points": [], "fvals": [], "updates": [], "accepted": []} - - def callback(nfev, point, fval, update, accepted): - history["nfevs"].append(nfev) - history["points"].append(point) - history["fvals"].append(fval) - history["updates"].append(update) - history["accepted"].append(accepted) - - maxiter = 10 - spsa = SPSA(maxiter=maxiter, learning_rate=0.01, perturbation=0.01, callback=callback) - _ = spsa.minimize(objective, x0=np.array([0.5, 0.5])) - - expected_types = [int, np.ndarray, float, float, bool] - for i, (key, values) in enumerate(history.items()): - self.assertTrue(all(isinstance(value, expected_types[i]) for value in values)) - self.assertEqual(len(history[key]), maxiter) - - @data(1, 2, 3, 4) - def test_estimate_stddev(self, max_evals_grouped): - """Test the estimate_stddev - See https://github.com/Qiskit/qiskit-nature/issues/797""" - - def objective(x): - if len(x.shape) == 2: - return np.array([sum(x_i) for x_i in x]) - return sum(x) - - point = np.ones(5) - result = SPSA.estimate_stddev(objective, point, avg=10, max_evals_grouped=max_evals_grouped) - self.assertAlmostEqual(result, 0) - - def test_qnspsa_fidelity_deprecation(self): - """Test using a backend and expectation converter in get_fidelity warns.""" - ansatz = PauliTwoDesign(2, reps=1, seed=2) - - with self.assertWarns(DeprecationWarning): - QNSPSA.get_fidelity(ansatz, backend=StatevectorSimulatorPy()) - with self.assertWarns(DeprecationWarning): - QNSPSA.get_fidelity(ansatz, expectation=MatrixExpectation()) - - # No warning when used correctly. - QNSPSA.get_fidelity(ansatz) - - def test_qnspsa_fidelity_primitives(self): - """Test the primitives can be used in get_fidelity.""" - ansatz = PauliTwoDesign(2, reps=1, seed=2) - initial_point = np.random.random(ansatz.num_parameters) - - with self.subTest(msg="pass as kwarg"): - fidelity = QNSPSA.get_fidelity(ansatz, sampler=Sampler()) - result = fidelity(initial_point, initial_point) - - self.assertAlmostEqual(result[0], 1) - - # this test can be removed once backend and expectation are removed - with self.subTest(msg="pass positionally"): - fidelity = QNSPSA.get_fidelity(ansatz, Sampler()) - result = fidelity(initial_point, initial_point) - - self.assertAlmostEqual(result[0], 1) - - def test_qnspsa_max_evals_grouped(self): - """Test using max_evals_grouped with QNSPSA.""" - circuit = PauliTwoDesign(3, reps=1, seed=1) - num_parameters = circuit.num_parameters - - with self.assertWarns(DeprecationWarning): - obs = Z ^ Z ^ I - - estimator = Estimator(options={"seed": 12}) - - initial_point = np.array( - [0.82311034, 0.02611798, 0.21077064, 0.61842177, 0.09828447, 0.62013131] - ) - - def objective(x): - x = np.reshape(x, (-1, num_parameters)).tolist() - n = len(x) - return estimator.run(n * [circuit], n * [obs.primitive], x).result().values.real - - fidelity = QNSPSA.get_fidelity(circuit) - optimizer = QNSPSA(fidelity) - optimizer.maxiter = 1 - optimizer.learning_rate = 0.05 - optimizer.perturbation = 0.05 - optimizer.set_max_evals_grouped(50) # greater than 1 - - result = optimizer.minimize(objective, initial_point) - - with self.subTest("check final accuracy"): - self.assertAlmostEqual(result.fun[0], 0.473, places=3) - - with self.subTest("check number of function calls"): - expected_nfev = 8 # 7 * maxiter + 1 - self.assertEqual(result.nfev, expected_nfev) - - def test_point_sample(self): - """Test point sample function in QNSPSA""" - - def fidelity(x, _y): - x = np.asarray(x) - return np.ones_like(x, dtype=float) # some float - - def objective(x): - return x - - def get_perturbation(): - def perturbation(): - while True: - yield 1 - - return perturbation - - qnspsa = QNSPSA(fidelity, maxiter=1, learning_rate=0.1, perturbation=get_perturbation()) - initial_point = 1.0 - result = qnspsa.minimize(objective, initial_point) - - expected_nfev = 8 # 7 * maxiter + 1 - self.assertEqual(result.nfev, expected_nfev) diff --git a/test/python/algorithms/optimizers/test_umda.py b/test/python/algorithms/optimizers/test_umda.py deleted file mode 100644 index 26952881de88..000000000000 --- a/test/python/algorithms/optimizers/test_umda.py +++ /dev/null @@ -1,98 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2021. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. - -"""Tests for the UMDA optimizer.""" - -import warnings - -from test.python.algorithms import QiskitAlgorithmsTestCase - -import numpy as np -from scipy.optimize import rosen - -from qiskit.algorithms.optimizers.umda import UMDA -from qiskit.utils import algorithm_globals - - -class TestUMDA(QiskitAlgorithmsTestCase): - """Tests for the UMDA optimizer.""" - - def test_get_set(self): - """Test if getters and setters work as expected""" - umda = UMDA(maxiter=1, size_gen=20) - umda.disp = True - umda.size_gen = 30 - umda.alpha = 0.6 - umda.maxiter = 100 - - self.assertTrue(umda.disp) - self.assertEqual(umda.size_gen, 30) - self.assertEqual(umda.alpha, 0.6) - self.assertEqual(umda.maxiter, 100) - - def test_settings(self): - """Test if the settings display works well""" - umda = UMDA(maxiter=1, size_gen=20) - umda.disp = True - umda.size_gen = 30 - umda.alpha = 0.6 - umda.maxiter = 100 - - set_ = { - "maxiter": 100, - "alpha": 0.6, - "size_gen": 30, - "callback": None, - } - - self.assertEqual(umda.settings, set_) - - def test_minimize(self): - """optimize function test""" - # UMDA is volatile so we need to set the seeds for the execution - with warnings.catch_warnings(): - warnings.filterwarnings("ignore", category=DeprecationWarning) - algorithm_globals.random_seed = 52 - - optimizer = UMDA(maxiter=1000, size_gen=100) - x_0 = [1.3, 0.7, 1.5] - res = optimizer.minimize(rosen, x_0) - - self.assertIsNotNone(res.fun) - self.assertEqual(len(res.x), len(x_0)) - np.testing.assert_array_almost_equal(res.x, [1.0] * len(x_0), decimal=2) - - def test_callback(self): - """Test the callback.""" - - def objective(x): - return np.linalg.norm(x) - 1 - - nfevs, parameters, fvals = [], [], [] - - def store_history(*args): - nfevs.append(args[0]) - parameters.append(args[1]) - fvals.append(args[2]) - - optimizer = UMDA(maxiter=1, callback=store_history) - _ = optimizer.minimize(objective, x0=np.arange(5)) - - self.assertEqual(len(nfevs), 1) - self.assertIsInstance(nfevs[0], int) - - self.assertEqual(len(parameters), 1) - self.assertIsInstance(parameters[0], np.ndarray) - self.assertEqual(parameters[0].size, 5) - - self.assertEqual(len(fvals), 1) - self.assertIsInstance(fvals[0], float) diff --git a/test/python/algorithms/optimizers/utils/__init__.py b/test/python/algorithms/optimizers/utils/__init__.py deleted file mode 100644 index f3adc3e3b4da..000000000000 --- a/test/python/algorithms/optimizers/utils/__init__.py +++ /dev/null @@ -1,12 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2022. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. -"""Tests for Optimizer Utils.""" diff --git a/test/python/algorithms/optimizers/utils/test_learning_rate.py b/test/python/algorithms/optimizers/utils/test_learning_rate.py deleted file mode 100644 index 52acdbf98aaa..000000000000 --- a/test/python/algorithms/optimizers/utils/test_learning_rate.py +++ /dev/null @@ -1,54 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2022. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. - -"""Tests for LearningRate.""" - -from test.python.algorithms import QiskitAlgorithmsTestCase -import numpy as np -from qiskit.algorithms.optimizers.optimizer_utils import LearningRate - - -class TestLearningRate(QiskitAlgorithmsTestCase): - """Tests for the LearningRate class.""" - - def setUp(self): - super().setUp() - np.random.seed(12) - self.initial_point = np.array([1, 1, 1, 1, 0]) - - def objective(self, x): - """Objective Function for the tests""" - return (np.linalg.norm(x) - 1) ** 2 - - def test_learning_rate(self): - """ - Tests if the learning rate is initialized properly for each kind of input: - float, list and iterator. - """ - constant_learning_rate_input = 0.01 - list_learning_rate_input = [0.01 * n for n in range(10)] - generator_learning_rate_input = lambda: (el for el in list_learning_rate_input) - - with self.subTest("Check constant learning rate."): - constant_learning_rate = LearningRate(learning_rate=constant_learning_rate_input) - for _ in range(5): - self.assertEqual(constant_learning_rate_input, next(constant_learning_rate)) - - with self.subTest("Check learning rate list."): - list_learning_rate = LearningRate(learning_rate=list_learning_rate_input) - for i in range(5): - self.assertEqual(list_learning_rate_input[i], next(list_learning_rate)) - - with self.subTest("Check learning rate generator."): - generator_learning_rate = LearningRate(generator_learning_rate_input) - for i in range(5): - self.assertEqual(list_learning_rate_input[i], next(generator_learning_rate)) diff --git a/test/python/algorithms/state_fidelities/__init__.py b/test/python/algorithms/state_fidelities/__init__.py deleted file mode 100644 index d8b7d587c4cc..000000000000 --- a/test/python/algorithms/state_fidelities/__init__.py +++ /dev/null @@ -1,13 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2022. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. - -"""Tests for the primitive-based fidelity interfaces.""" diff --git a/test/python/algorithms/state_fidelities/test_compute_uncompute.py b/test/python/algorithms/state_fidelities/test_compute_uncompute.py deleted file mode 100644 index f3b106d9b5d7..000000000000 --- a/test/python/algorithms/state_fidelities/test_compute_uncompute.py +++ /dev/null @@ -1,264 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2022, 2023. -# -# 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. - -"""Tests for Fidelity.""" - -import unittest - -import numpy as np - -from qiskit.circuit import QuantumCircuit, ParameterVector -from qiskit.circuit.library import RealAmplitudes -from qiskit.primitives import Sampler -from qiskit.algorithms.state_fidelities import ComputeUncompute -from qiskit.test import QiskitTestCase - - -class TestComputeUncompute(QiskitTestCase): - """Test Compute-Uncompute Fidelity class""" - - def setUp(self): - super().setUp() - parameters = ParameterVector("x", 2) - - rx_rotations = QuantumCircuit(2) - rx_rotations.rx(parameters[0], 0) - rx_rotations.rx(parameters[1], 1) - - ry_rotations = QuantumCircuit(2) - ry_rotations.ry(parameters[0], 0) - ry_rotations.ry(parameters[1], 1) - - plus = QuantumCircuit(2) - plus.h([0, 1]) - - zero = QuantumCircuit(2) - - rx_rotation = QuantumCircuit(2) - rx_rotation.rx(parameters[0], 0) - rx_rotation.h(1) - - self._circuit = [rx_rotations, ry_rotations, plus, zero, rx_rotation] - self._sampler = Sampler() - self._left_params = np.array([[0, 0], [np.pi / 2, 0], [0, np.pi / 2], [np.pi, np.pi]]) - self._right_params = np.array([[0, 0], [0, 0], [np.pi / 2, 0], [0, 0]]) - - def test_1param_pair(self): - """test for fidelity with one pair of parameters""" - fidelity = ComputeUncompute(self._sampler) - job = fidelity.run( - self._circuit[0], self._circuit[1], self._left_params[0], self._right_params[0] - ) - result = job.result() - np.testing.assert_allclose(result.fidelities, np.array([1.0])) - - def test_1param_pair_local(self): - """test for fidelity with one pair of parameters""" - fidelity = ComputeUncompute(self._sampler, local=True) - job = fidelity.run( - self._circuit[0], self._circuit[1], self._left_params[0], self._right_params[0] - ) - result = job.result() - np.testing.assert_allclose(result.fidelities, np.array([1.0])) - - def test_local(self): - """test difference between local and global fidelity""" - fidelity_global = ComputeUncompute(self._sampler, local=False) - fidelity_local = ComputeUncompute(self._sampler, local=True) - fidelities = [] - for fidelity in [fidelity_global, fidelity_local]: - job = fidelity.run(self._circuit[2], self._circuit[3]) - result = job.result() - fidelities.append(result.fidelities[0]) - np.testing.assert_allclose(fidelities, np.array([0.25, 0.5]), atol=1e-16) - - def test_4param_pairs(self): - """test for fidelity with four pairs of parameters""" - fidelity = ComputeUncompute(self._sampler) - n = len(self._left_params) - job = fidelity.run( - [self._circuit[0]] * n, [self._circuit[1]] * n, self._left_params, self._right_params - ) - results = job.result() - np.testing.assert_allclose(results.fidelities, np.array([1.0, 0.5, 0.25, 0.0]), atol=1e-16) - - def test_symmetry(self): - """test for fidelity with the same circuit""" - fidelity = ComputeUncompute(self._sampler) - n = len(self._left_params) - job_1 = fidelity.run( - [self._circuit[0]] * n, [self._circuit[0]] * n, self._left_params, self._right_params - ) - job_2 = fidelity.run( - [self._circuit[0]] * n, [self._circuit[0]] * n, self._right_params, self._left_params - ) - results_1 = job_1.result() - results_2 = job_2.result() - np.testing.assert_allclose(results_1.fidelities, results_2.fidelities, atol=1e-16) - - def test_no_params(self): - """test for fidelity without parameters""" - fidelity = ComputeUncompute(self._sampler) - job = fidelity.run([self._circuit[2]], [self._circuit[3]]) - results = job.result() - np.testing.assert_allclose(results.fidelities, np.array([0.25]), atol=1e-16) - - job = fidelity.run([self._circuit[2]], [self._circuit[3]], [], []) - results = job.result() - np.testing.assert_allclose(results.fidelities, np.array([0.25]), atol=1e-16) - - def test_left_param(self): - """test for fidelity with only left parameters""" - fidelity = ComputeUncompute(self._sampler) - n = len(self._left_params) - job = fidelity.run( - [self._circuit[1]] * n, [self._circuit[3]] * n, values_1=self._left_params - ) - results = job.result() - np.testing.assert_allclose(results.fidelities, np.array([1.0, 0.5, 0.5, 0.0]), atol=1e-16) - - def test_right_param(self): - """test for fidelity with only right parameters""" - fidelity = ComputeUncompute(self._sampler) - n = len(self._left_params) - job = fidelity.run( - [self._circuit[3]] * n, [self._circuit[1]] * n, values_2=self._left_params - ) - results = job.result() - np.testing.assert_allclose(results.fidelities, np.array([1.0, 0.5, 0.5, 0.0]), atol=1e-16) - - def test_not_set_circuits(self): - """test for fidelity with no circuits.""" - fidelity = ComputeUncompute(self._sampler) - with self.assertRaises(TypeError): - job = fidelity.run( - circuits_1=None, - circuits_2=None, - values_1=self._left_params, - values_2=self._right_params, - ) - job.result() - - def test_circuit_mismatch(self): - """test for fidelity with different number of left/right circuits.""" - fidelity = ComputeUncompute(self._sampler) - n = len(self._left_params) - with self.assertRaises(ValueError): - job = fidelity.run( - [self._circuit[0]] * n, - [self._circuit[1]] * (n + 1), - self._left_params, - self._right_params, - ) - job.result() - - def test_asymmetric_params(self): - """test for fidelity when the 2 circuits have different number of - left/right parameters.""" - - fidelity = ComputeUncompute(self._sampler) - n = len(self._left_params) - right_params = [[p] for p in self._right_params[:, 0]] - job = fidelity.run( - [self._circuit[0]] * n, [self._circuit[4]] * n, self._left_params, right_params - ) - result = job.result() - np.testing.assert_allclose(result.fidelities, np.array([0.5, 0.25, 0.25, 0.0]), atol=1e-16) - - def test_input_format(self): - """test for different input format variations""" - - fidelity = ComputeUncompute(self._sampler) - circuit = RealAmplitudes(2) - values = np.random.random(circuit.num_parameters) - shift = np.ones_like(values) * 0.01 - - # lists of circuits, lists of numpy arrays - job = fidelity.run([circuit], [circuit], [values], [values + shift]) - result_1 = job.result() - - # lists of circuits, lists of lists - shift_val = values + shift - job = fidelity.run([circuit], [circuit], [values.tolist()], [shift_val.tolist()]) - result_2 = job.result() - - # circuits, lists - shift_val = values + shift - job = fidelity.run(circuit, circuit, values.tolist(), shift_val.tolist()) - result_3 = job.result() - - # circuits, np.arrays - job = fidelity.run(circuit, circuit, values, values + shift) - result_4 = job.result() - - np.testing.assert_allclose(result_1.fidelities, result_2.fidelities, atol=1e-16) - np.testing.assert_allclose(result_1.fidelities, result_3.fidelities, atol=1e-16) - np.testing.assert_allclose(result_1.fidelities, result_4.fidelities, atol=1e-16) - - def test_input_measurements(self): - """test for fidelity with measurements on input circuits""" - fidelity = ComputeUncompute(self._sampler) - circuit_1 = self._circuit[0] - circuit_1.measure_all() - circuit_2 = self._circuit[1] - circuit_2.measure_all() - - job = fidelity.run(circuit_1, circuit_2, self._left_params[0], self._right_params[0]) - result = job.result() - np.testing.assert_allclose(result.fidelities, np.array([1.0])) - - def test_options(self): - """Test fidelity's run options""" - sampler_shots = Sampler(options={"shots": 1024}) - - with self.subTest("sampler"): - # Only options in sampler - fidelity = ComputeUncompute(sampler_shots) - options = fidelity.options - job = fidelity.run(self._circuit[2], self._circuit[3]) - result = job.result() - self.assertEqual(options.__dict__, {"shots": 1024}) - self.assertEqual(result.options.__dict__, {"shots": 1024}) - - with self.subTest("fidelity init"): - # Fidelity default options override sampler - # options and add new fields - fidelity = ComputeUncompute(sampler_shots, options={"shots": 2048, "dummy": 100}) - options = fidelity.options - job = fidelity.run(self._circuit[2], self._circuit[3]) - result = job.result() - self.assertEqual(options.__dict__, {"shots": 2048, "dummy": 100}) - self.assertEqual(result.options.__dict__, {"shots": 2048, "dummy": 100}) - - with self.subTest("fidelity update"): - # Update fidelity options - fidelity = ComputeUncompute(sampler_shots, options={"shots": 2048, "dummy": 100}) - fidelity.update_default_options(shots=100) - options = fidelity.options - job = fidelity.run(self._circuit[2], self._circuit[3]) - result = job.result() - self.assertEqual(options.__dict__, {"shots": 100, "dummy": 100}) - self.assertEqual(result.options.__dict__, {"shots": 100, "dummy": 100}) - - with self.subTest("fidelity run"): - # Run options override fidelity options - fidelity = ComputeUncompute(sampler_shots, options={"shots": 2048, "dummy": 100}) - job = fidelity.run(self._circuit[2], self._circuit[3], shots=50, dummy=None) - options = fidelity.options - result = job.result() - # Only default + sampler options. Not run. - self.assertEqual(options.__dict__, {"shots": 2048, "dummy": 100}) - self.assertEqual(result.options.__dict__, {"shots": 50, "dummy": None}) - - -if __name__ == "__main__": - unittest.main() diff --git a/test/python/algorithms/test_amplitude_estimators.py b/test/python/algorithms/test_amplitude_estimators.py deleted file mode 100644 index ea0a1af099ee..000000000000 --- a/test/python/algorithms/test_amplitude_estimators.py +++ /dev/null @@ -1,724 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2018, 2023. -# -# 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 quantum amplitude estimation algorithm.""" - -import unittest -from test.python.algorithms import QiskitAlgorithmsTestCase -import numpy as np -from ddt import ddt, idata, data, unpack -from qiskit import QuantumRegister, QuantumCircuit, BasicAer -from qiskit.circuit.library import QFT, GroverOperator -from qiskit.utils import QuantumInstance -from qiskit.algorithms import ( - AmplitudeEstimation, - MaximumLikelihoodAmplitudeEstimation, - IterativeAmplitudeEstimation, - FasterAmplitudeEstimation, - EstimationProblem, -) -from qiskit.quantum_info import Operator, Statevector -from qiskit.primitives import Sampler - - -class BernoulliStateIn(QuantumCircuit): - """A circuit preparing sqrt(1 - p)|0> + sqrt(p)|1>.""" - - def __init__(self, probability): - super().__init__(1) - angle = 2 * np.arcsin(np.sqrt(probability)) - self.ry(angle, 0) - - -class BernoulliGrover(QuantumCircuit): - """The Grover operator corresponding to the Bernoulli A operator.""" - - def __init__(self, probability): - super().__init__(1, global_phase=np.pi) - self.angle = 2 * np.arcsin(np.sqrt(probability)) - self.ry(2 * self.angle, 0) - - def power(self, power, matrix_power=False): - if matrix_power: - return super().power(power, True) - - powered = QuantumCircuit(1) - powered.ry(power * 2 * self.angle, 0) - return powered - - -class SineIntegral(QuantumCircuit): - r"""Construct the A operator to approximate the integral - - \int_0^1 \sin^2(x) d x - - with a specified number of qubits. - """ - - def __init__(self, num_qubits): - qr_state = QuantumRegister(num_qubits, "state") - qr_objective = QuantumRegister(1, "obj") - super().__init__(qr_state, qr_objective) - - # prepare 1/sqrt{2^n} sum_x |x>_n - self.h(qr_state) - - # apply the sine/cosine term - self.ry(2 * 1 / 2 / 2**num_qubits, qr_objective[0]) - for i, qubit in enumerate(qr_state): - self.cry(2 * 2**i / 2**num_qubits, qubit, qr_objective[0]) - - -@ddt -class TestBernoulli(QiskitAlgorithmsTestCase): - """Tests based on the Bernoulli A operator. - - This class tests - * the estimation result - * the constructed circuits - """ - - def setUp(self): - super().setUp() - - with self.assertWarns(DeprecationWarning): - self._statevector = QuantumInstance( - backend=BasicAer.get_backend("statevector_simulator"), - seed_simulator=2, - seed_transpiler=2, - ) - - self._sampler = Sampler(options={"seed": 2}) - - def qasm(shots=100): - with self.assertWarns(DeprecationWarning): - qi = QuantumInstance( - backend=BasicAer.get_backend("qasm_simulator"), - shots=shots, - seed_simulator=2, - seed_transpiler=2, - ) - return qi - - self._qasm = qasm - - def sampler_shots(shots=100): - return Sampler(options={"shots": shots, "seed": 2}) - - self._sampler_shots = sampler_shots - - @idata( - [ - [0.2, AmplitudeEstimation(2), {"estimation": 0.5, "mle": 0.2}], - [0.49, AmplitudeEstimation(3), {"estimation": 0.5, "mle": 0.49}], - [0.2, MaximumLikelihoodAmplitudeEstimation([0, 1, 2]), {"estimation": 0.2}], - [0.49, MaximumLikelihoodAmplitudeEstimation(3), {"estimation": 0.49}], - [0.2, IterativeAmplitudeEstimation(0.1, 0.1), {"estimation": 0.2}], - [0.49, IterativeAmplitudeEstimation(0.001, 0.01), {"estimation": 0.49}], - [0.2, FasterAmplitudeEstimation(0.1, 3, rescale=False), {"estimation": 0.2}], - [0.12, FasterAmplitudeEstimation(0.1, 2, rescale=False), {"estimation": 0.12}], - ] - ) - @unpack - def test_statevector(self, prob, qae, expect): - """statevector test""" - - problem = EstimationProblem(BernoulliStateIn(prob), 0, BernoulliGrover(prob)) - - with self.assertWarns(DeprecationWarning): - qae.quantum_instance = self._statevector - result = qae.estimate(problem) - - self._statevector.reset_execution_results() - for key, value in expect.items(): - self.assertAlmostEqual( - value, getattr(result, key), places=3, msg=f"estimate `{key}` failed" - ) - - @idata( - [ - [0.2, AmplitudeEstimation(2), {"estimation": 0.5, "mle": 0.2}], - [0.49, AmplitudeEstimation(3), {"estimation": 0.5, "mle": 0.49}], - [0.2, MaximumLikelihoodAmplitudeEstimation([0, 1, 2]), {"estimation": 0.2}], - [0.49, MaximumLikelihoodAmplitudeEstimation(3), {"estimation": 0.49}], - [0.2, IterativeAmplitudeEstimation(0.1, 0.1), {"estimation": 0.2}], - [0.49, IterativeAmplitudeEstimation(0.001, 0.01), {"estimation": 0.49}], - [0.2, FasterAmplitudeEstimation(0.1, 3, rescale=False), {"estimation": 0.199}], - [0.12, FasterAmplitudeEstimation(0.1, 2, rescale=False), {"estimation": 0.12}], - ] - ) - @unpack - def test_sampler(self, prob, qae, expect): - """sampler test""" - qae.sampler = self._sampler - problem = EstimationProblem(BernoulliStateIn(prob), 0, BernoulliGrover(prob)) - - result = qae.estimate(problem) - for key, value in expect.items(): - self.assertAlmostEqual( - value, getattr(result, key), places=3, msg=f"estimate `{key}` failed" - ) - - @idata( - [ - [0.2, 100, AmplitudeEstimation(4), {"estimation": 0.14644, "mle": 0.193888}], - [0.0, 1000, AmplitudeEstimation(2), {"estimation": 0.0, "mle": 0.0}], - [ - 0.2, - 100, - MaximumLikelihoodAmplitudeEstimation([0, 1, 2, 4, 8]), - {"estimation": 0.199606}, - ], - [0.8, 10, IterativeAmplitudeEstimation(0.1, 0.05), {"estimation": 0.811711}], - [0.2, 1000, FasterAmplitudeEstimation(0.1, 3, rescale=False), {"estimation": 0.198640}], - [ - 0.12, - 100, - FasterAmplitudeEstimation(0.01, 3, rescale=False), - {"estimation": 0.119037}, - ], - ] - ) - @unpack - def test_qasm(self, prob, shots, qae, expect): - """qasm test""" - - with self.assertWarns(DeprecationWarning): - qae.quantum_instance = self._qasm(shots) - problem = EstimationProblem(BernoulliStateIn(prob), [0], BernoulliGrover(prob)) - result = qae.estimate(problem) - - for key, value in expect.items(): - self.assertAlmostEqual( - value, getattr(result, key), places=3, msg=f"estimate `{key}` failed" - ) - - @idata( - [ - [0.2, 100, AmplitudeEstimation(4), {"estimation": 0.14644, "mle": 0.198783}], - [0.0, 1000, AmplitudeEstimation(2), {"estimation": 0.0, "mle": 0.0}], - [ - 0.2, - 100, - MaximumLikelihoodAmplitudeEstimation([0, 1, 2, 4, 8]), - {"estimation": 0.200308}, - ], - [0.8, 10, IterativeAmplitudeEstimation(0.1, 0.05), {"estimation": 0.811711}], - [0.2, 1000, FasterAmplitudeEstimation(0.1, 3, rescale=False), {"estimation": 0.198640}], - [ - 0.12, - 100, - FasterAmplitudeEstimation(0.01, 3, rescale=False), - {"estimation": 0.120017}, - ], - ] - ) - @unpack - def test_sampler_with_shots(self, prob, shots, qae, expect): - """sampler with shots test""" - qae.sampler = self._sampler_shots(shots) - problem = EstimationProblem(BernoulliStateIn(prob), [0], BernoulliGrover(prob)) - - result = qae.estimate(problem) - for key, value in expect.items(): - self.assertAlmostEqual( - value, getattr(result, key), places=3, msg=f"estimate `{key}` failed" - ) - - @data(True, False) - def test_qae_circuit(self, efficient_circuit): - """Test circuits resulting from canonical amplitude estimation. - - Build the circuit manually and from the algorithm and compare the resulting unitaries. - """ - prob = 0.5 - - problem = EstimationProblem(BernoulliStateIn(prob), objective_qubits=[0]) - for m in [2, 5]: - qae = AmplitudeEstimation(m) - angle = 2 * np.arcsin(np.sqrt(prob)) - - # manually set up the inefficient AE circuit - qr_eval = QuantumRegister(m, "a") - qr_objective = QuantumRegister(1, "q") - circuit = QuantumCircuit(qr_eval, qr_objective) - - # initial Hadamard gates - for i in range(m): - circuit.h(qr_eval[i]) - - # A operator - circuit.ry(angle, qr_objective) - - if efficient_circuit: - qae.grover_operator = BernoulliGrover(prob) - for power in range(m): - circuit.cry(2 * 2**power * angle, qr_eval[power], qr_objective[0]) - else: - oracle = QuantumCircuit(1) - oracle.z(0) - - state_preparation = QuantumCircuit(1) - state_preparation.ry(angle, 0) - grover_op = GroverOperator(oracle, state_preparation) - for power in range(m): - circuit.compose( - grover_op.power(2**power).control(), - qubits=[qr_eval[power], qr_objective[0]], - inplace=True, - ) - - # fourier transform - iqft = QFT(m, do_swaps=False).inverse().reverse_bits() - circuit.append(iqft.to_instruction(), qr_eval) - - actual_circuit = qae.construct_circuit(problem, measurement=False) - - self.assertEqual(Operator(circuit), Operator(actual_circuit)) - - @data(True, False) - def test_iqae_circuits(self, efficient_circuit): - """Test circuits resulting from iterative amplitude estimation. - - Build the circuit manually and from the algorithm and compare the resulting unitaries. - """ - prob = 0.5 - problem = EstimationProblem(BernoulliStateIn(prob), objective_qubits=[0]) - - for k in [2, 5]: - qae = IterativeAmplitudeEstimation(0.01, 0.05) - angle = 2 * np.arcsin(np.sqrt(prob)) - - # manually set up the inefficient AE circuit - q_objective = QuantumRegister(1, "q") - circuit = QuantumCircuit(q_objective) - - # A operator - circuit.ry(angle, q_objective) - - if efficient_circuit: - qae.grover_operator = BernoulliGrover(prob) - circuit.ry(2 * k * angle, q_objective[0]) - - else: - oracle = QuantumCircuit(1) - oracle.z(0) - state_preparation = QuantumCircuit(1) - state_preparation.ry(angle, 0) - grover_op = GroverOperator(oracle, state_preparation) - for _ in range(k): - circuit.compose(grover_op, inplace=True) - - actual_circuit = qae.construct_circuit(problem, k, measurement=False) - self.assertEqual(Operator(circuit), Operator(actual_circuit)) - - @data(True, False) - def test_mlae_circuits(self, efficient_circuit): - """Test the circuits constructed for MLAE""" - prob = 0.5 - problem = EstimationProblem(BernoulliStateIn(prob), objective_qubits=[0]) - - for k in [2, 5]: - qae = MaximumLikelihoodAmplitudeEstimation(k) - angle = 2 * np.arcsin(np.sqrt(prob)) - - # compute all the circuits used for MLAE - circuits = [] - - # 0th power - q_objective = QuantumRegister(1, "q") - circuit = QuantumCircuit(q_objective) - circuit.ry(angle, q_objective) - circuits += [circuit] - - # powers of 2 - for power in range(k): - q_objective = QuantumRegister(1, "q") - circuit = QuantumCircuit(q_objective) - - # A operator - circuit.ry(angle, q_objective) - - # Q^(2^j) operator - if efficient_circuit: - qae.grover_operator = BernoulliGrover(prob) - circuit.ry(2 * 2**power * angle, q_objective[0]) - - else: - oracle = QuantumCircuit(1) - oracle.z(0) - state_preparation = QuantumCircuit(1) - state_preparation.ry(angle, 0) - grover_op = GroverOperator(oracle, state_preparation) - for _ in range(2**power): - circuit.compose(grover_op, inplace=True) - circuits += [circuit] - - actual_circuits = qae.construct_circuits(problem, measurement=False) - - for actual, expected in zip(actual_circuits, circuits): - self.assertEqual(Operator(actual), Operator(expected)) - - -@ddt -class TestSineIntegral(QiskitAlgorithmsTestCase): - """Tests based on the A operator to integrate sin^2(x). - - This class tests - * the estimation result - * the confidence intervals - """ - - def setUp(self): - super().setUp() - - with self.assertWarns(DeprecationWarning): - self._statevector = QuantumInstance( - backend=BasicAer.get_backend("statevector_simulator"), - seed_simulator=123, - seed_transpiler=41, - ) - - self._sampler = Sampler(options={"seed": 123}) - - def qasm(shots=100): - with self.assertWarns(DeprecationWarning): - qi = QuantumInstance( - backend=BasicAer.get_backend("qasm_simulator"), - shots=shots, - seed_simulator=7192, - seed_transpiler=90000, - ) - return qi - - self._qasm = qasm - - def sampler_shots(shots=100): - return Sampler(options={"shots": shots, "seed": 7192}) - - self._sampler_shots = sampler_shots - - @idata( - [ - [2, AmplitudeEstimation(2), {"estimation": 0.5, "mle": 0.270290}], - [4, MaximumLikelihoodAmplitudeEstimation(4), {"estimation": 0.272675}], - [3, IterativeAmplitudeEstimation(0.1, 0.1), {"estimation": 0.272082}], - [3, FasterAmplitudeEstimation(0.01, 1), {"estimation": 0.272082}], - ] - ) - @unpack - def test_statevector(self, n, qae, expect): - """Statevector end-to-end test""" - # construct factories for A and Q - # qae.state_preparation = SineIntegral(n) - estimation_problem = EstimationProblem(SineIntegral(n), objective_qubits=[n]) - - with self.assertWarns(DeprecationWarning): - qae.quantum_instance = self._statevector - # result = qae.run(self._statevector) - result = qae.estimate(estimation_problem) - - self._statevector.reset_execution_results() - for key, value in expect.items(): - self.assertAlmostEqual( - value, getattr(result, key), places=3, msg=f"estimate `{key}` failed" - ) - - @idata( - [ - [2, AmplitudeEstimation(2), {"estimation": 0.5, "mle": 0.2702}], - [4, MaximumLikelihoodAmplitudeEstimation(4), {"estimation": 0.2725}], - [3, IterativeAmplitudeEstimation(0.1, 0.1), {"estimation": 0.2721}], - [3, FasterAmplitudeEstimation(0.01, 1), {"estimation": 0.2792}], - ] - ) - @unpack - def test_sampler(self, n, qae, expect): - """sampler end-to-end test""" - # construct factories for A and Q - # qae.state_preparation = SineIntegral(n) - qae.sampler = self._sampler - estimation_problem = EstimationProblem(SineIntegral(n), objective_qubits=[n]) - - result = qae.estimate(estimation_problem) - for key, value in expect.items(): - self.assertAlmostEqual( - value, getattr(result, key), places=3, msg=f"estimate `{key}` failed" - ) - - @idata( - [ - [4, 100, AmplitudeEstimation(2), {"estimation": 0.5, "mle": 0.281196}], - [3, 10, MaximumLikelihoodAmplitudeEstimation(2), {"estimation": 0.256878}], - [3, 1000, IterativeAmplitudeEstimation(0.01, 0.01), {"estimation": 0.271790}], - [3, 1000, FasterAmplitudeEstimation(0.1, 4), {"estimation": 0.274168}], - ] - ) - @unpack - def test_qasm(self, n, shots, qae, expect): - """QASM simulator end-to-end test.""" - estimation_problem = EstimationProblem(SineIntegral(n), objective_qubits=[n]) - - with self.assertWarns(DeprecationWarning): - qae.quantum_instance = self._qasm(shots) - result = qae.estimate(estimation_problem) - - for key, value in expect.items(): - self.assertAlmostEqual( - value, getattr(result, key), places=3, msg=f"estimate `{key}` failed" - ) - - @idata( - [ - [4, 1000, AmplitudeEstimation(2), {"estimation": 0.5, "mle": 0.2636}], - [3, 10, MaximumLikelihoodAmplitudeEstimation(2), {"estimation": 0.2904}], - [3, 1000, IterativeAmplitudeEstimation(0.01, 0.01), {"estimation": 0.2706}], - [3, 1000, FasterAmplitudeEstimation(0.1, 4), {"estimation": 0.2764}], - ] - ) - @unpack - def test_sampler_with_shots(self, n, shots, qae, expect): - """Sampler with shots end-to-end test.""" - # construct factories for A and Q - qae.sampler = self._sampler_shots(shots) - estimation_problem = EstimationProblem(SineIntegral(n), objective_qubits=[n]) - - result = qae.estimate(estimation_problem) - for key, value in expect.items(): - self.assertAlmostEqual( - value, getattr(result, key), places=3, msg=f"estimate `{key}` failed" - ) - - @idata( - [ - [ - AmplitudeEstimation(3), - "mle", - { - "likelihood_ratio": (0.2494734, 0.3003771), - "fisher": (0.2486176, 0.2999286), - "observed_fisher": (0.2484562, 0.3000900), - }, - ], - [ - MaximumLikelihoodAmplitudeEstimation(3), - "estimation", - { - "likelihood_ratio": (0.2598794, 0.2798536), - "fisher": (0.2584889, 0.2797018), - "observed_fisher": (0.2659279, 0.2722627), - }, - ], - ] - ) - @unpack - def test_confidence_intervals(self, qae, key, expect): - """End-to-end test for all confidence intervals.""" - n = 3 - - estimation_problem = EstimationProblem(SineIntegral(n), objective_qubits=[n]) - with self.assertWarns(DeprecationWarning): - qae.quantum_instance = self._statevector - # statevector simulator - result = qae.estimate(estimation_problem) - - self._statevector.reset_execution_results() - methods = ["lr", "fi", "oi"] # short for likelihood_ratio, fisher, observed_fisher - alphas = [0.1, 0.00001, 0.9] # alpha shouldn't matter in statevector - for alpha, method in zip(alphas, methods): - confint = qae.compute_confidence_interval(result, alpha, method) - # confidence interval based on statevector should be empty, as we are sure of the result - self.assertAlmostEqual(confint[1] - confint[0], 0.0) - self.assertAlmostEqual(confint[0], getattr(result, key)) - - # qasm simulator - shots = 100 - alpha = 0.01 - with self.assertWarns(DeprecationWarning): - qae.quantum_instance = self._qasm(shots) - result = qae.estimate(estimation_problem) - - for method, expected_confint in expect.items(): - confint = qae.compute_confidence_interval(result, alpha, method) - np.testing.assert_array_almost_equal(confint, expected_confint) - self.assertTrue(confint[0] <= getattr(result, key) <= confint[1]) - - def test_iqae_confidence_intervals(self): - """End-to-end test for the IQAE confidence interval.""" - n = 3 - expected_confint = (0.1984050, 0.3511015) - estimation_problem = EstimationProblem(SineIntegral(n), objective_qubits=[n]) - - with self.assertWarns(DeprecationWarning): - qae = IterativeAmplitudeEstimation(0.1, 0.01, quantum_instance=self._statevector) - # statevector simulator - result = qae.estimate(estimation_problem) - - self._statevector.reset_execution_results() - confint = result.confidence_interval - # confidence interval based on statevector should be empty, as we are sure of the result - self.assertAlmostEqual(confint[1] - confint[0], 0.0) - self.assertAlmostEqual(confint[0], result.estimation) - - # qasm simulator - shots = 100 - - with self.assertWarns(DeprecationWarning): - qae.quantum_instance = self._qasm(shots) - result = qae.estimate(estimation_problem) - - confint = result.confidence_interval - np.testing.assert_array_almost_equal(confint, expected_confint) - self.assertTrue(confint[0] <= result.estimation <= confint[1]) - - -class TestAmplitudeEstimation(QiskitAlgorithmsTestCase): - """Specific tests for canonical AE.""" - - def test_warns_if_good_state_set(self): - """Check AE warns if is_good_state is set.""" - circuit = QuantumCircuit(1) - problem = EstimationProblem(circuit, objective_qubits=[0], is_good_state=lambda x: True) - - qae = AmplitudeEstimation(num_eval_qubits=1, sampler=Sampler()) - - with self.assertWarns(Warning): - _ = qae.estimate(problem) - - -@ddt -class TestFasterAmplitudeEstimation(QiskitAlgorithmsTestCase): - """Specific tests for Faster AE.""" - - def setUp(self): - super().setUp() - self._sampler = Sampler(options={"seed": 2}) - - def test_rescaling(self): - """Test the rescaling.""" - amplitude = 0.8 - scaling = 0.25 - circuit = QuantumCircuit(1) - circuit.ry(2 * np.arcsin(amplitude), 0) - problem = EstimationProblem(circuit, objective_qubits=[0]) - - rescaled = problem.rescale(scaling) - rescaled_amplitude = Statevector.from_instruction(rescaled.state_preparation).data[3] - - self.assertAlmostEqual(scaling * amplitude, rescaled_amplitude) - - def test_run_without_rescaling(self): - """Run Faster AE without rescaling if the amplitude is in [0, 1/4].""" - # construct estimation problem - prob = 0.11 - a_op = QuantumCircuit(1) - a_op.ry(2 * np.arcsin(np.sqrt(prob)), 0) - problem = EstimationProblem(a_op, objective_qubits=[0]) - - # construct algo without rescaling - backend = BasicAer.get_backend("statevector_simulator") - - with self.assertWarns(DeprecationWarning): - fae = FasterAmplitudeEstimation(0.1, 1, rescale=False, quantum_instance=backend) - - # run the algo - result = fae.estimate(problem) - - # assert the result is correct - self.assertAlmostEqual(result.estimation, prob) - - # assert no rescaling was used - theta = np.mean(result.theta_intervals[-1]) - value_without_scaling = np.sin(theta) ** 2 - self.assertAlmostEqual(result.estimation, value_without_scaling) - - def test_sampler_run_without_rescaling(self): - """Run Faster AE without rescaling if the amplitude is in [0, 1/4].""" - # construct estimation problem - prob = 0.11 - a_op = QuantumCircuit(1) - a_op.ry(2 * np.arcsin(np.sqrt(prob)), 0) - problem = EstimationProblem(a_op, objective_qubits=[0]) - - # construct algo without rescaling - fae = FasterAmplitudeEstimation(0.1, 1, rescale=False, sampler=self._sampler) - - # run the algo - result = fae.estimate(problem) - - # assert the result is correct - self.assertAlmostEqual(result.estimation, prob, places=2) - - # assert no rescaling was used - theta = np.mean(result.theta_intervals[-1]) - value_without_scaling = np.sin(theta) ** 2 - self.assertAlmostEqual(result.estimation, value_without_scaling) - - def test_rescaling_with_custom_grover_raises(self): - """Test that the rescaling option fails if a custom Grover operator is used.""" - prob = 0.8 - a_op = BernoulliStateIn(prob) - q_op = BernoulliGrover(prob) - problem = EstimationProblem(a_op, objective_qubits=[0], grover_operator=q_op) - - # construct algo without rescaling - backend = BasicAer.get_backend("statevector_simulator") - with self.assertWarns(DeprecationWarning): - fae = FasterAmplitudeEstimation(0.1, 1, quantum_instance=backend) - - # run the algo - with self.assertWarns(Warning): - _ = fae.estimate(problem) - - @data(("statevector_simulator", 0.2), ("qasm_simulator", 0.199440)) - @unpack - def test_good_state(self, backend_str, expect): - """Test with a good state function.""" - - def is_good_state(bitstr): - return bitstr[1] == "1" - - # construct the estimation problem where the second qubit is ignored - a_op = QuantumCircuit(2) - a_op.ry(2 * np.arcsin(np.sqrt(0.2)), 0) - - # oracle only affects first qubit - oracle = QuantumCircuit(2) - oracle.z(0) - - # reflect only on first qubit - q_op = GroverOperator(oracle, a_op, reflection_qubits=[0]) - - # but we measure both qubits (hence both are objective qubits) - problem = EstimationProblem( - a_op, objective_qubits=[0, 1], grover_operator=q_op, is_good_state=is_good_state - ) - - # construct algo - with self.assertWarns(DeprecationWarning): - backend = QuantumInstance( - BasicAer.get_backend(backend_str), seed_simulator=2, seed_transpiler=2 - ) - # cannot use rescaling with a custom grover operator - - with self.assertWarns(DeprecationWarning): - fae = FasterAmplitudeEstimation(0.01, 5, rescale=False, quantum_instance=backend) - - # run the algo - result = fae.estimate(problem) - - # assert the result is correct - self.assertAlmostEqual(result.estimation, expect, places=5) - - -if __name__ == "__main__": - unittest.main() diff --git a/test/python/algorithms/test_aux_ops_evaluator.py b/test/python/algorithms/test_aux_ops_evaluator.py deleted file mode 100644 index bff0b07c09c9..000000000000 --- a/test/python/algorithms/test_aux_ops_evaluator.py +++ /dev/null @@ -1,197 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2022, 2023. -# -# 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. -"""Tests evaluator of auxiliary operators for algorithms.""" - -import unittest -import warnings -from typing import Tuple, Union - -from test.python.algorithms import QiskitAlgorithmsTestCase -import numpy as np -from ddt import ddt, data - -from qiskit.algorithms.list_or_dict import ListOrDict -from qiskit.providers import Backend -from qiskit.quantum_info import Statevector -from qiskit.algorithms import eval_observables -from qiskit import BasicAer, QuantumCircuit -from qiskit.circuit.library import EfficientSU2 -from qiskit.opflow import ( - PauliSumOp, - X, - Z, - I, - ExpectationFactory, - OperatorBase, - ExpectationBase, - StateFn, -) -from qiskit.utils import QuantumInstance, algorithm_globals - - -@ddt -class TestAuxOpsEvaluator(QiskitAlgorithmsTestCase): - """Tests evaluator of auxiliary operators for algorithms.""" - - def setUp(self): - super().setUp() - self.seed = 50 - with warnings.catch_warnings(): - warnings.filterwarnings("ignore", category=DeprecationWarning) - algorithm_globals.random_seed = self.seed - with self.assertWarns(DeprecationWarning): - self.h2_op = ( - -1.052373245772859 * (I ^ I) - + 0.39793742484318045 * (I ^ Z) - - 0.39793742484318045 * (Z ^ I) - - 0.01128010425623538 * (Z ^ Z) - + 0.18093119978423156 * (X ^ X) - ) - - self.threshold = 1e-8 - self.backend_names = ["statevector_simulator", "qasm_simulator"] - - def get_exact_expectation(self, ansatz: QuantumCircuit, observables: ListOrDict[OperatorBase]): - """ - Calculates the exact expectation to be used as an expected result for unit tests. - """ - if isinstance(observables, dict): - observables_list = list(observables.values()) - else: - observables_list = observables - - # the exact value is a list of (mean, variance) where we expect 0 variance - exact = [ - (Statevector(ansatz).expectation_value(observable), 0) - for observable in observables_list - ] - - if isinstance(observables, dict): - return dict(zip(observables.keys(), exact)) - - return exact - - def _run_test( - self, - expected_result: ListOrDict[Tuple[complex, complex]], - quantum_state: Union[QuantumCircuit, Statevector], - decimal: int, - expectation: ExpectationBase, - observables: ListOrDict[OperatorBase], - quantum_instance: Union[QuantumInstance, Backend], - ): - - with self.assertWarns(DeprecationWarning): - result = eval_observables( - quantum_instance, quantum_state, observables, expectation, self.threshold - ) - - if isinstance(observables, dict): - np.testing.assert_equal(list(result.keys()), list(expected_result.keys())) - np.testing.assert_array_almost_equal( - list(result.values()), list(expected_result.values()), decimal=decimal - ) - else: - np.testing.assert_array_almost_equal(result, expected_result, decimal=decimal) - - @data( - [ - PauliSumOp.from_list([("II", 0.5), ("ZZ", 0.5), ("YY", 0.5), ("XX", -0.5)]), - PauliSumOp.from_list([("II", 2.0)]), - ], - [ - PauliSumOp.from_list([("ZZ", 2.0)]), - ], - { - "op1": PauliSumOp.from_list([("II", 2.0)]), - "op2": PauliSumOp.from_list([("II", 0.5), ("ZZ", 0.5), ("YY", 0.5), ("XX", -0.5)]), - }, - { - "op1": PauliSumOp.from_list([("ZZ", 2.0)]), - }, - ) - def test_eval_observables(self, observables: ListOrDict[OperatorBase]): - """Tests evaluator of auxiliary operators for algorithms.""" - - ansatz = EfficientSU2(2) - parameters = np.array( - [1.2, 4.2, 1.4, 2.0, 1.2, 4.2, 1.4, 2.0, 1.2, 4.2, 1.4, 2.0, 1.2, 4.2, 1.4, 2.0], - dtype=float, - ) - - bound_ansatz = ansatz.assign_parameters(parameters) - expected_result = self.get_exact_expectation(bound_ansatz, observables) - - for backend_name in self.backend_names: - shots = 4096 if backend_name == "qasm_simulator" else 1 - decimal = ( - 1 if backend_name == "qasm_simulator" else 6 - ) # to accommodate for qasm being imperfect - with self.subTest(msg=f"Test {backend_name} backend."): - backend = BasicAer.get_backend(backend_name) - with self.assertWarns(DeprecationWarning): - quantum_instance = QuantumInstance( - backend=backend, - shots=shots, - seed_simulator=self.seed, - seed_transpiler=self.seed, - ) - expectation = ExpectationFactory.build( - operator=self.h2_op, - backend=quantum_instance, - ) - - with self.subTest(msg="Test QuantumCircuit."): - self._run_test( - expected_result, - bound_ansatz, - decimal, - expectation, - observables, - quantum_instance, - ) - - with self.subTest(msg="Test QuantumCircuit with Backend."): - self._run_test( - expected_result, - bound_ansatz, - decimal, - expectation, - observables, - backend, - ) - - with self.subTest(msg="Test Statevector."): - statevector = Statevector(bound_ansatz) - self._run_test( - expected_result, - statevector, - decimal, - expectation, - observables, - quantum_instance, - ) - with self.assertWarns(DeprecationWarning): - with self.subTest(msg="Test StateFn."): - statefn = StateFn(bound_ansatz) - self._run_test( - expected_result, - statefn, - decimal, - expectation, - observables, - quantum_instance, - ) - - -if __name__ == "__main__": - unittest.main() diff --git a/test/python/algorithms/test_backendv1.py b/test/python/algorithms/test_backendv1.py deleted file mode 100644 index d0a544834c72..000000000000 --- a/test/python/algorithms/test_backendv1.py +++ /dev/null @@ -1,148 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2021, 2023. -# -# 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 Providers that support BackendV1 interface""" - -import unittest -import warnings - -from test.python.algorithms import QiskitAlgorithmsTestCase -from qiskit import QuantumCircuit -from qiskit.providers.fake_provider import FakeProvider -from qiskit.utils import QuantumInstance, algorithm_globals -from qiskit.algorithms import VQE, Grover, AmplificationProblem -from qiskit.opflow import X, Z, I -from qiskit.algorithms.optimizers import SPSA -from qiskit.circuit.library import TwoLocal, EfficientSU2 -from qiskit.utils.mitigation import CompleteMeasFitter - - -class TestBackendV1(QiskitAlgorithmsTestCase): - """test BackendV1 interface""" - - def setUp(self): - super().setUp() - self._provider = FakeProvider() - self._qasm = self._provider.get_backend("fake_qasm_simulator") - self.seed = 50 - - def test_vqe_qasm(self): - """Test the VQE on QASM simulator.""" - optimizer = SPSA(maxiter=300, last_avg=5) - wavefunction = TwoLocal(rotation_blocks="ry", entanglement_blocks="cz") - - with self.assertWarns(DeprecationWarning): - h2_op = ( - -1.052373245772859 * (I ^ I) - + 0.39793742484318045 * (I ^ Z) - - 0.39793742484318045 * (Z ^ I) - - 0.01128010425623538 * (Z ^ Z) - + 0.18093119978423156 * (X ^ X) - ) - qasm_simulator = QuantumInstance( - self._qasm, shots=1024, seed_simulator=self.seed, seed_transpiler=self.seed - ) - - with self.assertWarns(DeprecationWarning): - vqe = VQE( - ansatz=wavefunction, - optimizer=optimizer, - max_evals_grouped=1, - quantum_instance=qasm_simulator, - ) - result = vqe.compute_minimum_eigenvalue(operator=h2_op) - - self.assertAlmostEqual(result.eigenvalue.real, -1.86, delta=0.05) - - def test_run_circuit_oracle(self): - """Test execution with a quantum circuit oracle""" - oracle = QuantumCircuit(2) - oracle.cz(0, 1) - problem = AmplificationProblem(oracle, is_good_state=["11"]) - - with self.assertWarns(DeprecationWarning): - qi = QuantumInstance( - self._provider.get_backend("fake_vigo"), seed_simulator=12, seed_transpiler=32 - ) - - with self.assertWarns(DeprecationWarning): - grover = Grover(quantum_instance=qi) - result = grover.amplify(problem) - - self.assertIn(result.top_measurement, ["11"]) - - def test_run_circuit_oracle_single_experiment_backend(self): - """Test execution with a quantum circuit oracle""" - oracle = QuantumCircuit(2) - oracle.cz(0, 1) - problem = AmplificationProblem(oracle, is_good_state=["11"]) - backend = self._provider.get_backend("fake_vigo") - backend._configuration.max_experiments = 1 - - with self.assertWarns(DeprecationWarning): - qi = QuantumInstance(backend, seed_simulator=12, seed_transpiler=32) - - with self.assertWarns(DeprecationWarning): - grover = Grover(quantum_instance=qi) - result = grover.amplify(problem) - - self.assertIn(result.top_measurement, ["11"]) - - def test_measurement_error_mitigation_with_vqe(self): - """measurement error mitigation test with vqe""" - try: - from qiskit.providers.aer import noise - except ImportError as ex: - self.skipTest(f"Package doesn't appear to be installed. Error: '{str(ex)}'") - return - - with warnings.catch_warnings(): - warnings.filterwarnings("ignore", category=DeprecationWarning) - algorithm_globals.random_seed = 0 - - # build noise model - noise_model = noise.NoiseModel() - read_err = noise.errors.readout_error.ReadoutError([[0.9, 0.1], [0.25, 0.75]]) - noise_model.add_all_qubit_readout_error(read_err) - - backend = self._qasm - - with self.assertWarns(DeprecationWarning): - quantum_instance = QuantumInstance( - backend=backend, - seed_simulator=167, - seed_transpiler=167, - noise_model=noise_model, - measurement_error_mitigation_cls=CompleteMeasFitter, - ) - h2_hamiltonian = ( - -1.052373245772859 * (I ^ I) - + 0.39793742484318045 * (I ^ Z) - - 0.39793742484318045 * (Z ^ I) - - 0.01128010425623538 * (Z ^ Z) - + 0.18093119978423156 * (X ^ X) - ) - - optimizer = SPSA(maxiter=200) - ansatz = EfficientSU2(2, reps=1) - - with self.assertWarns(DeprecationWarning): - vqe = VQE(ansatz=ansatz, optimizer=optimizer, quantum_instance=quantum_instance) - result = vqe.compute_minimum_eigenvalue(operator=h2_hamiltonian) - - self.assertGreater(quantum_instance.time_taken, 0.0) - quantum_instance.reset_execution_results() - self.assertAlmostEqual(result.eigenvalue.real, -1.86, delta=0.05) - - -if __name__ == "__main__": - unittest.main() diff --git a/test/python/algorithms/test_backendv2.py b/test/python/algorithms/test_backendv2.py deleted file mode 100644 index 0987a631f690..000000000000 --- a/test/python/algorithms/test_backendv2.py +++ /dev/null @@ -1,102 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2021, 2023. -# -# 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 Providers that support BackendV2 interface""" - -import unittest -from test.python.algorithms import QiskitAlgorithmsTestCase -from qiskit import QuantumCircuit -from qiskit.providers.fake_provider import FakeProvider -from qiskit.providers.fake_provider.fake_backend_v2 import FakeBackendSimple -from qiskit.utils import QuantumInstance -from qiskit.algorithms import VQE, Grover, AmplificationProblem -from qiskit.opflow import X, Z, I -from qiskit.algorithms.optimizers import SPSA -from qiskit.circuit.library import TwoLocal - - -class TestBackendV2(QiskitAlgorithmsTestCase): - """test BackendV2 interface""" - - def setUp(self): - super().setUp() - self._provider = FakeProvider() - self._qasm = FakeBackendSimple() - self.seed = 50 - - def test_vqe_qasm(self): - """Test the VQE on QASM simulator.""" - optimizer = SPSA(maxiter=300, last_avg=5) - wavefunction = TwoLocal(rotation_blocks="ry", entanglement_blocks="cz") - - with self.assertWarns(DeprecationWarning): - h2_op = ( - -1.052373245772859 * (I ^ I) - + 0.39793742484318045 * (I ^ Z) - - 0.39793742484318045 * (Z ^ I) - - 0.01128010425623538 * (Z ^ Z) - + 0.18093119978423156 * (X ^ X) - ) - qasm_simulator = QuantumInstance( - self._qasm, shots=1024, seed_simulator=self.seed, seed_transpiler=self.seed - ) - - with self.assertWarns(DeprecationWarning): - vqe = VQE( - ansatz=wavefunction, - optimizer=optimizer, - max_evals_grouped=1, - quantum_instance=qasm_simulator, - ) - result = vqe.compute_minimum_eigenvalue(operator=h2_op) - - self.assertAlmostEqual(result.eigenvalue.real, -1.86, delta=0.05) - - def test_run_circuit_oracle(self): - """Test execution with a quantum circuit oracle""" - oracle = QuantumCircuit(2) - oracle.cz(0, 1) - problem = AmplificationProblem(oracle, is_good_state=["11"]) - - with self.assertWarns(DeprecationWarning): - qi = QuantumInstance( - self._provider.get_backend("fake_yorktown"), seed_simulator=12, seed_transpiler=32 - ) - - with self.assertWarns(DeprecationWarning): - grover = Grover(quantum_instance=qi) - result = grover.amplify(problem) - - self.assertIn(result.top_measurement, ["11"]) - - def test_run_circuit_oracle_single_experiment_backend(self): - """Test execution with a quantum circuit oracle""" - oracle = QuantumCircuit(2) - oracle.cz(0, 1) - problem = AmplificationProblem(oracle, is_good_state=["11"]) - backend = self._provider.get_backend("fake_yorktown") - backend._configuration.max_experiments = 1 - - with self.assertWarns(DeprecationWarning): - qi = QuantumInstance( - self._provider.get_backend("fake_yorktown"), seed_simulator=12, seed_transpiler=32 - ) - - with self.assertWarns(DeprecationWarning): - grover = Grover(quantum_instance=qi) - result = grover.amplify(problem) - - self.assertIn(result.top_measurement, ["11"]) - - -if __name__ == "__main__": - unittest.main() diff --git a/test/python/algorithms/test_entangler_map.py b/test/python/algorithms/test_entangler_map.py deleted file mode 100644 index 56dd5100c64f..000000000000 --- a/test/python/algorithms/test_entangler_map.py +++ /dev/null @@ -1,68 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2019, 2020. -# -# 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 Entangler Map""" - -import unittest - -from test.python.algorithms import QiskitAlgorithmsTestCase -from qiskit.utils import get_entangler_map, validate_entangler_map - - -class TestEntanglerMap(QiskitAlgorithmsTestCase): - """Test Entangler Map""" - - def test_map_type_linear(self): - """,ap type linear test""" - ref_map = [[0, 1], [1, 2], [2, 3]] - entangler_map = get_entangler_map("linear", 4) - - for (ref_src, ref_targ), (exp_src, exp_targ) in zip(ref_map, entangler_map): - self.assertEqual(ref_src, exp_src) - self.assertEqual(ref_targ, exp_targ) - - def test_map_type_full(self): - """map type full test""" - ref_map = [[0, 1], [0, 2], [0, 3], [1, 2], [1, 3], [2, 3]] - entangler_map = get_entangler_map("full", 4) - - for (ref_src, ref_targ), (exp_src, exp_targ) in zip(ref_map, entangler_map): - self.assertEqual(ref_src, exp_src) - self.assertEqual(ref_targ, exp_targ) - - def test_validate_entangler_map(self): - """validate entangler map test""" - valid_map = [[0, 1], [0, 2], [0, 3], [1, 2], [1, 3], [2, 3]] - self.assertTrue(validate_entangler_map(valid_map, 4)) - - valid_map_2 = [[0, 1], [0, 2], [0, 3], [1, 2], [1, 3], [2, 3], [3, 2]] - self.assertTrue(validate_entangler_map(valid_map_2, 4, True)) - - invalid_map = [[0, 4], [4, 2], [0, 3], [1, 2], [1, 3], [2, 3]] - with self.assertRaises(ValueError): - validate_entangler_map(invalid_map, 4) - - invalid_map_2 = [[0, 1], [0, 2], [0, 3], [1, 2], [1, 3], [2, 3], [3, 2]] - with self.assertRaises(ValueError): - validate_entangler_map(invalid_map_2, 4) - - wrong_type_map = {0: [1, 2, 3], 1: [2, 3]} - with self.assertRaises(TypeError): - validate_entangler_map(wrong_type_map, 4) - - wrong_type_map_2 = [(0, 1), (0, 2), (0, 3)] - with self.assertRaises(TypeError): - validate_entangler_map(wrong_type_map_2, 4) - - -if __name__ == "__main__": - unittest.main() diff --git a/test/python/algorithms/test_grover.py b/test/python/algorithms/test_grover.py deleted file mode 100644 index 0ba6cbaccf04..000000000000 --- a/test/python/algorithms/test_grover.py +++ /dev/null @@ -1,402 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2018, 2022. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. - -"""Test Grover's algorithm.""" - -import itertools -import unittest -import warnings - -from test.python.algorithms import QiskitAlgorithmsTestCase - -import numpy as np -from ddt import data, ddt, idata, unpack - -from qiskit import BasicAer, QuantumCircuit -from qiskit.algorithms import AmplificationProblem, Grover -from qiskit.circuit.library import GroverOperator, PhaseOracle -from qiskit.primitives import Sampler -from qiskit.quantum_info import Operator, Statevector -from qiskit.utils import QuantumInstance, algorithm_globals -from qiskit.utils.optionals import HAS_TWEEDLEDUM - - -@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() - with self.assertWarns(DeprecationWarning): - self.statevector = QuantumInstance( - BasicAer.get_backend("statevector_simulator"), seed_simulator=12, seed_transpiler=32 - ) - self.qasm = QuantumInstance( - BasicAer.get_backend("qasm_simulator"), seed_simulator=12, seed_transpiler=32 - ) - self._sampler = Sampler() - self._sampler_with_shots = Sampler(options={"shots": 1024, "seed": 123}) - with warnings.catch_warnings(): - warnings.filterwarnings("ignore", category=DeprecationWarning) - algorithm_globals.random_seed = 123 - - @unittest.skipUnless(HAS_TWEEDLEDUM, "tweedledum required for this test") - @data("ideal", "shots", False) - 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) - if not use_sampler: - with self.assertWarns(DeprecationWarning): - result = grover.amplify(problem) - else: - result = grover.amplify(problem) - self.assertEqual(result.top_measurement, "11") - - @idata(itertools.product(["ideal", "shots", False], [[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"]) - if not use_sampler: - with self.assertWarns(DeprecationWarning): - result = grover.amplify(problem) - else: - result = grover.amplify(problem) - self.assertEqual(result.top_measurement, "111") - - @idata(itertools.product(["shots", False], [[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"]) - if not use_sampler: - with self.assertWarns(DeprecationWarning): - result = grover.amplify(problem) - else: - result = grover.amplify(problem) - self.assertEqual(result.top_measurement, "111") - - @data("ideal", "shots", False) - 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")) - if not use_sampler: - with self.assertWarns(DeprecationWarning): - result = grover.amplify(problem) - else: - result = grover.amplify(problem) - self.assertEqual(result.top_measurement, "111") - - @idata(itertools.product(["ideal", "shots", False], [[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" - ): - if not use_sampler: - with self.assertWarns(DeprecationWarning): - grover.amplify(problem) - else: - grover.amplify(problem) - - @data("ideal", "shots", False) - 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"]) - if not use_sampler: - with self.assertWarns(DeprecationWarning): - result = grover.amplify(problem) - else: - result = grover.amplify(problem) - self.assertEqual(result.top_measurement, "111") - - @data("ideal", "shots", False) - 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"]) - if not use_sampler: - with self.assertWarns(DeprecationWarning): - result = grover.amplify(problem) - else: - result = grover.amplify(problem) - self.assertEqual(result.top_measurement, "111") - - @data("ideal", "shots", False) - 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]) - if not use_sampler: - with self.assertWarns(DeprecationWarning): - result = grover.amplify(problem) - else: - result = grover.amplify(problem) - self.assertEqual(len(result.iterations), 2**n) - - @data("ideal", "shots", False) - 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", False) - 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) - if not use_sampler: - with self.assertWarns(DeprecationWarning): - result = grover.amplify(problem) - else: - result = grover.amplify(problem) - self.assertIn(result.top_measurement, ["11"]) - - @data("ideal", "shots", False) - 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) - if not use_sampler: - with self.assertWarns(DeprecationWarning): - result = grover.amplify(problem) - else: - result = grover.amplify(problem) - self.assertIn(result.top_measurement, ["11"]) - - @data("ideal", "shots", False) - 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) - if not use_sampler: - with self.assertWarns(DeprecationWarning): - result = grover.amplify(problem) - else: - 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", False) - 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]) - if not use_sampler: - with self.assertWarns(DeprecationWarning): - result = grover.amplify(problem) - else: - result = grover.amplify(problem) - if use_sampler: - 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) - else: - expected_results = [ - {"11": 1024}, - {"00": 238, "01": 253, "10": 263, "11": 270}, - {"00": 238, "01": 253, "10": 263, "11": 270}, - {"11": 1024}, - ] - self.assertEqual(result.circuit_results, expected_results) - - @data("ideal", "shots", False) - 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) - if not use_sampler: - with self.assertWarns(DeprecationWarning): - result = grover.amplify(problem) - else: - result = grover.amplify(problem) - self.assertAlmostEqual(result.max_probability, 1.0) - - @unittest.skipUnless(HAS_TWEEDLEDUM, "tweedledum required for this test") - @data("ideal", "shots", False) - 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) - if not use_sampler: - with self.assertWarns(DeprecationWarning): - result = grover.amplify(problem) - else: - 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: - with self.assertWarns(DeprecationWarning): - grover = Grover( - quantum_instance=self.qasm, - iterations=iterations, - growth_rate=growth_rate, - sample_from_iterations=sample_from_iterations, - ) - return grover - - -if __name__ == "__main__": - unittest.main() diff --git a/test/python/algorithms/test_measure_error_mitigation.py b/test/python/algorithms/test_measure_error_mitigation.py deleted file mode 100644 index ed9e972c524e..000000000000 --- a/test/python/algorithms/test_measure_error_mitigation.py +++ /dev/null @@ -1,524 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2019, 2023. -# -# 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 Measurement Error Mitigation""" - -import unittest -import warnings - -from test.python.algorithms import QiskitAlgorithmsTestCase -from ddt import ddt, data, unpack -import numpy as np -import rustworkx as rx -from qiskit import QuantumCircuit, execute -from qiskit.quantum_info import Pauli -from qiskit.exceptions import QiskitError -from qiskit.utils import QuantumInstance, algorithm_globals -from qiskit.algorithms import VQE, QAOA -from qiskit.opflow import I, X, Z, PauliSumOp -from qiskit.algorithms.optimizers import SPSA, COBYLA -from qiskit.circuit.library import EfficientSU2 -from qiskit.utils.mitigation import CompleteMeasFitter, TensoredMeasFitter -from qiskit.utils.measurement_error_mitigation import build_measurement_error_mitigation_circuits -from qiskit.utils import optionals - -if optionals.HAS_AER: - # pylint: disable=no-name-in-module - from qiskit import Aer - from qiskit.providers.aer import noise -if optionals.HAS_IGNIS: - # pylint: disable=no-name-in-module - from qiskit.ignis.mitigation.measurement import ( - CompleteMeasFitter as CompleteMeasFitter_IG, - TensoredMeasFitter as TensoredMeasFitter_IG, - ) - - -@ddt -class TestMeasurementErrorMitigation(QiskitAlgorithmsTestCase): - """Test measurement error mitigation.""" - - @unittest.skipUnless(optionals.HAS_AER, "qiskit-aer is required for this test") - @data( - ("CompleteMeasFitter", None, False), - ("TensoredMeasFitter", None, False), - ("TensoredMeasFitter", [[0, 1]], True), - ("TensoredMeasFitter", [[1], [0]], False), - ) - @unpack - def test_measurement_error_mitigation_with_diff_qubit_order( - self, - fitter_str, - mit_pattern, - fails, - ): - """measurement error mitigation with different qubit order""" - with warnings.catch_warnings(): - warnings.filterwarnings("ignore", category=DeprecationWarning) - algorithm_globals.random_seed = 0 - - # build noise model - noise_model = noise.NoiseModel() - read_err = noise.errors.readout_error.ReadoutError([[0.9, 0.1], [0.25, 0.75]]) - noise_model.add_all_qubit_readout_error(read_err) - - fitter_cls = ( - CompleteMeasFitter if fitter_str == "CompleteMeasFitter" else TensoredMeasFitter - ) - backend = Aer.get_backend("aer_simulator") - - with self.assertWarns(DeprecationWarning): - quantum_instance = QuantumInstance( - backend=backend, - seed_simulator=1679, - seed_transpiler=167, - shots=1000, - noise_model=noise_model, - measurement_error_mitigation_cls=fitter_cls, - cals_matrix_refresh_period=0, - mit_pattern=mit_pattern, - ) - - # circuit - qc1 = QuantumCircuit(2, 2) - qc1.h(0) - qc1.cx(0, 1) - qc1.measure(0, 0) - qc1.measure(1, 1) - qc2 = QuantumCircuit(2, 2) - qc2.h(0) - qc2.cx(0, 1) - qc2.measure(1, 0) - qc2.measure(0, 1) - - with self.assertWarns(DeprecationWarning): - if fails: - self.assertRaisesRegex( - QiskitError, - "Each element in the mit pattern should have length 1.", - quantum_instance.execute, - [qc1, qc2], - ) - else: - quantum_instance.execute([qc1, qc2]) - - self.assertGreater(quantum_instance.time_taken, 0.0) - quantum_instance.reset_execution_results() - - # failure case - qc3 = QuantumCircuit(3, 3) - qc3.h(2) - qc3.cx(1, 2) - qc3.measure(2, 1) - qc3.measure(1, 2) - - with self.assertWarns(DeprecationWarning): - self.assertRaises(QiskitError, quantum_instance.execute, [qc1, qc3]) - - @unittest.skipUnless(optionals.HAS_AER, "qiskit-aer is required for this test") - @data(("CompleteMeasFitter", None), ("TensoredMeasFitter", [[0], [1]])) - def test_measurement_error_mitigation_with_vqe(self, config): - """measurement error mitigation test with vqe""" - - fitter_str, mit_pattern = config - with warnings.catch_warnings(): - warnings.filterwarnings("ignore", category=DeprecationWarning) - algorithm_globals.random_seed = 0 - - # build noise model - noise_model = noise.NoiseModel() - read_err = noise.errors.readout_error.ReadoutError([[0.9, 0.1], [0.25, 0.75]]) - noise_model.add_all_qubit_readout_error(read_err) - - fitter_cls = ( - CompleteMeasFitter if fitter_str == "CompleteMeasFitter" else TensoredMeasFitter - ) - backend = Aer.get_backend("aer_simulator") - - with self.assertWarns(DeprecationWarning): - quantum_instance = QuantumInstance( - backend=backend, - seed_simulator=167, - seed_transpiler=167, - noise_model=noise_model, - measurement_error_mitigation_cls=fitter_cls, - mit_pattern=mit_pattern, - ) - - optimizer = SPSA(maxiter=200) - ansatz = EfficientSU2(2, reps=1) - - with self.assertWarns(DeprecationWarning): - h2_hamiltonian = ( - -1.052373245772859 * (I ^ I) - + 0.39793742484318045 * (I ^ Z) - - 0.39793742484318045 * (Z ^ I) - - 0.01128010425623538 * (Z ^ Z) - + 0.18093119978423156 * (X ^ X) - ) - - with self.assertWarns(DeprecationWarning): - vqe = VQE(ansatz=ansatz, optimizer=optimizer, quantum_instance=quantum_instance) - result = vqe.compute_minimum_eigenvalue(operator=h2_hamiltonian) - - self.assertGreater(quantum_instance.time_taken, 0.0) - quantum_instance.reset_execution_results() - self.assertAlmostEqual(result.eigenvalue.real, -1.86, delta=0.05) - - def _get_operator(self, weight_matrix): - """Generate Hamiltonian for the max-cut problem of a graph. - - Args: - weight_matrix (numpy.ndarray) : adjacency matrix. - - Returns: - PauliSumOp: operator for the Hamiltonian - float: a constant shift for the obj function. - - """ - num_nodes = weight_matrix.shape[0] - pauli_list = [] - shift = 0 - for i in range(num_nodes): - for j in range(i): - if weight_matrix[i, j] != 0: - x_p = np.zeros(num_nodes, dtype=bool) - z_p = np.zeros(num_nodes, dtype=bool) - z_p[i] = True - z_p[j] = True - pauli_list.append([0.5 * weight_matrix[i, j], Pauli((z_p, x_p))]) - shift -= 0.5 * weight_matrix[i, j] - opflow_list = [(pauli[1].to_label(), pauli[0]) for pauli in pauli_list] - - with self.assertWarns(DeprecationWarning): - return PauliSumOp.from_list(opflow_list), shift - - @unittest.skipUnless(optionals.HAS_AER, "qiskit-aer is required for this test") - def test_measurement_error_mitigation_qaoa(self): - """measurement error mitigation test with QAOA""" - with warnings.catch_warnings(): - warnings.filterwarnings("ignore", category=DeprecationWarning) - algorithm_globals.random_seed = 167 - - backend = Aer.get_backend("aer_simulator") - with warnings.catch_warnings(): - warnings.filterwarnings("ignore", category=DeprecationWarning) - w = rx.adjacency_matrix( - rx.undirected_gnp_random_graph(5, 0.5, seed=algorithm_globals.random_seed) - ) - qubit_op, _ = self._get_operator(w) - initial_point = np.asarray([0.0, 0.0]) - - # Compute first without noise - with self.assertWarns(DeprecationWarning): - quantum_instance = QuantumInstance( - backend=backend, - seed_simulator=algorithm_globals.random_seed, - seed_transpiler=algorithm_globals.random_seed, - ) - - with self.assertWarns(DeprecationWarning): - qaoa = QAOA( - optimizer=COBYLA(maxiter=3), - quantum_instance=quantum_instance, - initial_point=initial_point, - ) - result = qaoa.compute_minimum_eigenvalue(operator=qubit_op) - - ref_eigenvalue = result.eigenvalue.real - - # compute with noise - # build noise model - noise_model = noise.NoiseModel() - read_err = noise.errors.readout_error.ReadoutError([[0.9, 0.1], [0.25, 0.75]]) - noise_model.add_all_qubit_readout_error(read_err) - - with self.assertWarns(DeprecationWarning): - quantum_instance = QuantumInstance( - backend=backend, - seed_simulator=algorithm_globals.random_seed, - seed_transpiler=algorithm_globals.random_seed, - noise_model=noise_model, - measurement_error_mitigation_cls=CompleteMeasFitter, - shots=10000, - ) - - with self.assertWarns(DeprecationWarning): - - qaoa = QAOA( - optimizer=COBYLA(maxiter=3), - quantum_instance=quantum_instance, - initial_point=initial_point, - ) - result = qaoa.compute_minimum_eigenvalue(operator=qubit_op) - - self.assertAlmostEqual(result.eigenvalue.real, ref_eigenvalue, delta=0.05) - - @unittest.skipUnless(optionals.HAS_AER, "qiskit-aer is required for this test") - @unittest.skipUnless(optionals.HAS_IGNIS, "qiskit-ignis is required to run this test") - @data("CompleteMeasFitter", "TensoredMeasFitter") - def test_measurement_error_mitigation_with_diff_qubit_order_ignis(self, fitter_str): - """measurement error mitigation with different qubit order""" - with warnings.catch_warnings(): - warnings.filterwarnings("ignore", category=DeprecationWarning) - algorithm_globals.random_seed = 0 - - # build noise model - noise_model = noise.NoiseModel() - read_err = noise.errors.readout_error.ReadoutError([[0.9, 0.1], [0.25, 0.75]]) - noise_model.add_all_qubit_readout_error(read_err) - - fitter_cls = ( - CompleteMeasFitter_IG if fitter_str == "CompleteMeasFitter" else TensoredMeasFitter_IG - ) - backend = Aer.get_backend("aer_simulator") - - with self.assertWarns(DeprecationWarning): - quantum_instance = QuantumInstance( - backend=backend, - seed_simulator=1679, - seed_transpiler=167, - shots=1000, - noise_model=noise_model, - measurement_error_mitigation_cls=fitter_cls, - cals_matrix_refresh_period=0, - ) - - # circuit - qc1 = QuantumCircuit(2, 2) - qc1.h(0) - qc1.cx(0, 1) - qc1.measure(0, 0) - qc1.measure(1, 1) - qc2 = QuantumCircuit(2, 2) - qc2.h(0) - qc2.cx(0, 1) - qc2.measure(1, 0) - qc2.measure(0, 1) - - if fitter_cls == TensoredMeasFitter_IG: - with self.assertWarnsRegex(DeprecationWarning, r".*ignis.*"): - self.assertRaisesRegex( - QiskitError, - "TensoredMeasFitter doesn't support subset_fitter.", - quantum_instance.execute, - [qc1, qc2], - ) - else: - # this should run smoothly - with self.assertWarnsRegex(DeprecationWarning, r".*ignis.*"): - quantum_instance.execute([qc1, qc2]) - - self.assertGreater(quantum_instance.time_taken, 0.0) - quantum_instance.reset_execution_results() - - # failure case - qc3 = QuantumCircuit(3, 3) - qc3.h(2) - qc3.cx(1, 2) - qc3.measure(2, 1) - qc3.measure(1, 2) - - self.assertRaises(QiskitError, quantum_instance.execute, [qc1, qc3]) - - @unittest.skipUnless(optionals.HAS_AER, "qiskit-aer is required for this test") - @unittest.skipUnless(optionals.HAS_IGNIS, "qiskit-ignis is required to run this test") - @data(("CompleteMeasFitter", None), ("TensoredMeasFitter", [[0], [1]])) - def test_measurement_error_mitigation_with_vqe_ignis(self, config): - """measurement error mitigation test with vqe""" - fitter_str, mit_pattern = config - with warnings.catch_warnings(): - warnings.filterwarnings("ignore", category=DeprecationWarning) - algorithm_globals.random_seed = 0 - - # build noise model - noise_model = noise.NoiseModel() - read_err = noise.errors.readout_error.ReadoutError([[0.9, 0.1], [0.25, 0.75]]) - noise_model.add_all_qubit_readout_error(read_err) - - fitter_cls = ( - CompleteMeasFitter_IG if fitter_str == "CompleteMeasFitter" else TensoredMeasFitter_IG - ) - backend = Aer.get_backend("aer_simulator") - - with self.assertWarns(DeprecationWarning): - quantum_instance = QuantumInstance( - backend=backend, - seed_simulator=167, - seed_transpiler=167, - noise_model=noise_model, - measurement_error_mitigation_cls=fitter_cls, - mit_pattern=mit_pattern, - ) - - h2_hamiltonian = ( - -1.052373245772859 * (I ^ I) - + 0.39793742484318045 * (I ^ Z) - - 0.39793742484318045 * (Z ^ I) - - 0.01128010425623538 * (Z ^ Z) - + 0.18093119978423156 * (X ^ X) - ) - optimizer = SPSA(maxiter=200) - ansatz = EfficientSU2(2, reps=1) - - with self.assertWarnsRegex(DeprecationWarning): - vqe = VQE(ansatz=ansatz, optimizer=optimizer, quantum_instance=quantum_instance) - result = vqe.compute_minimum_eigenvalue(operator=h2_hamiltonian) - - self.assertGreater(quantum_instance.time_taken, 0.0) - quantum_instance.reset_execution_results() - self.assertAlmostEqual(result.eigenvalue.real, -1.86, delta=0.05) - - @unittest.skipUnless(optionals.HAS_AER, "qiskit-aer is required for this test") - @unittest.skipUnless(optionals.HAS_IGNIS, "qiskit-ignis is required to run this test") - def test_calibration_results(self): - """check that results counts are the same with/without error mitigation""" - with warnings.catch_warnings(): - warnings.filterwarnings("ignore", category=DeprecationWarning) - algorithm_globals.random_seed = 1679 - np.random.seed(algorithm_globals.random_seed) - - qc = QuantumCircuit(1) - qc.x(0) - - qc_meas = qc.copy() - qc_meas.measure_all() - backend = Aer.get_backend("aer_simulator") - - counts_array = [None, None] - for idx, is_use_mitigation in enumerate([True, False]): - with self.assertWarns(DeprecationWarning): - if is_use_mitigation: - quantum_instance = QuantumInstance( - backend, - seed_simulator=algorithm_globals.random_seed, - seed_transpiler=algorithm_globals.random_seed, - shots=1024, - measurement_error_mitigation_cls=CompleteMeasFitter_IG, - ) - with self.assertWarnsRegex(DeprecationWarning, r".*ignis.*"): - counts_array[idx] = quantum_instance.execute(qc_meas).get_counts() - else: - quantum_instance = QuantumInstance( - backend, - seed_simulator=algorithm_globals.random_seed, - seed_transpiler=algorithm_globals.random_seed, - shots=1024, - ) - counts_array[idx] = quantum_instance.execute(qc_meas).get_counts() - self.assertEqual( - counts_array[0], counts_array[1], msg="Counts different with/without fitter." - ) - - @unittest.skipUnless(optionals.HAS_AER, "qiskit-aer is required for this test") - def test_circuit_modified(self): - """tests that circuits don't get modified on QI execute with error mitigation - as per issue #7449 - """ - with warnings.catch_warnings(): - warnings.filterwarnings("ignore", category=DeprecationWarning) - algorithm_globals.random_seed = 1679 - np.random.seed(algorithm_globals.random_seed) - - circuit = QuantumCircuit(1) - circuit.x(0) - circuit.measure_all() - - with self.assertWarns(DeprecationWarning): - qi = QuantumInstance( - Aer.get_backend("aer_simulator"), - seed_simulator=algorithm_globals.random_seed, - seed_transpiler=algorithm_globals.random_seed, - shots=1024, - measurement_error_mitigation_cls=CompleteMeasFitter, - ) - # The error happens on transpiled circuits since "execute" was changing the input array - # Non transpiled circuits didn't have a problem because a new transpiled array was created - # internally. - circuits_ref = qi.transpile(circuit) # always returns a new array - circuits_input = circuits_ref.copy() - - with self.assertWarns(DeprecationWarning): - _ = qi.execute(circuits_input, had_transpiled=True) - self.assertEqual(circuits_ref, circuits_input, msg="Transpiled circuit array modified.") - - @unittest.skipUnless(optionals.HAS_AER, "qiskit-aer is required for this test") - def test_tensor_subset_fitter(self): - """Test the subset fitter method of the tensor fitter.""" - - # Construct a noise model where readout has errors of different strengths. - noise_model = noise.NoiseModel() - # big error - read_err0 = noise.errors.readout_error.ReadoutError([[0.90, 0.10], [0.25, 0.75]]) - # ideal - read_err1 = noise.errors.readout_error.ReadoutError([[1.00, 0.00], [0.00, 1.00]]) - # small error - read_err2 = noise.errors.readout_error.ReadoutError([[0.98, 0.02], [0.03, 0.97]]) - noise_model.add_readout_error(read_err0, (0,)) - noise_model.add_readout_error(read_err1, (1,)) - noise_model.add_readout_error(read_err2, (2,)) - - mit_pattern = [[idx] for idx in range(3)] - backend = Aer.get_backend("aer_simulator") - backend.set_options(seed_simulator=123) - - with self.assertWarns(DeprecationWarning): - mit_circuits = build_measurement_error_mitigation_circuits( - [0, 1, 2], - TensoredMeasFitter, - backend, - backend_config={}, - compile_config={}, - mit_pattern=mit_pattern, - ) - result = execute(mit_circuits[0], backend, noise_model=noise_model).result() - fitter = TensoredMeasFitter(result, mit_pattern=mit_pattern) - - cal_matrices = fitter.cal_matrices - - # Check that permutations and permuted subsets match. - for subset in [[1, 0], [1, 2], [0, 2], [2, 0, 1]]: - with self.subTest(subset=subset): - with self.assertWarns(DeprecationWarning): - new_fitter = fitter.subset_fitter(subset) - - for idx, qubit in enumerate(subset): - self.assertTrue(np.allclose(new_fitter.cal_matrices[idx], cal_matrices[qubit])) - - self.assertRaisesRegex( - QiskitError, - "Qubit 3 is not in the mit pattern", - fitter.subset_fitter, - [0, 2, 3], - ) - - # Test that we properly correct a circuit with permuted measurements. - circuit = QuantumCircuit(3, 3) - circuit.x(range(3)) - circuit.measure(1, 0) - circuit.measure(2, 1) - circuit.measure(0, 2) - - result = execute( - circuit, backend, noise_model=noise_model, shots=1000, seed_simulator=0 - ).result() - with self.subTest(subset=subset): - with self.assertWarns(DeprecationWarning): - new_result = fitter.subset_fitter([1, 2, 0]).filter.apply(result) - - # The noisy result should have a poor 111 state, the mit. result should be good. - self.assertTrue(result.get_counts()["111"] < 800) - self.assertTrue(new_result.get_counts()["111"] > 990) - - -if __name__ == "__main__": - unittest.main() diff --git a/test/python/algorithms/test_numpy_eigen_solver.py b/test/python/algorithms/test_numpy_eigen_solver.py deleted file mode 100644 index 36d2b66148d0..000000000000 --- a/test/python/algorithms/test_numpy_eigen_solver.py +++ /dev/null @@ -1,210 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2018, 2023. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. - -"""Test NumPy Eigen solver""" - -import unittest -from test.python.algorithms import QiskitAlgorithmsTestCase - -import numpy as np -from ddt import data, ddt - -from qiskit.algorithms import NumPyEigensolver -from qiskit.opflow import PauliSumOp, X, Y, Z - - -@ddt -class TestNumPyEigensolver(QiskitAlgorithmsTestCase): - """Test NumPy Eigen solver""" - - def setUp(self): - super().setUp() - with self.assertWarns(DeprecationWarning): - self.qubit_op = PauliSumOp.from_list( - [ - ("II", -1.052373245772859), - ("ZI", 0.39793742484318045), - ("IZ", -0.39793742484318045), - ("ZZ", -0.01128010425623538), - ("XX", 0.18093119978423156), - ] - ) - - def test_ce(self): - """Test basics""" - with self.assertWarns(DeprecationWarning): - algo = NumPyEigensolver() - result = algo.compute_eigenvalues(operator=self.qubit_op, aux_operators=[]) - - self.assertEqual(len(result.eigenvalues), 1) - self.assertEqual(len(result.eigenstates), 1) - self.assertEqual(result.eigenvalues.dtype, np.float64) - self.assertAlmostEqual(result.eigenvalues[0], -1.85727503) - - def test_ce_k4(self): - """Test for k=4 eigenvalues""" - with self.assertWarns(DeprecationWarning): - algo = NumPyEigensolver(k=4) - result = algo.compute_eigenvalues(operator=self.qubit_op, aux_operators=[]) - - self.assertEqual(len(result.eigenvalues), 4) - self.assertEqual(len(result.eigenstates), 4) - self.assertEqual(result.eigenvalues.dtype, np.float64) - np.testing.assert_array_almost_equal( - result.eigenvalues, [-1.85727503, -1.24458455, -0.88272215, -0.22491125] - ) - - def test_ce_k4_filtered(self): - """Test for k=4 eigenvalues with filter""" - - # define filter criterion - # pylint: disable=unused-argument - def criterion(x, v, a_v): - return v >= -1 - - with self.assertWarns(DeprecationWarning): - algo = NumPyEigensolver(k=4, filter_criterion=criterion) - result = algo.compute_eigenvalues(operator=self.qubit_op, aux_operators=[]) - - self.assertEqual(len(result.eigenvalues), 2) - self.assertEqual(len(result.eigenstates), 2) - self.assertEqual(result.eigenvalues.dtype, np.float64) - np.testing.assert_array_almost_equal(result.eigenvalues, [-0.88272215, -0.22491125]) - - def test_ce_k4_filtered_empty(self): - """Test for k=4 eigenvalues with filter always returning False""" - - # define filter criterion - # pylint: disable=unused-argument - def criterion(x, v, a_v): - return False - - with self.assertWarns(DeprecationWarning): - algo = NumPyEigensolver(k=4, filter_criterion=criterion) - result = algo.compute_eigenvalues(operator=self.qubit_op, aux_operators=[]) - self.assertEqual(len(result.eigenvalues), 0) - self.assertEqual(len(result.eigenstates), 0) - - @data(X, Y, Z) - def test_ce_k1_1q(self, op): - """Test for 1 qubit operator""" - - with self.assertWarns(DeprecationWarning): - algo = NumPyEigensolver(k=1) - result = algo.compute_eigenvalues(operator=op) - np.testing.assert_array_almost_equal(result.eigenvalues, [-1]) - - @data(X, Y, Z) - def test_ce_k2_1q(self, op): - """Test for 1 qubit operator""" - - with self.assertWarns(DeprecationWarning): - algo = NumPyEigensolver(k=2) - result = algo.compute_eigenvalues(operator=op) - np.testing.assert_array_almost_equal(result.eigenvalues, [-1, 1]) - - def test_aux_operators_list(self): - """Test list-based aux_operators.""" - - with self.assertWarns(DeprecationWarning): - aux_op1 = PauliSumOp.from_list([("II", 2.0)]) - aux_op2 = PauliSumOp.from_list([("II", 0.5), ("ZZ", 0.5), ("YY", 0.5), ("XX", -0.5)]) - aux_ops = [aux_op1, aux_op2] - - with self.assertWarns(DeprecationWarning): - algo = NumPyEigensolver() - result = algo.compute_eigenvalues(operator=self.qubit_op, aux_operators=aux_ops) - - self.assertEqual(len(result.eigenvalues), 1) - self.assertEqual(len(result.eigenstates), 1) - self.assertEqual(result.eigenvalues.dtype, np.float64) - self.assertAlmostEqual(result.eigenvalues[0], -1.85727503) - self.assertEqual(len(result.aux_operator_eigenvalues), 1) - self.assertEqual(len(result.aux_operator_eigenvalues[0]), 2) - # expectation values - self.assertAlmostEqual(result.aux_operator_eigenvalues[0][0][0], 2, places=6) - self.assertAlmostEqual(result.aux_operator_eigenvalues[0][1][0], 0, places=6) - # standard deviations - self.assertAlmostEqual(result.aux_operator_eigenvalues[0][0][1], 0.0) - self.assertAlmostEqual(result.aux_operator_eigenvalues[0][1][1], 0.0) - - # Go again with additional None and zero operators - extra_ops = [*aux_ops, None, 0] - - with self.assertWarns(DeprecationWarning): - result = algo.compute_eigenvalues(operator=self.qubit_op, aux_operators=extra_ops) - - self.assertEqual(len(result.eigenvalues), 1) - self.assertEqual(len(result.eigenstates), 1) - self.assertEqual(result.eigenvalues.dtype, np.float64) - self.assertAlmostEqual(result.eigenvalues[0], -1.85727503) - self.assertEqual(len(result.aux_operator_eigenvalues), 1) - self.assertEqual(len(result.aux_operator_eigenvalues[0]), 4) - # expectation values - self.assertAlmostEqual(result.aux_operator_eigenvalues[0][0][0], 2, places=6) - self.assertAlmostEqual(result.aux_operator_eigenvalues[0][1][0], 0, places=6) - self.assertIsNone(result.aux_operator_eigenvalues[0][2], None) - self.assertEqual(result.aux_operator_eigenvalues[0][3][0], 0.0) - # standard deviations - self.assertAlmostEqual(result.aux_operator_eigenvalues[0][0][1], 0.0) - self.assertAlmostEqual(result.aux_operator_eigenvalues[0][1][1], 0.0) - self.assertEqual(result.aux_operator_eigenvalues[0][3][1], 0.0) - - def test_aux_operators_dict(self): - """Test dict-based aux_operators.""" - - with self.assertWarns(DeprecationWarning): - aux_op1 = PauliSumOp.from_list([("II", 2.0)]) - aux_op2 = PauliSumOp.from_list([("II", 0.5), ("ZZ", 0.5), ("YY", 0.5), ("XX", -0.5)]) - aux_ops = {"aux_op1": aux_op1, "aux_op2": aux_op2} - - with self.assertWarns(DeprecationWarning): - algo = NumPyEigensolver() - result = algo.compute_eigenvalues(operator=self.qubit_op, aux_operators=aux_ops) - self.assertEqual(len(result.eigenvalues), 1) - self.assertEqual(len(result.eigenstates), 1) - self.assertEqual(result.eigenvalues.dtype, np.float64) - self.assertAlmostEqual(result.eigenvalues[0], -1.85727503) - self.assertEqual(len(result.aux_operator_eigenvalues), 1) - self.assertEqual(len(result.aux_operator_eigenvalues[0]), 2) - # expectation values - self.assertAlmostEqual(result.aux_operator_eigenvalues[0]["aux_op1"][0], 2, places=6) - self.assertAlmostEqual(result.aux_operator_eigenvalues[0]["aux_op2"][0], 0, places=6) - # standard deviations - self.assertAlmostEqual(result.aux_operator_eigenvalues[0]["aux_op1"][1], 0.0) - self.assertAlmostEqual(result.aux_operator_eigenvalues[0]["aux_op2"][1], 0.0) - - # Go again with additional None and zero operators - extra_ops = {**aux_ops, "None_operator": None, "zero_operator": 0} - - with self.assertWarns(DeprecationWarning): - result = algo.compute_eigenvalues(operator=self.qubit_op, aux_operators=extra_ops) - - self.assertEqual(len(result.eigenvalues), 1) - self.assertEqual(len(result.eigenstates), 1) - self.assertEqual(result.eigenvalues.dtype, np.float64) - self.assertAlmostEqual(result.eigenvalues[0], -1.85727503) - self.assertEqual(len(result.aux_operator_eigenvalues), 1) - self.assertEqual(len(result.aux_operator_eigenvalues[0]), 3) - # expectation values - self.assertAlmostEqual(result.aux_operator_eigenvalues[0]["aux_op1"][0], 2, places=6) - self.assertAlmostEqual(result.aux_operator_eigenvalues[0]["aux_op2"][0], 0, places=6) - self.assertEqual(result.aux_operator_eigenvalues[0]["zero_operator"][0], 0.0) - self.assertTrue("None_operator" not in result.aux_operator_eigenvalues[0].keys()) - # standard deviations - self.assertAlmostEqual(result.aux_operator_eigenvalues[0]["aux_op1"][1], 0.0) - self.assertAlmostEqual(result.aux_operator_eigenvalues[0]["aux_op2"][1], 0.0) - self.assertAlmostEqual(result.aux_operator_eigenvalues[0]["zero_operator"][1], 0.0) - - -if __name__ == "__main__": - unittest.main() diff --git a/test/python/algorithms/test_numpy_minimum_eigen_solver.py b/test/python/algorithms/test_numpy_minimum_eigen_solver.py deleted file mode 100644 index 8f50d5738338..000000000000 --- a/test/python/algorithms/test_numpy_minimum_eigen_solver.py +++ /dev/null @@ -1,277 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2020, 2023. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. - -"""Test NumPy Minimum Eigensolver""" - -import unittest -from test.python.algorithms import QiskitAlgorithmsTestCase - -import numpy as np -from ddt import ddt, data - -from qiskit.algorithms import NumPyMinimumEigensolver -from qiskit.opflow import PauliSumOp, X, Y, Z - - -@ddt -class TestNumPyMinimumEigensolver(QiskitAlgorithmsTestCase): - """Test NumPy Minimum Eigensolver""" - - def setUp(self): - super().setUp() - with self.assertWarns(DeprecationWarning): - self.qubit_op = PauliSumOp.from_list( - [ - ("II", -1.052373245772859), - ("ZI", 0.39793742484318045), - ("IZ", -0.39793742484318045), - ("ZZ", -0.01128010425623538), - ("XX", 0.18093119978423156), - ] - ) - aux_op1 = PauliSumOp.from_list([("II", 2.0)]) - aux_op2 = PauliSumOp.from_list([("II", 0.5), ("ZZ", 0.5), ("YY", 0.5), ("XX", -0.5)]) - - self.aux_ops_list = [aux_op1, aux_op2] - self.aux_ops_dict = {"aux_op1": aux_op1, "aux_op2": aux_op2} - - def test_cme(self): - """Basic test""" - - with self.assertWarns(DeprecationWarning): - algo = NumPyMinimumEigensolver() - result = algo.compute_minimum_eigenvalue( - operator=self.qubit_op, aux_operators=self.aux_ops_list - ) - - self.assertAlmostEqual(result.eigenvalue, -1.85727503 + 0j) - self.assertEqual(len(result.aux_operator_eigenvalues), 2) - np.testing.assert_array_almost_equal(result.aux_operator_eigenvalues[0], [2, 0]) - np.testing.assert_array_almost_equal(result.aux_operator_eigenvalues[1], [0, 0]) - - def test_cme_reuse(self): - """Test reuse""" - # Start with no operator or aux_operators, give via compute method - with self.assertWarns(DeprecationWarning): - algo = NumPyMinimumEigensolver() - result = algo.compute_minimum_eigenvalue(operator=self.qubit_op) - - self.assertEqual(result.eigenvalue.dtype, np.float64) - self.assertAlmostEqual(result.eigenvalue, -1.85727503) - self.assertIsNone(result.aux_operator_eigenvalues) - - # Add aux_operators and go again - with self.assertWarns(DeprecationWarning): - result = algo.compute_minimum_eigenvalue( - operator=self.qubit_op, aux_operators=self.aux_ops_list - ) - - self.assertAlmostEqual(result.eigenvalue, -1.85727503 + 0j) - self.assertEqual(len(result.aux_operator_eigenvalues), 2) - np.testing.assert_array_almost_equal(result.aux_operator_eigenvalues[0], [2, 0]) - np.testing.assert_array_almost_equal(result.aux_operator_eigenvalues[1], [0, 0]) - - # "Remove" aux_operators and go again - with self.assertWarns(DeprecationWarning): - result = algo.compute_minimum_eigenvalue(operator=self.qubit_op, aux_operators=[]) - - self.assertEqual(result.eigenvalue.dtype, np.float64) - self.assertAlmostEqual(result.eigenvalue, -1.85727503) - self.assertIsNone(result.aux_operator_eigenvalues) - - # Set aux_operators and go again - with self.assertWarns(DeprecationWarning): - result = algo.compute_minimum_eigenvalue( - operator=self.qubit_op, aux_operators=self.aux_ops_list - ) - - self.assertAlmostEqual(result.eigenvalue, -1.85727503 + 0j) - self.assertEqual(len(result.aux_operator_eigenvalues), 2) - np.testing.assert_array_almost_equal(result.aux_operator_eigenvalues[0], [2, 0]) - np.testing.assert_array_almost_equal(result.aux_operator_eigenvalues[1], [0, 0]) - - # Finally just set one of aux_operators and main operator, remove aux_operators - - with self.assertWarns(DeprecationWarning): - result = algo.compute_minimum_eigenvalue( - operator=self.aux_ops_list[0], aux_operators=[] - ) - - self.assertAlmostEqual(result.eigenvalue, 2 + 0j) - self.assertIsNone(result.aux_operator_eigenvalues) - - def test_cme_filter(self): - """Basic test""" - - # define filter criterion - # pylint: disable=unused-argument - def criterion(x, v, a_v): - return v >= -0.5 - - with self.assertWarns(DeprecationWarning): - algo = NumPyMinimumEigensolver(filter_criterion=criterion) - result = algo.compute_minimum_eigenvalue( - operator=self.qubit_op, aux_operators=self.aux_ops_list - ) - - self.assertAlmostEqual(result.eigenvalue, -0.22491125 + 0j) - self.assertEqual(len(result.aux_operator_eigenvalues), 2) - np.testing.assert_array_almost_equal(result.aux_operator_eigenvalues[0], [2, 0]) - np.testing.assert_array_almost_equal(result.aux_operator_eigenvalues[1], [0, 0]) - - def test_cme_filter_empty(self): - """Test with filter always returning False""" - - # define filter criterion - # pylint: disable=unused-argument - def criterion(x, v, a_v): - return False - - with self.assertWarns(DeprecationWarning): - algo = NumPyMinimumEigensolver(filter_criterion=criterion) - result = algo.compute_minimum_eigenvalue( - operator=self.qubit_op, aux_operators=self.aux_ops_list - ) - - self.assertEqual(result.eigenvalue, None) - self.assertEqual(result.eigenstate, None) - self.assertEqual(result.aux_operator_eigenvalues, None) - - @data(X, Y, Z) - def test_cme_1q(self, op): - """Test for 1 qubit operator""" - - with self.assertWarns(DeprecationWarning): - algo = NumPyMinimumEigensolver() - result = algo.compute_minimum_eigenvalue(operator=op) - - self.assertAlmostEqual(result.eigenvalue, -1) - - def test_cme_aux_ops_dict(self): - """Test dictionary compatibility of aux_operators""" - # Start with an empty dictionary - with self.assertWarns(DeprecationWarning): - algo = NumPyMinimumEigensolver() - result = algo.compute_minimum_eigenvalue(operator=self.qubit_op, aux_operators={}) - - self.assertAlmostEqual(result.eigenvalue, -1.85727503 + 0j) - self.assertIsNone(result.aux_operator_eigenvalues) - - # Add aux_operators dictionary and go again - with self.assertWarns(DeprecationWarning): - result = algo.compute_minimum_eigenvalue( - operator=self.qubit_op, aux_operators=self.aux_ops_dict - ) - - self.assertAlmostEqual(result.eigenvalue, -1.85727503 + 0j) - self.assertEqual(len(result.aux_operator_eigenvalues), 2) - np.testing.assert_array_almost_equal(result.aux_operator_eigenvalues["aux_op1"], [2, 0]) - np.testing.assert_array_almost_equal(result.aux_operator_eigenvalues["aux_op2"], [0, 0]) - - # Add None and zero operators and go again - extra_ops = {"None_op": None, "zero_op": 0, **self.aux_ops_dict} - with self.assertWarns(DeprecationWarning): - result = algo.compute_minimum_eigenvalue( - operator=self.qubit_op, aux_operators=extra_ops - ) - - self.assertAlmostEqual(result.eigenvalue, -1.85727503 + 0j) - self.assertEqual(len(result.aux_operator_eigenvalues), 3) - np.testing.assert_array_almost_equal(result.aux_operator_eigenvalues["aux_op1"], [2, 0]) - np.testing.assert_array_almost_equal(result.aux_operator_eigenvalues["aux_op2"], [0, 0]) - self.assertEqual(result.aux_operator_eigenvalues["zero_op"], (0.0, 0)) - - def test_aux_operators_list(self): - """Test list-based aux_operators.""" - - with self.assertWarns(DeprecationWarning): - aux_op1 = PauliSumOp.from_list([("II", 2.0)]) - aux_op2 = PauliSumOp.from_list([("II", 0.5), ("ZZ", 0.5), ("YY", 0.5), ("XX", -0.5)]) - aux_ops = [aux_op1, aux_op2] - - with self.assertWarns(DeprecationWarning): - algo = NumPyMinimumEigensolver() - result = algo.compute_minimum_eigenvalue(operator=self.qubit_op, aux_operators=aux_ops) - - self.assertAlmostEqual(result.eigenvalue, -1.85727503 + 0j) - self.assertEqual(len(result.aux_operator_eigenvalues), 2) - # expectation values - self.assertAlmostEqual(result.aux_operator_eigenvalues[0][0], 2, places=6) - self.assertAlmostEqual(result.aux_operator_eigenvalues[1][0], 0, places=6) - # standard deviations - self.assertAlmostEqual(result.aux_operator_eigenvalues[0][1], 0.0) - self.assertAlmostEqual(result.aux_operator_eigenvalues[1][1], 0.0) - - # Go again with additional None and zero operators - extra_ops = [*aux_ops, None, 0] - - with self.assertWarns(DeprecationWarning): - result = algo.compute_minimum_eigenvalue( - operator=self.qubit_op, aux_operators=extra_ops - ) - - self.assertAlmostEqual(result.eigenvalue, -1.85727503 + 0j) - self.assertEqual(len(result.aux_operator_eigenvalues), 4) - # expectation values - self.assertAlmostEqual(result.aux_operator_eigenvalues[0][0], 2, places=6) - self.assertAlmostEqual(result.aux_operator_eigenvalues[1][0], 0, places=6) - self.assertIsNone(result.aux_operator_eigenvalues[2], None) - self.assertEqual(result.aux_operator_eigenvalues[3][0], 0.0) - # standard deviations - self.assertAlmostEqual(result.aux_operator_eigenvalues[0][1], 0.0) - self.assertAlmostEqual(result.aux_operator_eigenvalues[1][1], 0.0) - self.assertEqual(result.aux_operator_eigenvalues[3][1], 0.0) - - def test_aux_operators_dict(self): - """Test dict-based aux_operators.""" - - with self.assertWarns(DeprecationWarning): - aux_op1 = PauliSumOp.from_list([("II", 2.0)]) - aux_op2 = PauliSumOp.from_list([("II", 0.5), ("ZZ", 0.5), ("YY", 0.5), ("XX", -0.5)]) - aux_ops = {"aux_op1": aux_op1, "aux_op2": aux_op2} - - with self.assertWarns(DeprecationWarning): - algo = NumPyMinimumEigensolver() - result = algo.compute_minimum_eigenvalue(operator=self.qubit_op, aux_operators=aux_ops) - - self.assertAlmostEqual(result.eigenvalue, -1.85727503 + 0j) - self.assertEqual(len(result.aux_operator_eigenvalues), 2) - # expectation values - self.assertAlmostEqual(result.aux_operator_eigenvalues["aux_op1"][0], 2, places=6) - self.assertAlmostEqual(result.aux_operator_eigenvalues["aux_op2"][0], 0, places=6) - # standard deviations - self.assertAlmostEqual(result.aux_operator_eigenvalues["aux_op1"][1], 0.0) - self.assertAlmostEqual(result.aux_operator_eigenvalues["aux_op2"][1], 0.0) - - # Go again with additional None and zero operators - extra_ops = {**aux_ops, "None_operator": None, "zero_operator": 0} - - with self.assertWarns(DeprecationWarning): - result = algo.compute_minimum_eigenvalue( - operator=self.qubit_op, aux_operators=extra_ops - ) - - self.assertAlmostEqual(result.eigenvalue, -1.85727503 + 0j) - self.assertEqual(len(result.aux_operator_eigenvalues), 3) - # expectation values - self.assertAlmostEqual(result.aux_operator_eigenvalues["aux_op1"][0], 2, places=6) - self.assertAlmostEqual(result.aux_operator_eigenvalues["aux_op2"][0], 0, places=6) - self.assertEqual(result.aux_operator_eigenvalues["zero_operator"][0], 0.0) - self.assertTrue("None_operator" not in result.aux_operator_eigenvalues.keys()) - # standard deviations - self.assertAlmostEqual(result.aux_operator_eigenvalues["aux_op1"][1], 0.0) - self.assertAlmostEqual(result.aux_operator_eigenvalues["aux_op2"][1], 0.0) - self.assertAlmostEqual(result.aux_operator_eigenvalues["zero_operator"][1], 0.0) - - -if __name__ == "__main__": - unittest.main() diff --git a/test/python/algorithms/test_observables_evaluator.py b/test/python/algorithms/test_observables_evaluator.py deleted file mode 100644 index d4482c9467b5..000000000000 --- a/test/python/algorithms/test_observables_evaluator.py +++ /dev/null @@ -1,189 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2022, 2023. -# -# 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. - -"""Tests evaluator of auxiliary operators for algorithms.""" - -from __future__ import annotations -import unittest -import warnings -from typing import Tuple - -from test.python.algorithms import QiskitAlgorithmsTestCase -import numpy as np -from ddt import ddt, data - -from qiskit.algorithms.list_or_dict import ListOrDict -from qiskit.quantum_info.operators.base_operator import BaseOperator -from qiskit.algorithms import estimate_observables -from qiskit.primitives import Estimator -from qiskit.quantum_info import Statevector, SparsePauliOp -from qiskit import QuantumCircuit -from qiskit.circuit.library import EfficientSU2 -from qiskit.opflow import PauliSumOp -from qiskit.utils import algorithm_globals - - -@ddt -class TestObservablesEvaluator(QiskitAlgorithmsTestCase): - """Tests evaluator of auxiliary operators for algorithms.""" - - def setUp(self): - super().setUp() - self.seed = 50 - with warnings.catch_warnings(): - warnings.filterwarnings("ignore", category=DeprecationWarning) - algorithm_globals.random_seed = self.seed - - self.threshold = 1e-8 - - def get_exact_expectation( - self, ansatz: QuantumCircuit, observables: ListOrDict[BaseOperator | PauliSumOp] - ): - """ - Calculates the exact expectation to be used as an expected result for unit tests. - """ - if isinstance(observables, dict): - observables_list = list(observables.values()) - else: - observables_list = observables - # the exact value is a list of (mean, (variance, shots)) where we expect 0 variance and - # 0 shots - exact = [ - (Statevector(ansatz).expectation_value(observable), {}) - for observable in observables_list - ] - - if isinstance(observables, dict): - return dict(zip(observables.keys(), exact)) - - return exact - - def _run_test( - self, - expected_result: ListOrDict[Tuple[complex, complex]], - quantum_state: QuantumCircuit, - decimal: int, - observables: ListOrDict[BaseOperator | PauliSumOp], - estimator: Estimator, - ): - result = estimate_observables(estimator, quantum_state, observables, None, self.threshold) - - if isinstance(observables, dict): - np.testing.assert_equal(list(result.keys()), list(expected_result.keys())) - means = [element[0] for element in result.values()] - expected_means = [element[0] for element in expected_result.values()] - np.testing.assert_array_almost_equal(means, expected_means, decimal=decimal) - - vars_and_shots = [element[1] for element in result.values()] - expected_vars_and_shots = [element[1] for element in expected_result.values()] - np.testing.assert_array_equal(vars_and_shots, expected_vars_and_shots) - else: - means = [element[0] for element in result] - expected_means = [element[0] for element in expected_result] - np.testing.assert_array_almost_equal(means, expected_means, decimal=decimal) - - vars_and_shots = [element[1] for element in result] - expected_vars_and_shots = [element[1] for element in expected_result] - np.testing.assert_array_equal(vars_and_shots, expected_vars_and_shots) - - @data( - [ - PauliSumOp.from_list([("II", 0.5), ("ZZ", 0.5), ("YY", 0.5), ("XX", -0.5)]), - PauliSumOp.from_list([("II", 2.0)]), - ], - [ - PauliSumOp.from_list([("ZZ", 2.0)]), - ], - { - "op1": PauliSumOp.from_list([("II", 2.0)]), - "op2": PauliSumOp.from_list([("II", 0.5), ("ZZ", 0.5), ("YY", 0.5), ("XX", -0.5)]), - }, - { - "op1": PauliSumOp.from_list([("ZZ", 2.0)]), - }, - [], - {}, - ) - def test_estimate_observables(self, observables: ListOrDict[BaseOperator | PauliSumOp]): - """Tests evaluator of auxiliary operators for algorithms.""" - - ansatz = EfficientSU2(2) - parameters = np.array( - [1.2, 4.2, 1.4, 2.0, 1.2, 4.2, 1.4, 2.0, 1.2, 4.2, 1.4, 2.0, 1.2, 4.2, 1.4, 2.0], - dtype=float, - ) - - bound_ansatz = ansatz.assign_parameters(parameters) - states = bound_ansatz - expected_result = self.get_exact_expectation(bound_ansatz, observables) - estimator = Estimator() - decimal = 6 - self._run_test( - expected_result, - states, - decimal, - observables, - estimator, - ) - - def test_estimate_observables_zero_op(self): - """Tests if a zero operator is handled correctly.""" - ansatz = EfficientSU2(2) - parameters = np.array( - [1.2, 4.2, 1.4, 2.0, 1.2, 4.2, 1.4, 2.0, 1.2, 4.2, 1.4, 2.0, 1.2, 4.2, 1.4, 2.0], - dtype=float, - ) - - bound_ansatz = ansatz.assign_parameters(parameters) - state = bound_ansatz - estimator = Estimator() - observables = [SparsePauliOp(["XX", "YY"]), 0] - result = estimate_observables(estimator, state, observables, None, self.threshold) - expected_result = [(0.015607318055509564, {}), (0.0, {})] - means = [element[0] for element in result] - expected_means = [element[0] for element in expected_result] - np.testing.assert_array_almost_equal(means, expected_means, decimal=0.01) - - vars_and_shots = [element[1] for element in result] - expected_vars_and_shots = [element[1] for element in expected_result] - np.testing.assert_array_equal(vars_and_shots, expected_vars_and_shots) - - def test_estimate_observables_shots(self): - """Tests that variances and shots are returned properly.""" - ansatz = EfficientSU2(2) - parameters = np.array( - [1.2, 4.2, 1.4, 2.0, 1.2, 4.2, 1.4, 2.0, 1.2, 4.2, 1.4, 2.0, 1.2, 4.2, 1.4, 2.0], - dtype=float, - ) - - bound_ansatz = ansatz.assign_parameters(parameters) - state = bound_ansatz - estimator = Estimator(options={"shots": 2048}) - with self.assertWarns(DeprecationWarning): - observables = [PauliSumOp.from_list([("ZZ", 2.0)])] - result = estimate_observables(estimator, state, observables, None, self.threshold) - exact_result = self.get_exact_expectation(bound_ansatz, observables) - expected_result = [(exact_result[0][0], {"variance": 1.0898, "shots": 2048})] - - means = [element[0] for element in result] - expected_means = [element[0] for element in expected_result] - np.testing.assert_array_almost_equal(means, expected_means, decimal=0.01) - - vars_and_shots = [element[1] for element in result] - expected_vars_and_shots = [element[1] for element in expected_result] - for computed, expected in zip(vars_and_shots, expected_vars_and_shots): - self.assertAlmostEqual(computed.pop("variance"), expected.pop("variance"), 2) - self.assertEqual(computed.pop("shots"), expected.pop("shots")) - - -if __name__ == "__main__": - unittest.main() diff --git a/test/python/algorithms/test_phase_estimator.py b/test/python/algorithms/test_phase_estimator.py deleted file mode 100644 index 9040bc1d6912..000000000000 --- a/test/python/algorithms/test_phase_estimator.py +++ /dev/null @@ -1,665 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2018, 2023. -# -# 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 phase estimation""" - -import unittest -from test.python.algorithms import QiskitAlgorithmsTestCase -from ddt import ddt, data, unpack -import numpy as np -from qiskit.circuit.library import ZGate, XGate, HGate, IGate -from qiskit.quantum_info import Pauli, SparsePauliOp, Statevector, Operator -from qiskit.synthesis import MatrixExponential, SuzukiTrotter -from qiskit.primitives import Sampler -from qiskit.algorithms import PhaseEstimationScale -from qiskit.algorithms.phase_estimators import ( - PhaseEstimation, - HamiltonianPhaseEstimation, - IterativePhaseEstimation, -) -import qiskit -from qiskit import QuantumCircuit -from qiskit.opflow import ( - H, - X, - Y, - Z, - I, - StateFn, - PauliTrotterEvolution, - MatrixEvolution, - PauliSumOp, -) -from qiskit.test import slow_test - - -@ddt -class TestHamiltonianPhaseEstimation(QiskitAlgorithmsTestCase): - """Tests for obtaining eigenvalues from phase estimation""" - - def hamiltonian_pe( - self, - hamiltonian, - state_preparation=None, - num_evaluation_qubits=6, - backend=None, - evolution=None, - bound=None, - ): - """Run HamiltonianPhaseEstimation and return result with all phases.""" - if backend is None: - backend = qiskit.BasicAer.get_backend("statevector_simulator") - - with self.assertWarns(DeprecationWarning): - quantum_instance = qiskit.utils.QuantumInstance(backend=backend, shots=10000) - - with self.assertWarns(DeprecationWarning): - phase_est = HamiltonianPhaseEstimation( - num_evaluation_qubits=num_evaluation_qubits, quantum_instance=quantum_instance - ) - result = phase_est.estimate( - hamiltonian=hamiltonian, - state_preparation=state_preparation, - evolution=evolution, - bound=bound, - ) - return result - - @data(MatrixEvolution(), PauliTrotterEvolution("suzuki", 4)) - def test_pauli_sum_1(self, evolution): - """Two eigenvalues from Pauli sum with X, Z""" - with self.assertWarns(DeprecationWarning): - hamiltonian = 0.5 * X + Z - state_preparation = StateFn(H.to_circuit()) - result = self.hamiltonian_pe(hamiltonian, state_preparation, evolution=evolution) - - phase_dict = result.filter_phases(0.162, as_float=True) - phases = list(phase_dict.keys()) - phases.sort() - - self.assertAlmostEqual(phases[0], -1.125, delta=0.001) - self.assertAlmostEqual(phases[1], 1.125, delta=0.001) - - @data(MatrixEvolution(), PauliTrotterEvolution("suzuki", 3)) - def test_pauli_sum_2(self, evolution): - """Two eigenvalues from Pauli sum with X, Y, Z""" - with self.assertWarns(DeprecationWarning): - hamiltonian = 0.5 * X + Y + Z - state_preparation = None - result = self.hamiltonian_pe(hamiltonian, state_preparation, evolution=evolution) - - phase_dict = result.filter_phases(0.1, as_float=True) - phases = list(phase_dict.keys()) - phases.sort() - - self.assertAlmostEqual(phases[0], -1.484, delta=0.001) - self.assertAlmostEqual(phases[1], 1.484, delta=0.001) - - def test_single_pauli_op(self): - """Two eigenvalues from Pauli sum with X, Y, Z""" - hamiltonian = Z - state_preparation = None - with self.assertWarns(DeprecationWarning): - result = self.hamiltonian_pe(hamiltonian, state_preparation, evolution=None) - - eigv = result.most_likely_eigenvalue - with self.subTest("First eigenvalue"): - self.assertAlmostEqual(eigv, 1.0, delta=0.001) - - with self.assertWarns(DeprecationWarning): - state_preparation = StateFn(X.to_circuit()) - result = self.hamiltonian_pe(hamiltonian, state_preparation, bound=1.05) - - eigv = result.most_likely_eigenvalue - with self.subTest("Second eigenvalue"): - self.assertAlmostEqual(eigv, -0.98, delta=0.01) - - @slow_test - def test_H2_hamiltonian(self): - """Test H2 hamiltonian""" - with self.assertWarns(DeprecationWarning): - hamiltonian = ( - (-1.0523732457728587 * (I ^ I)) - + (0.3979374248431802 * (I ^ Z)) - + (-0.3979374248431802 * (Z ^ I)) - + (-0.011280104256235324 * (Z ^ Z)) - + (0.18093119978423147 * (X ^ X)) - ) - state_preparation = StateFn((I ^ H).to_circuit()) - evo = PauliTrotterEvolution(trotter_mode="suzuki", reps=4) - with self.assertWarns(DeprecationWarning): - result = self.hamiltonian_pe(hamiltonian, state_preparation, evolution=evo) - with self.subTest("Most likely eigenvalues"): - self.assertAlmostEqual(result.most_likely_eigenvalue, -1.855, delta=0.001) - with self.subTest("Most likely phase"): - self.assertAlmostEqual(result.phase, 0.5937, delta=0.001) - with self.subTest("All eigenvalues"): - phase_dict = result.filter_phases(0.1) - phases = list(phase_dict.keys()) - self.assertAlmostEqual(phases[0], -0.8979, delta=0.001) - self.assertAlmostEqual(phases[1], -1.8551, delta=0.001) - self.assertAlmostEqual(phases[2], -1.2376, delta=0.001) - - def test_matrix_evolution(self): - """1Q Hamiltonian with MatrixEvolution""" - with self.assertWarns(DeprecationWarning): - hamiltonian = (0.5 * X) + (0.6 * Y) + (0.7 * I) - state_preparation = None - result = self.hamiltonian_pe( - hamiltonian, state_preparation, evolution=MatrixEvolution() - ) - phase_dict = result.filter_phases(0.2, as_float=True) - phases = list(phase_dict.keys()) - self.assertAlmostEqual(phases[0], 1.490, delta=0.001) - self.assertAlmostEqual(phases[1], -0.090, delta=0.001) - - def _setup_from_bound(self, evolution, op_class): - with self.assertWarns(DeprecationWarning): - hamiltonian = 0.5 * X + Y + Z - state_preparation = None - bound = 1.2 * sum(abs(hamiltonian.coeff * coeff) for coeff in hamiltonian.coeffs) - if op_class == "MatrixOp": - hamiltonian = hamiltonian.to_matrix_op() - backend = qiskit.BasicAer.get_backend("statevector_simulator") - - with self.assertWarns(DeprecationWarning): - qi = qiskit.utils.QuantumInstance(backend=backend, shots=10000) - with self.assertWarns(DeprecationWarning): - phase_est = HamiltonianPhaseEstimation(num_evaluation_qubits=6, quantum_instance=qi) - - result = phase_est.estimate( - hamiltonian=hamiltonian, - bound=bound, - evolution=evolution, - state_preparation=state_preparation, - ) - return result - - def test_from_bound(self): - """HamiltonianPhaseEstimation with bound""" - with self.assertWarns(DeprecationWarning): - for op_class in ("SummedOp", "MatrixOp"): - result = self._setup_from_bound(MatrixEvolution(), op_class) - cutoff = 0.01 - phases = result.filter_phases(cutoff) - with self.subTest(f"test phases has the correct length: {op_class}"): - self.assertEqual(len(phases), 2) - with self.subTest(f"test scaled phases are correct: {op_class}"): - self.assertEqual(list(phases.keys()), [1.5, -1.5]) - phases = result.filter_phases(cutoff, scaled=False) - with self.subTest(f"test unscaled phases are correct: {op_class}"): - self.assertEqual(list(phases.keys()), [0.25, 0.75]) - - def test_trotter_from_bound(self): - """HamiltonianPhaseEstimation with bound and Trotterization""" - with self.assertWarns(DeprecationWarning): - result = self._setup_from_bound( - PauliTrotterEvolution(trotter_mode="suzuki", reps=3), op_class="SummedOp" - ) - phase_dict = result.filter_phases(0.1) - phases = list(phase_dict.keys()) - with self.subTest("test phases has the correct length"): - self.assertEqual(len(phases), 2) - with self.subTest("test phases has correct values"): - self.assertAlmostEqual(phases[0], 1.5, delta=0.001) - self.assertAlmostEqual(phases[1], -1.5, delta=0.001) - - # sampler tests - def hamiltonian_pe_sampler( - self, - hamiltonian, - state_preparation=None, - num_evaluation_qubits=6, - evolution=None, - bound=None, - uses_opflow=True, - ): - """Run HamiltonianPhaseEstimation and return result with all phases.""" - sampler = Sampler() - phase_est = HamiltonianPhaseEstimation( - num_evaluation_qubits=num_evaluation_qubits, sampler=sampler - ) - if uses_opflow: - with self.assertWarns(DeprecationWarning): - result = phase_est.estimate( - hamiltonian=hamiltonian, - state_preparation=state_preparation, - evolution=evolution, - bound=bound, - ) - else: - result = phase_est.estimate( - hamiltonian=hamiltonian, - state_preparation=state_preparation, - evolution=evolution, - bound=bound, - ) - return result - - @data(MatrixExponential(), SuzukiTrotter(reps=4)) - def test_pauli_sum_1_sampler(self, evolution): - """Two eigenvalues from Pauli sum with X, Z""" - with self.assertWarns(DeprecationWarning): - hamiltonian = PauliSumOp(SparsePauliOp.from_list([("X", 0.5), ("Z", 1)])) - state_preparation = QuantumCircuit(1).compose(HGate()) - - result = self.hamiltonian_pe_sampler(hamiltonian, state_preparation, evolution=evolution) - phase_dict = result.filter_phases(0.162, as_float=True) - phases = list(phase_dict.keys()) - phases.sort() - - self.assertAlmostEqual(phases[0], -1.125, delta=0.001) - self.assertAlmostEqual(phases[1], 1.125, delta=0.001) - - @data(MatrixExponential(), SuzukiTrotter(reps=3)) - def test_pauli_sum_2_sampler(self, evolution): - """Two eigenvalues from Pauli sum with X, Y, Z""" - with self.assertWarns(DeprecationWarning): - hamiltonian = PauliSumOp(SparsePauliOp.from_list([("X", 0.5), ("Z", 1), ("Y", 1)])) - state_preparation = None - - result = self.hamiltonian_pe_sampler(hamiltonian, state_preparation, evolution=evolution) - phase_dict = result.filter_phases(0.1, as_float=True) - phases = list(phase_dict.keys()) - phases.sort() - - self.assertAlmostEqual(phases[0], -1.484, delta=0.001) - self.assertAlmostEqual(phases[1], 1.484, delta=0.001) - - def test_single_pauli_op_sampler(self): - """Two eigenvalues from Pauli sum with X, Y, Z""" - hamiltonian = SparsePauliOp(Pauli("Z")) - state_preparation = None - - result = self.hamiltonian_pe_sampler( - hamiltonian, state_preparation, evolution=None, uses_opflow=False - ) - eigv = result.most_likely_eigenvalue - with self.subTest("First eigenvalue"): - self.assertAlmostEqual(eigv, 1.0, delta=0.001) - - state_preparation = QuantumCircuit(1).compose(XGate()) - - result = self.hamiltonian_pe_sampler( - hamiltonian, state_preparation, bound=1.05, uses_opflow=False - ) - eigv = result.most_likely_eigenvalue - with self.subTest("Second eigenvalue"): - self.assertAlmostEqual(eigv, -0.98, delta=0.01) - - @data( - Statevector(QuantumCircuit(2).compose(IGate()).compose(HGate())), - QuantumCircuit(2).compose(IGate()).compose(HGate()), - ) - def test_H2_hamiltonian_sampler(self, state_preparation): - """Test H2 hamiltonian""" - - with self.assertWarns(DeprecationWarning): - hamiltonian = PauliSumOp( - SparsePauliOp.from_list( - [ - ("II", -1.0523732457728587), - ("IZ", 0.3979374248431802), - ("ZI", -0.3979374248431802), - ("ZZ", -0.011280104256235324), - ("XX", 0.18093119978423147), - ] - ) - ) - - evo = SuzukiTrotter(reps=4) - result = self.hamiltonian_pe_sampler(hamiltonian, state_preparation, evolution=evo) - with self.subTest("Most likely eigenvalues"): - self.assertAlmostEqual(result.most_likely_eigenvalue, -1.855, delta=0.001) - with self.subTest("Most likely phase"): - self.assertAlmostEqual(result.phase, 0.5937, delta=0.001) - with self.subTest("All eigenvalues"): - phase_dict = result.filter_phases(0.1) - phases = sorted(phase_dict.keys()) - self.assertAlmostEqual(phases[0], -1.8551, delta=0.001) - self.assertAlmostEqual(phases[1], -1.2376, delta=0.001) - self.assertAlmostEqual(phases[2], -0.8979, delta=0.001) - - def test_matrix_evolution_sampler(self): - """1Q Hamiltonian with MatrixEvolution""" - with self.assertWarns(DeprecationWarning): - hamiltonian = PauliSumOp(SparsePauliOp.from_list([("X", 0.5), ("Y", 0.6), ("I", 0.7)])) - state_preparation = None - result = self.hamiltonian_pe_sampler( - hamiltonian, state_preparation, evolution=MatrixExponential() - ) - phase_dict = result.filter_phases(0.2, as_float=True) - phases = sorted(phase_dict.keys()) - self.assertAlmostEqual(phases[0], -0.090, delta=0.001) - self.assertAlmostEqual(phases[1], 1.490, delta=0.001) - - -@ddt -class TestPhaseEstimation(QiskitAlgorithmsTestCase): - """Evolution tests.""" - - def one_phase( - self, - unitary_circuit, - state_preparation=None, - backend_type=None, - phase_estimator=None, - num_iterations=6, - ): - """Run phase estimation with operator, eigenvalue pair `unitary_circuit`, - `state_preparation`. Return the estimated phase as a value in :math:`[0,1)`. - """ - if backend_type is None: - backend_type = "qasm_simulator" - backend = qiskit.BasicAer.get_backend(backend_type) - if phase_estimator is None: - phase_estimator = IterativePhaseEstimation - - with self.assertWarns(DeprecationWarning): - qi = qiskit.utils.QuantumInstance(backend=backend, shots=10000) - - with self.assertWarns(DeprecationWarning): - - if phase_estimator == IterativePhaseEstimation: - p_est = IterativePhaseEstimation(num_iterations=num_iterations, quantum_instance=qi) - elif phase_estimator == PhaseEstimation: - p_est = PhaseEstimation(num_evaluation_qubits=6, quantum_instance=qi) - else: - raise ValueError("Unrecognized phase_estimator") - - result = p_est.estimate(unitary=unitary_circuit, state_preparation=state_preparation) - phase = result.phase - return phase - - @data( - (X.to_circuit(), 0.5, "statevector_simulator", IterativePhaseEstimation), - (X.to_circuit(), 0.5, "qasm_simulator", IterativePhaseEstimation), - (None, 0.0, "qasm_simulator", IterativePhaseEstimation), - (X.to_circuit(), 0.5, "qasm_simulator", PhaseEstimation), - (None, 0.0, "qasm_simulator", PhaseEstimation), - (X.to_circuit(), 0.5, "statevector_simulator", PhaseEstimation), - ) - @unpack - def test_qpe_Z(self, state_preparation, expected_phase, backend_type, phase_estimator): - """eigenproblem Z, |0> and |1>""" - unitary_circuit = Z.to_circuit() - with self.assertWarns(DeprecationWarning): - phase = self.one_phase( - unitary_circuit, - state_preparation, - backend_type=backend_type, - phase_estimator=phase_estimator, - ) - self.assertEqual(phase, expected_phase) - - @data( - (H.to_circuit(), 0.0, IterativePhaseEstimation), - ((H @ X).to_circuit(), 0.5, IterativePhaseEstimation), - (H.to_circuit(), 0.0, PhaseEstimation), - ((H @ X).to_circuit(), 0.5, PhaseEstimation), - ) - @unpack - def test_qpe_X_plus_minus(self, state_preparation, expected_phase, phase_estimator): - """eigenproblem X, (|+>, |->)""" - unitary_circuit = X.to_circuit() - with self.assertWarns(DeprecationWarning): - phase = self.one_phase( - unitary_circuit, state_preparation, phase_estimator=phase_estimator - ) - self.assertEqual(phase, expected_phase) - - @data( - (X.to_circuit(), 0.125, IterativePhaseEstimation), - (I.to_circuit(), 0.875, IterativePhaseEstimation), - (X.to_circuit(), 0.125, PhaseEstimation), - (I.to_circuit(), 0.875, PhaseEstimation), - ) - @unpack - def test_qpe_RZ(self, state_preparation, expected_phase, phase_estimator): - """eigenproblem RZ, (|0>, |1>)""" - alpha = np.pi / 2 - unitary_circuit = QuantumCircuit(1) - unitary_circuit.rz(alpha, 0) - with self.assertWarns(DeprecationWarning): - phase = self.one_phase( - unitary_circuit, state_preparation, phase_estimator=phase_estimator - ) - self.assertEqual(phase, expected_phase) - - def test_check_num_iterations(self): - """test check for num_iterations greater than zero""" - unitary_circuit = X.to_circuit() - state_preparation = None - with self.assertRaises(ValueError): - self.one_phase(unitary_circuit, state_preparation, num_iterations=-1) - - def phase_estimation( - self, - unitary_circuit, - state_preparation=None, - num_evaluation_qubits=6, - backend=None, - construct_circuit=False, - ): - """Run phase estimation with operator, eigenvalue pair `unitary_circuit`, - `state_preparation`. Return all results - """ - if backend is None: - backend = qiskit.BasicAer.get_backend("statevector_simulator") - - with self.assertWarns(DeprecationWarning): - qi = qiskit.utils.QuantumInstance(backend=backend, shots=10000) - with self.assertWarns(DeprecationWarning): - phase_est = PhaseEstimation( - num_evaluation_qubits=num_evaluation_qubits, quantum_instance=qi - ) - if construct_circuit: - pe_circuit = phase_est.construct_circuit(unitary_circuit, state_preparation) - result = phase_est.estimate_from_pe_circuit(pe_circuit, unitary_circuit.num_qubits) - else: - result = phase_est.estimate( - unitary=unitary_circuit, state_preparation=state_preparation - ) - - return result - - @data(True, False) - def test_qpe_Zplus(self, construct_circuit): - """superposition eigenproblem Z, |+>""" - unitary_circuit = Z.to_circuit() - state_preparation = H.to_circuit() # prepare |+> - - with self.assertWarns(DeprecationWarning): - result = self.phase_estimation( - unitary_circuit, - state_preparation, - backend=qiskit.BasicAer.get_backend("statevector_simulator"), - construct_circuit=construct_circuit, - ) - - phases = result.filter_phases(1e-15, as_float=True) - with self.subTest("test phases has correct values"): - self.assertEqual(list(phases.keys()), [0.0, 0.5]) - - with self.subTest("test phases has correct probabilities"): - np.testing.assert_allclose(list(phases.values()), [0.5, 0.5]) - - with self.subTest("test bitstring representation"): - phases = result.filter_phases(1e-15, as_float=False) - self.assertEqual(list(phases.keys()), ["000000", "100000"]) - - # sampler tests - def one_phase_sampler( - self, - unitary_circuit, - state_preparation=None, - phase_estimator=None, - num_iterations=6, - shots=None, - ): - """Run phase estimation with operator, eigenvalue pair `unitary_circuit`, - `state_preparation`. Return the estimated phase as a value in :math:`[0,1)`. - """ - if shots is not None: - options = {"shots": shots} - else: - options = {} - sampler = Sampler(options=options) - if phase_estimator is None: - phase_estimator = IterativePhaseEstimation - if phase_estimator == IterativePhaseEstimation: - p_est = IterativePhaseEstimation(num_iterations=num_iterations, sampler=sampler) - elif phase_estimator == PhaseEstimation: - p_est = PhaseEstimation(num_evaluation_qubits=6, sampler=sampler) - else: - raise ValueError("Unrecognized phase_estimator") - result = p_est.estimate(unitary=unitary_circuit, state_preparation=state_preparation) - phase = result.phase - return phase - - @data( - (QuantumCircuit(1).compose(XGate()), 0.5, None, IterativePhaseEstimation), - (QuantumCircuit(1).compose(XGate()), 0.5, 1000, IterativePhaseEstimation), - (None, 0.0, 1000, IterativePhaseEstimation), - (QuantumCircuit(1).compose(XGate()), 0.5, 1000, PhaseEstimation), - (None, 0.0, 1000, PhaseEstimation), - (QuantumCircuit(1).compose(XGate()), 0.5, None, PhaseEstimation), - ) - @unpack - def test_qpe_Z_sampler(self, state_preparation, expected_phase, shots, phase_estimator): - """eigenproblem Z, |0> and |1>""" - unitary_circuit = QuantumCircuit(1).compose(ZGate()) - phase = self.one_phase_sampler( - unitary_circuit, - state_preparation=state_preparation, - phase_estimator=phase_estimator, - shots=shots, - ) - self.assertEqual(phase, expected_phase) - - @data( - (QuantumCircuit(1).compose(HGate()), 0.0, IterativePhaseEstimation), - (QuantumCircuit(1).compose(HGate()).compose(ZGate()), 0.5, IterativePhaseEstimation), - (QuantumCircuit(1).compose(HGate()), 0.0, PhaseEstimation), - (QuantumCircuit(1).compose(HGate()).compose(ZGate()), 0.5, PhaseEstimation), - ) - @unpack - def test_qpe_X_plus_minus_sampler(self, state_preparation, expected_phase, phase_estimator): - """eigenproblem X, (|+>, |->)""" - unitary_circuit = QuantumCircuit(1).compose(XGate()) - phase = self.one_phase_sampler( - unitary_circuit, - state_preparation, - phase_estimator, - ) - self.assertEqual(phase, expected_phase) - - @data( - (QuantumCircuit(1).compose(XGate()), 0.125, IterativePhaseEstimation), - (QuantumCircuit(1).compose(IGate()), 0.875, IterativePhaseEstimation), - (QuantumCircuit(1).compose(XGate()), 0.125, PhaseEstimation), - (QuantumCircuit(1).compose(IGate()), 0.875, PhaseEstimation), - ) - @unpack - def test_qpe_RZ_sampler(self, state_preparation, expected_phase, phase_estimator): - """eigenproblem RZ, (|0>, |1>)""" - alpha = np.pi / 2 - unitary_circuit = QuantumCircuit(1) - unitary_circuit.rz(alpha, 0) - phase = self.one_phase_sampler( - unitary_circuit, - state_preparation, - phase_estimator, - ) - self.assertEqual(phase, expected_phase) - - @data( - ((X ^ X).to_circuit(), 0.25, IterativePhaseEstimation), - ((I ^ X).to_circuit(), 0.125, IterativePhaseEstimation), - ((X ^ X).to_circuit(), 0.25, PhaseEstimation), - ((I ^ X).to_circuit(), 0.125, PhaseEstimation), - ) - @unpack - def test_qpe_two_qubit_unitary(self, state_preparation, expected_phase, phase_estimator): - """two qubit unitary T ^ T""" - unitary_circuit = QuantumCircuit(2) - unitary_circuit.t(0) - unitary_circuit.t(1) - phase = self.one_phase_sampler( - unitary_circuit, - state_preparation, - phase_estimator, - ) - self.assertEqual(phase, expected_phase) - - def test_check_num_iterations_sampler(self): - """test check for num_iterations greater than zero""" - unitary_circuit = QuantumCircuit(1).compose(XGate()) - state_preparation = None - with self.assertRaises(ValueError): - self.one_phase_sampler(unitary_circuit, state_preparation, num_iterations=-1) - - def test_phase_estimation_scale_from_operator(self): - """test that PhaseEstimationScale from_pauli_sum works with Operator""" - circ = QuantumCircuit(2) - op = Operator(circ) - scale = PhaseEstimationScale.from_pauli_sum(op) - self.assertEqual(scale._bound, 4.0) - - def phase_estimation_sampler( - self, - unitary_circuit, - sampler: Sampler, - state_preparation=None, - num_evaluation_qubits=6, - construct_circuit=False, - ): - """Run phase estimation with operator, eigenvalue pair `unitary_circuit`, - `state_preparation`. Return all results - """ - phase_est = PhaseEstimation(num_evaluation_qubits=num_evaluation_qubits, sampler=sampler) - if construct_circuit: - pe_circuit = phase_est.construct_circuit(unitary_circuit, state_preparation) - result = phase_est.estimate_from_pe_circuit(pe_circuit, unitary_circuit.num_qubits) - else: - result = phase_est.estimate( - unitary=unitary_circuit, state_preparation=state_preparation - ) - return result - - @data(True, False) - def test_qpe_Zplus_sampler(self, construct_circuit): - """superposition eigenproblem Z, |+>""" - unitary_circuit = QuantumCircuit(1).compose(ZGate()) - state_preparation = QuantumCircuit(1).compose(HGate()) # prepare |+> - sampler = Sampler() - result = self.phase_estimation_sampler( - unitary_circuit, - sampler, - state_preparation, - construct_circuit=construct_circuit, - ) - - phases = result.filter_phases(1e-15, as_float=True) - with self.subTest("test phases has correct values"): - self.assertEqual(list(phases.keys()), [0.0, 0.5]) - - with self.subTest("test phases has correct probabilities"): - np.testing.assert_allclose(list(phases.values()), [0.5, 0.5]) - - with self.subTest("test bitstring representation"): - phases = result.filter_phases(1e-15, as_float=False) - self.assertEqual(list(phases.keys()), ["000000", "100000"]) - - -if __name__ == "__main__": - unittest.main() diff --git a/test/python/algorithms/test_qaoa.py b/test/python/algorithms/test_qaoa.py deleted file mode 100644 index 5edb525022a3..000000000000 --- a/test/python/algorithms/test_qaoa.py +++ /dev/null @@ -1,410 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2018, 2023. -# -# 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 QAOA""" - -import unittest -import warnings -from test.python.algorithms import QiskitAlgorithmsTestCase - -from functools import partial -import math -import numpy as np -from scipy.optimize import minimize as scipy_minimize -from ddt import ddt, idata, unpack -import rustworkx as rx - -from qiskit.algorithms import QAOA -from qiskit.algorithms.optimizers import COBYLA, NELDER_MEAD - -from qiskit.opflow import I, X, Z, PauliSumOp - -from qiskit import BasicAer, QuantumCircuit, QuantumRegister - -from qiskit.circuit import Parameter -from qiskit.quantum_info import Pauli -from qiskit.utils import QuantumInstance, algorithm_globals - -W1 = np.array([[0, 1, 0, 1], [1, 0, 1, 0], [0, 1, 0, 1], [1, 0, 1, 0]]) -P1 = 1 -M1 = (I ^ I ^ I ^ X) + (I ^ I ^ X ^ I) + (I ^ X ^ I ^ I) + (X ^ I ^ I ^ I) -S1 = {"0101", "1010"} - - -W2 = np.array( - [ - [0.0, 8.0, -9.0, 0.0], - [8.0, 0.0, 7.0, 9.0], - [-9.0, 7.0, 0.0, -8.0], - [0.0, 9.0, -8.0, 0.0], - ] -) -P2 = 1 -M2 = None -S2 = {"1011", "0100"} - -CUSTOM_SUPERPOSITION = [1 / math.sqrt(15)] * 15 + [0] - - -@ddt -class TestQAOA(QiskitAlgorithmsTestCase): - """Test QAOA with MaxCut.""" - - def setUp(self): - super().setUp() - self.seed = 10598 - with warnings.catch_warnings(): - warnings.filterwarnings("ignore", category=DeprecationWarning) - algorithm_globals.random_seed = self.seed - - with self.assertWarns(DeprecationWarning): - self.qasm_simulator = QuantumInstance( - BasicAer.get_backend("qasm_simulator"), - shots=4096, - seed_simulator=self.seed, - seed_transpiler=self.seed, - ) - self.statevector_simulator = QuantumInstance( - BasicAer.get_backend("statevector_simulator"), - seed_simulator=self.seed, - seed_transpiler=self.seed, - ) - - @idata( - [ - [W1, P1, M1, S1, False], - [W2, P2, M2, S2, False], - [W1, P1, M1, S1, True], - [W2, P2, M2, S2, True], - ] - ) - @unpack - def test_qaoa(self, w, prob, m, solutions, convert_to_matrix_op): - """QAOA test""" - self.log.debug("Testing %s-step QAOA with MaxCut on graph\n%s", prob, w) - - qubit_op, _ = self._get_operator(w) - - if convert_to_matrix_op: - with self.assertWarns(DeprecationWarning): - qubit_op = qubit_op.to_matrix_op() - - with self.assertWarns(DeprecationWarning): - qaoa = QAOA(COBYLA(), prob, mixer=m, quantum_instance=self.statevector_simulator) - result = qaoa.compute_minimum_eigenvalue(operator=qubit_op) - - x = self._sample_most_likely(result.eigenstate) - graph_solution = self._get_graph_solution(x) - self.assertIn(graph_solution, solutions) - - @idata( - [ - [W1, P1, S1, False], - [W2, P2, S2, False], - [W1, P1, S1, True], - [W2, P2, S2, True], - ] - ) - @unpack - def test_qaoa_qc_mixer(self, w, prob, solutions, convert_to_matrix_op): - """QAOA test with a mixer as a parameterized circuit""" - self.log.debug( - "Testing %s-step QAOA with MaxCut on graph with a mixer as a parameterized circuit\n%s", - prob, - w, - ) - - optimizer = COBYLA() - qubit_op, _ = self._get_operator(w) - if convert_to_matrix_op: - with self.assertWarns(DeprecationWarning): - qubit_op = qubit_op.to_matrix_op() - - num_qubits = qubit_op.num_qubits - mixer = QuantumCircuit(num_qubits) - theta = Parameter("θ") - mixer.rx(theta, range(num_qubits)) - - with self.assertWarns(DeprecationWarning): - qaoa = QAOA(optimizer, prob, mixer=mixer, quantum_instance=self.statevector_simulator) - - result = qaoa.compute_minimum_eigenvalue(operator=qubit_op) - - x = self._sample_most_likely(result.eigenstate) - graph_solution = self._get_graph_solution(x) - self.assertIn(graph_solution, solutions) - - def test_qaoa_qc_mixer_many_parameters(self): - """QAOA test with a mixer as a parameterized circuit with the num of parameters > 1.""" - optimizer = COBYLA() - qubit_op, _ = self._get_operator(W1) - - num_qubits = qubit_op.num_qubits - mixer = QuantumCircuit(num_qubits) - for i in range(num_qubits): - theta = Parameter("θ" + str(i)) - mixer.rx(theta, range(num_qubits)) - - with self.assertWarns(DeprecationWarning): - qaoa = QAOA(optimizer, reps=2, mixer=mixer, quantum_instance=self.statevector_simulator) - result = qaoa.compute_minimum_eigenvalue(operator=qubit_op) - - x = self._sample_most_likely(result.eigenstate) - self.log.debug(x) - graph_solution = self._get_graph_solution(x) - self.assertIn(graph_solution, S1) - - def test_qaoa_qc_mixer_no_parameters(self): - """QAOA test with a mixer as a parameterized circuit with zero parameters.""" - qubit_op, _ = self._get_operator(W1) - - num_qubits = qubit_op.num_qubits - mixer = QuantumCircuit(num_qubits) - # just arbitrary circuit - mixer.rx(np.pi / 2, range(num_qubits)) - - with self.assertWarns(DeprecationWarning): - qaoa = QAOA(COBYLA(), reps=1, mixer=mixer, quantum_instance=self.statevector_simulator) - result = qaoa.compute_minimum_eigenvalue(operator=qubit_op) - - # we just assert that we get a result, it is not meaningful. - self.assertIsNotNone(result.eigenstate) - - def test_change_operator_size(self): - """QAOA change operator size test""" - qubit_op, _ = self._get_operator( - np.array([[0, 1, 0, 1], [1, 0, 1, 0], [0, 1, 0, 1], [1, 0, 1, 0]]) - ) - with self.assertWarns(DeprecationWarning): - qaoa = QAOA(COBYLA(), 1, quantum_instance=self.statevector_simulator) - result = qaoa.compute_minimum_eigenvalue(operator=qubit_op) - - x = self._sample_most_likely(result.eigenstate) - graph_solution = self._get_graph_solution(x) - with self.subTest(msg="QAOA 4x4"): - self.assertIn(graph_solution, {"0101", "1010"}) - - qubit_op, _ = self._get_operator( - np.array( - [ - [0, 1, 0, 1, 0, 1], - [1, 0, 1, 0, 1, 0], - [0, 1, 0, 1, 0, 1], - [1, 0, 1, 0, 1, 0], - [0, 1, 0, 1, 0, 1], - [1, 0, 1, 0, 1, 0], - ] - ) - ) - with self.assertWarns(DeprecationWarning): - result = qaoa.compute_minimum_eigenvalue(operator=qubit_op) - - x = self._sample_most_likely(result.eigenstate) - graph_solution = self._get_graph_solution(x) - with self.subTest(msg="QAOA 6x6"): - self.assertIn(graph_solution, {"010101", "101010"}) - - @idata([[W2, S2, None], [W2, S2, [0.0, 0.0]], [W2, S2, [1.0, 0.8]]]) - @unpack - def test_qaoa_initial_point(self, w, solutions, init_pt): - """Check first parameter value used is initial point as expected""" - qubit_op, _ = self._get_operator(w) - - first_pt = [] - - def cb_callback(eval_count, parameters, mean, std): - nonlocal first_pt - if eval_count == 1: - first_pt = list(parameters) - - with self.assertWarns(DeprecationWarning): - qaoa = QAOA( - COBYLA(), - initial_point=init_pt, - callback=cb_callback, - quantum_instance=self.statevector_simulator, - ) - - result = qaoa.compute_minimum_eigenvalue(operator=qubit_op) - - x = self._sample_most_likely(result.eigenstate) - graph_solution = self._get_graph_solution(x) - - with self.subTest("Initial Point"): - # If None the preferred random initial point of QAOA variational form - if init_pt is None: - self.assertLess(result.eigenvalue, -0.97) - else: - self.assertListEqual(init_pt, first_pt) - - with self.subTest("Solution"): - self.assertIn(graph_solution, solutions) - - @idata([[W2, None], [W2, [1.0] + 15 * [0.0]], [W2, CUSTOM_SUPERPOSITION]]) - @unpack - def test_qaoa_initial_state(self, w, init_state): - """QAOA initial state test""" - optimizer = COBYLA() - qubit_op, _ = self._get_operator(w) - - init_pt = np.asarray([0.0, 0.0]) # Avoid generating random initial point - - if init_state is None: - initial_state = None - else: - initial_state = QuantumCircuit(QuantumRegister(4, "q")) - initial_state.initialize(init_state, initial_state.qubits) - - zero_init_state = QuantumCircuit(QuantumRegister(qubit_op.num_qubits, "q")) - - with self.assertWarns(DeprecationWarning): - qaoa_zero_init_state = QAOA( - optimizer=optimizer, - initial_state=zero_init_state, - initial_point=init_pt, - quantum_instance=self.statevector_simulator, - ) - qaoa = QAOA( - optimizer=optimizer, - initial_state=initial_state, - initial_point=init_pt, - quantum_instance=self.statevector_simulator, - ) - zero_circuits = qaoa_zero_init_state.construct_circuit(init_pt, qubit_op) - custom_circuits = qaoa.construct_circuit(init_pt, qubit_op) - - self.assertEqual(len(zero_circuits), len(custom_circuits)) - - for zero_circ, custom_circ in zip(zero_circuits, custom_circuits): - - z_length = len(zero_circ.data) - c_length = len(custom_circ.data) - - self.assertGreaterEqual(c_length, z_length) - self.assertTrue(zero_circ.data == custom_circ.data[-z_length:]) - - custom_init_qc = QuantumCircuit(custom_circ.num_qubits) - custom_init_qc.data = custom_circ.data[0 : c_length - z_length] - - if initial_state is None: - original_init_qc = QuantumCircuit(qubit_op.num_qubits) - original_init_qc.h(range(qubit_op.num_qubits)) - else: - original_init_qc = initial_state - - with self.assertWarns(DeprecationWarning): - job_init_state = self.statevector_simulator.execute(original_init_qc) - job_qaoa_init_state = self.statevector_simulator.execute(custom_init_qc) - - statevector_original = job_init_state.get_statevector(original_init_qc) - statevector_custom = job_qaoa_init_state.get_statevector(custom_init_qc) - - self.assertListEqual(statevector_original.tolist(), statevector_custom.tolist()) - - def test_qaoa_random_initial_point(self): - """QAOA random initial point""" - with warnings.catch_warnings(): - warnings.filterwarnings("ignore", category=DeprecationWarning) - w = rx.adjacency_matrix( - rx.undirected_gnp_random_graph(5, 0.5, seed=algorithm_globals.random_seed) - ) - qubit_op, _ = self._get_operator(w) - - with self.assertWarns(DeprecationWarning): - qaoa = QAOA( - optimizer=NELDER_MEAD(disp=True), reps=1, quantum_instance=self.qasm_simulator - ) - result = qaoa.compute_minimum_eigenvalue(operator=qubit_op) - - self.assertLess(result.eigenvalue, -0.97) - - def test_qaoa_construct_circuit_update(self): - """Test updating operators with QAOA construct_circuit""" - with self.assertWarns(DeprecationWarning): - qaoa = QAOA() - ref = qaoa.construct_circuit([0, 0], I ^ Z)[0] - circ2 = qaoa.construct_circuit([0, 0], I ^ Z)[0] - self.assertEqual(circ2, ref) - circ3 = qaoa.construct_circuit([0, 0], Z ^ I)[0] - self.assertNotEqual(circ3, ref) - circ4 = qaoa.construct_circuit([0, 0], I ^ Z)[0] - self.assertEqual(circ4, ref) - - def test_optimizer_scipy_callable(self): - """Test passing a SciPy optimizer directly as callable.""" - with self.assertWarns(DeprecationWarning): - qaoa = QAOA( - optimizer=partial(scipy_minimize, method="Nelder-Mead", options={"maxiter": 2}), - quantum_instance=self.statevector_simulator, - ) - result = qaoa.compute_minimum_eigenvalue(Z) - self.assertEqual(result.cost_function_evals, 4) - - def _get_operator(self, weight_matrix): - """Generate Hamiltonian for the max-cut problem of a graph. - - Args: - weight_matrix (numpy.ndarray) : adjacency matrix. - - Returns: - PauliSumOp: operator for the Hamiltonian - float: a constant shift for the obj function. - - """ - num_nodes = weight_matrix.shape[0] - pauli_list = [] - shift = 0 - for i in range(num_nodes): - for j in range(i): - if weight_matrix[i, j] != 0: - x_p = np.zeros(num_nodes, dtype=bool) - z_p = np.zeros(num_nodes, dtype=bool) - z_p[i] = True - z_p[j] = True - pauli_list.append([0.5 * weight_matrix[i, j], Pauli((z_p, x_p))]) - shift -= 0.5 * weight_matrix[i, j] - opflow_list = [(pauli[1].to_label(), pauli[0]) for pauli in pauli_list] - - with self.assertWarns(DeprecationWarning): - return PauliSumOp.from_list(opflow_list), shift - - def _get_graph_solution(self, x: np.ndarray) -> str: - """Get graph solution from binary string. - - Args: - x : binary string as numpy array. - - Returns: - a graph solution as string. - """ - - return "".join([str(int(i)) for i in 1 - x]) - - def _sample_most_likely(self, state_vector): - """Compute the most likely binary string from state vector. - Args: - state_vector (numpy.ndarray or dict): state vector or counts. - - Returns: - numpy.ndarray: binary string as numpy.ndarray of ints. - """ - n = int(np.log2(state_vector.shape[0])) - k = np.argmax(np.abs(state_vector)) - x = np.zeros(n) - for i in range(n): - x[i] = k % 2 - k >>= 1 - return x - - -if __name__ == "__main__": - unittest.main() diff --git a/test/python/algorithms/test_validation.py b/test/python/algorithms/test_validation.py deleted file mode 100644 index 90a1b9eed143..000000000000 --- a/test/python/algorithms/test_validation.py +++ /dev/null @@ -1,94 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2019, 2020. -# -# 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 Validation""" - -import unittest - -from test.python.algorithms import QiskitAlgorithmsTestCase -from qiskit.utils.validation import ( - validate_in_set, - validate_min, - validate_min_exclusive, - validate_max, - validate_max_exclusive, - validate_range, - validate_range_exclusive, - validate_range_exclusive_min, - validate_range_exclusive_max, -) - - -class TestValidation(QiskitAlgorithmsTestCase): - """Validation tests.""" - - def test_validate_in_set(self): - """validate in set test""" - test_value = "value1" - with self.assertWarns(DeprecationWarning): - validate_in_set("test_value", test_value, {"value1", "value2"}) - with self.assertRaises(ValueError): - validate_in_set("test_value", test_value, {"value3", "value4"}) - - def test_validate_min(self): - """validate min test""" - test_value = 2.5 - with self.assertWarns(DeprecationWarning): - validate_min("test_value", test_value, -1) - validate_min("test_value", test_value, 2.5) - with self.assertRaises(ValueError): - validate_min("test_value", test_value, 4) - validate_min_exclusive("test_value", test_value, -1) - with self.assertRaises(ValueError): - validate_min_exclusive("test_value", test_value, 2.5) - with self.assertRaises(ValueError): - validate_min_exclusive("test_value", test_value, 4) - - def test_validate_max(self): - """validate max test""" - test_value = 2.5 - with self.assertWarns(DeprecationWarning): - with self.assertRaises(ValueError): - validate_max("test_value", test_value, -1) - validate_max("test_value", test_value, 2.5) - validate_max("test_value", test_value, 4) - with self.assertRaises(ValueError): - validate_max_exclusive("test_value", test_value, -1) - with self.assertRaises(ValueError): - validate_max_exclusive("test_value", test_value, 2.5) - validate_max_exclusive("test_value", test_value, 4) - - def test_validate_range(self): - """validate range test""" - test_value = 2.5 - with self.assertWarns(DeprecationWarning): - with self.assertRaises(ValueError): - validate_range("test_value", test_value, 0, 2) - with self.assertRaises(ValueError): - validate_range("test_value", test_value, 3, 4) - validate_range("test_value", test_value, 2.5, 3) - validate_range_exclusive("test_value", test_value, 0, 3) - with self.assertRaises(ValueError): - validate_range_exclusive("test_value", test_value, 0, 2.5) - validate_range_exclusive("test_value", test_value, 2.5, 3) - validate_range_exclusive_min("test_value", test_value, 0, 3) - with self.assertRaises(ValueError): - validate_range_exclusive_min("test_value", test_value, 2.5, 3) - validate_range_exclusive_min("test_value", test_value, 0, 2.5) - validate_range_exclusive_max("test_value", test_value, 2.5, 3) - with self.assertRaises(ValueError): - validate_range_exclusive_max("test_value", test_value, 0, 2.5) - validate_range_exclusive_max("test_value", test_value, 2.5, 3) - - -if __name__ == "__main__": - unittest.main() diff --git a/test/python/algorithms/test_vqd.py b/test/python/algorithms/test_vqd.py deleted file mode 100644 index e153ba50ddb7..000000000000 --- a/test/python/algorithms/test_vqd.py +++ /dev/null @@ -1,663 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2018, 2023. -# -# 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 VQD""" - -import unittest -import warnings - -from test.python.algorithms import QiskitAlgorithmsTestCase - -import numpy as np -from ddt import data, ddt, unpack - -from qiskit import BasicAer, QuantumCircuit -from qiskit.algorithms import VQD, AlgorithmError -from qiskit.algorithms.optimizers import ( - COBYLA, - L_BFGS_B, - SLSQP, -) -from qiskit.circuit.library import EfficientSU2, RealAmplitudes, TwoLocal -from qiskit.exceptions import MissingOptionalLibraryError -from qiskit.opflow import ( - AerPauliExpectation, - I, - MatrixExpectation, - MatrixOp, - PauliExpectation, - PauliSumOp, - PrimitiveOp, - X, - Z, -) - -from qiskit.utils import QuantumInstance, algorithm_globals, has_aer -from qiskit.test import slow_test - - -if has_aer(): - from qiskit import Aer - - -@ddt -class TestVQD(QiskitAlgorithmsTestCase): - """Test VQD""" - - def setUp(self): - super().setUp() - self.seed = 50 - with warnings.catch_warnings(): - warnings.filterwarnings("ignore", category=DeprecationWarning) - algorithm_globals.random_seed = self.seed - self.h2_energy = -1.85727503 - self.h2_energy_excited = [-1.85727503, -1.24458455] - - self.test_results = [-3, -1] - - self.ryrz_wavefunction = TwoLocal( - rotation_blocks=["ry", "rz"], entanglement_blocks="cz", reps=1 - ) - self.ry_wavefunction = TwoLocal(rotation_blocks="ry", entanglement_blocks="cz") - - with self.assertWarns(DeprecationWarning): - self.h2_op = ( - -1.052373245772859 * (I ^ I) - + 0.39793742484318045 * (I ^ Z) - - 0.39793742484318045 * (Z ^ I) - - 0.01128010425623538 * (Z ^ Z) - + 0.18093119978423156 * (X ^ X) - ) - self.test_op = MatrixOp(np.diagflat([3, 5, -1, 0.8, 0.2, 2, 1, -3])).to_pauli_op() - self.qasm_simulator = QuantumInstance( - BasicAer.get_backend("qasm_simulator"), - shots=2048, - seed_simulator=self.seed, - seed_transpiler=self.seed, - ) - self.statevector_simulator = QuantumInstance( - BasicAer.get_backend("statevector_simulator"), - shots=1, - seed_simulator=self.seed, - seed_transpiler=self.seed, - ) - - @slow_test - def test_basic_aer_statevector(self): - """Test the VQD on BasicAer's statevector simulator.""" - wavefunction = self.ryrz_wavefunction - - with self.assertWarns(DeprecationWarning): - vqd = VQD( - k=2, - ansatz=wavefunction, - optimizer=COBYLA(), - quantum_instance=QuantumInstance( - BasicAer.get_backend("statevector_simulator"), - basis_gates=["u1", "u2", "u3", "cx", "id"], - coupling_map=[[0, 1]], - seed_simulator=algorithm_globals.random_seed, - seed_transpiler=algorithm_globals.random_seed, - ), - ) - - result = vqd.compute_eigenvalues(operator=self.h2_op) - - with self.subTest(msg="test eigenvalue"): - np.testing.assert_array_almost_equal( - result.eigenvalues.real, self.h2_energy_excited, decimal=1 - ) - - with self.subTest(msg="test dimension of optimal point"): - self.assertEqual(len(result.optimal_point[-1]), 8) - - with self.subTest(msg="assert cost_function_evals is set"): - self.assertIsNotNone(result.cost_function_evals) - - with self.subTest(msg="assert optimizer_time is set"): - self.assertIsNotNone(result.optimizer_time) - - def test_mismatching_num_qubits(self): - """Ensuring circuit and operator mismatch is caught""" - wavefunction = QuantumCircuit(1) - optimizer = SLSQP(maxiter=50) - - with self.assertWarns(DeprecationWarning): - vqd = VQD( - k=1, - ansatz=wavefunction, - optimizer=optimizer, - quantum_instance=self.statevector_simulator, - ) - with self.assertRaises(AlgorithmError): - _ = vqd.compute_eigenvalues(operator=self.h2_op) - - @data( - (MatrixExpectation(), 1), - (AerPauliExpectation(), 1), - (PauliExpectation(), 2), - ) - @unpack - def test_construct_circuit(self, expectation, num_circuits): - """Test construct circuits returns QuantumCircuits and the right number of them.""" - try: - wavefunction = EfficientSU2(2, reps=1) - - with self.assertWarns(DeprecationWarning): - vqd = VQD(k=2, ansatz=wavefunction, expectation=expectation) - params = [0] * wavefunction.num_parameters - circuits = vqd.construct_circuit(parameter=params, operator=self.h2_op) - self.assertEqual(len(circuits), num_circuits) - - for circuit in circuits: - self.assertIsInstance(circuit, QuantumCircuit) - - except MissingOptionalLibraryError as ex: - self.skipTest(str(ex)) - return - - def test_missing_varform_params(self): - """Test specifying a variational form with no parameters raises an error.""" - - circuit = QuantumCircuit(self.h2_op.num_qubits) - with self.assertWarns(DeprecationWarning): - vqd = VQD( - k=1, ansatz=circuit, quantum_instance=BasicAer.get_backend("statevector_simulator") - ) - with self.assertRaises(RuntimeError): - vqd.compute_eigenvalues(operator=self.h2_op) - - def test_basic_aer_qasm(self): - """Test the VQD on BasicAer's QASM simulator.""" - optimizer = COBYLA(maxiter=1000) - wavefunction = self.ry_wavefunction - - with self.assertWarns(DeprecationWarning): - vqd = VQD( - ansatz=wavefunction, - optimizer=optimizer, - max_evals_grouped=1, - quantum_instance=self.qasm_simulator, - ) - # TODO benchmark this later. - result = vqd.compute_eigenvalues(operator=self.h2_op) - - np.testing.assert_array_almost_equal( - result.eigenvalues.real, self.h2_energy_excited, decimal=1 - ) - - @unittest.skipUnless(has_aer(), "qiskit-aer doesn't appear to be installed.") - def test_with_aer_statevector(self): - """Test VQD with Aer's statevector_simulator.""" - backend = Aer.get_backend("aer_simulator_statevector") - wavefunction = self.ry_wavefunction - optimizer = L_BFGS_B() - - with self.assertWarns(DeprecationWarning): - quantum_instance = QuantumInstance( - backend, - seed_simulator=algorithm_globals.random_seed, - seed_transpiler=algorithm_globals.random_seed, - ) - with self.assertWarns(DeprecationWarning): - vqd = VQD( - k=2, - ansatz=wavefunction, - optimizer=optimizer, - max_evals_grouped=1, - quantum_instance=quantum_instance, - ) - result = vqd.compute_eigenvalues(operator=self.h2_op) - - np.testing.assert_array_almost_equal( - result.eigenvalues.real, self.h2_energy_excited, decimal=2 - ) - - @unittest.skipUnless(has_aer(), "qiskit-aer doesn't appear to be installed.") - def test_with_aer_qasm(self): - """Test VQD with Aer's qasm_simulator.""" - backend = Aer.get_backend("aer_simulator") - optimizer = COBYLA(maxiter=1000) - wavefunction = self.ry_wavefunction - - with self.assertWarns(DeprecationWarning): - quantum_instance = QuantumInstance( - backend, - seed_simulator=algorithm_globals.random_seed, - seed_transpiler=algorithm_globals.random_seed, - ) - - with self.assertWarns(DeprecationWarning): - vqd = VQD( - k=2, - ansatz=wavefunction, - optimizer=optimizer, - expectation=PauliExpectation(), - quantum_instance=quantum_instance, - ) - result = vqd.compute_eigenvalues(operator=self.h2_op) - - np.testing.assert_array_almost_equal( - result.eigenvalues.real, self.h2_energy_excited, decimal=1 - ) - - @unittest.skipUnless(has_aer(), "qiskit-aer doesn't appear to be installed.") - def test_with_aer_qasm_snapshot_mode(self): - """Test the VQD using Aer's qasm_simulator snapshot mode.""" - - backend = Aer.get_backend("aer_simulator") - optimizer = COBYLA(maxiter=400) - wavefunction = self.ryrz_wavefunction - - with self.assertWarns(DeprecationWarning): - quantum_instance = QuantumInstance( - backend, - shots=100, - seed_simulator=algorithm_globals.random_seed, - seed_transpiler=algorithm_globals.random_seed, - ) - with self.assertWarns(DeprecationWarning): - vqd = VQD( - k=2, - ansatz=wavefunction, - optimizer=optimizer, - expectation=AerPauliExpectation(), - quantum_instance=quantum_instance, - ) - result = vqd.compute_eigenvalues(operator=self.test_op) - - np.testing.assert_array_almost_equal(result.eigenvalues.real, self.test_results, decimal=1) - - def test_callback(self): - """Test the callback on VQD.""" - history = {"eval_count": [], "parameters": [], "mean": [], "std": [], "step": []} - - def store_intermediate_result(eval_count, parameters, mean, std, step): - history["eval_count"].append(eval_count) - history["parameters"].append(parameters) - history["mean"].append(mean) - history["std"].append(std) - history["step"].append(step) - - optimizer = COBYLA(maxiter=3) - wavefunction = self.ry_wavefunction - - with self.assertWarns(DeprecationWarning): - vqd = VQD( - ansatz=wavefunction, - optimizer=optimizer, - callback=store_intermediate_result, - quantum_instance=self.qasm_simulator, - ) - vqd.compute_eigenvalues(operator=self.h2_op) - - self.assertTrue(all(isinstance(count, int) for count in history["eval_count"])) - self.assertTrue(all(isinstance(mean, float) for mean in history["mean"])) - self.assertTrue(all(isinstance(std, float) for std in history["std"])) - self.assertTrue(all(isinstance(count, int) for count in history["step"])) - for params in history["parameters"]: - self.assertTrue(all(isinstance(param, float) for param in params)) - - ref_eval_count = [1, 2, 3, 1, 2, 3] - ref_mean = [-1.063, -1.457, -1.360, 37.340, 48.543, 28.586] - ref_std = [0.011, 0.010, 0.014, 0.011, 0.010, 0.015] - ref_step = [1, 1, 1, 2, 2, 2] - - np.testing.assert_array_almost_equal(history["eval_count"], ref_eval_count, decimal=0) - np.testing.assert_array_almost_equal(history["mean"], ref_mean, decimal=2) - np.testing.assert_array_almost_equal(history["std"], ref_std, decimal=2) - np.testing.assert_array_almost_equal(history["step"], ref_step, decimal=0) - - def test_reuse(self): - """Test re-using a VQD algorithm instance.""" - - with self.assertWarns(DeprecationWarning): - vqd = VQD(k=1) - - with self.subTest(msg="assert running empty raises AlgorithmError"): - with self.assertWarns(DeprecationWarning), self.assertRaises(AlgorithmError): - _ = vqd.compute_eigenvalues(operator=self.h2_op) - - ansatz = TwoLocal(rotation_blocks=["ry", "rz"], entanglement_blocks="cz") - vqd.ansatz = ansatz - - with self.subTest(msg="assert missing operator raises AlgorithmError"): - with self.assertWarns(DeprecationWarning), self.assertRaises(AlgorithmError): - _ = vqd.compute_eigenvalues(operator=self.h2_op) - - with self.assertWarns(DeprecationWarning): - vqd.expectation = MatrixExpectation() - vqd.quantum_instance = self.statevector_simulator - - with self.subTest(msg="assert VQE works once all info is available"): - with self.assertWarns(DeprecationWarning): - result = vqd.compute_eigenvalues(operator=self.h2_op) - np.testing.assert_array_almost_equal(result.eigenvalues.real, self.h2_energy, decimal=2) - - with self.assertWarns(DeprecationWarning): - operator = PrimitiveOp( - np.array([[1, 0, 0, 0], [0, -1, 0, 0], [0, 0, 2, 0], [0, 0, 0, 3]]) - ) - - with self.subTest(msg="assert minimum eigensolver interface works"): - with self.assertWarns(DeprecationWarning): - result = vqd.compute_eigenvalues(operator=operator) - self.assertAlmostEqual(result.eigenvalues.real[0], -1.0, places=5) - - def test_vqd_optimizer(self): - """Test running same VQD twice to re-use optimizer, then switch optimizer""" - with self.assertWarns(DeprecationWarning): - vqd = VQD( - k=2, - optimizer=SLSQP(), - quantum_instance=QuantumInstance(BasicAer.get_backend("statevector_simulator")), - ) - - def run_check(): - with self.assertWarns(DeprecationWarning): - result = vqd.compute_eigenvalues(operator=self.h2_op) - - np.testing.assert_array_almost_equal( - result.eigenvalues.real, self.h2_energy_excited, decimal=3 - ) - - run_check() - - with self.subTest("Optimizer re-use"): - run_check() - - with self.subTest("Optimizer replace"): - vqd.optimizer = L_BFGS_B() - run_check() - - @data(MatrixExpectation(), None) - def test_backend_change(self, user_expectation): - """Test that VQE works when backend changes.""" - - with self.assertWarns(DeprecationWarning): - vqd = VQD( - k=1, - ansatz=TwoLocal(rotation_blocks=["ry", "rz"], entanglement_blocks="cz"), - optimizer=SLSQP(maxiter=2), - expectation=user_expectation, - quantum_instance=BasicAer.get_backend("statevector_simulator"), - ) - result0 = vqd.compute_eigenvalues(operator=self.h2_op) - if user_expectation is not None: - with self.subTest("User expectation kept."): - self.assertEqual(vqd.expectation, user_expectation) - - with self.assertWarns(DeprecationWarning): - vqd.quantum_instance = BasicAer.get_backend("qasm_simulator") - # works also if no expectation is set, since it will be determined automatically - - result1 = vqd.compute_eigenvalues(operator=self.h2_op) - - if user_expectation is not None: - with self.subTest("Change backend with user expectation, it is kept."): - self.assertEqual(vqd.expectation, user_expectation) - - with self.subTest("Check results."): - self.assertEqual(len(result0.optimal_point), len(result1.optimal_point)) - - def test_set_ansatz_to_none(self): - """Tests that setting the ansatz to None results in the default behavior""" - with self.assertWarns(DeprecationWarning): - vqd = VQD( - k=1, - ansatz=self.ryrz_wavefunction, - optimizer=L_BFGS_B(), - quantum_instance=self.statevector_simulator, - ) - vqd.ansatz = None - self.assertIsInstance(vqd.ansatz, RealAmplitudes) - - def test_set_optimizer_to_none(self): - """Tests that setting the optimizer to None results in the default behavior""" - - with self.assertWarns(DeprecationWarning): - vqd = VQD( - k=1, - ansatz=self.ryrz_wavefunction, - optimizer=L_BFGS_B(), - quantum_instance=self.statevector_simulator, - ) - vqd.optimizer = None - self.assertIsInstance(vqd.optimizer, SLSQP) - - def test_aux_operators_list(self): - """Test list-based aux_operators.""" - wavefunction = self.ry_wavefunction - - with self.assertWarns(DeprecationWarning): - vqd = VQD(k=2, ansatz=wavefunction, quantum_instance=self.statevector_simulator) - - # Start with an empty list - result = vqd.compute_eigenvalues(self.h2_op, aux_operators=[]) - - np.testing.assert_array_almost_equal( - result.eigenvalues.real, self.h2_energy_excited, decimal=2 - ) - self.assertIsNone(result.aux_operator_eigenvalues) - - # Go again with two auxiliary operators - with self.assertWarns(DeprecationWarning): - aux_op1 = PauliSumOp.from_list([("II", 2.0)]) - aux_op2 = PauliSumOp.from_list([("II", 0.5), ("ZZ", 0.5), ("YY", 0.5), ("XX", -0.5)]) - aux_ops = [aux_op1, aux_op2] - result = vqd.compute_eigenvalues(self.h2_op, aux_operators=aux_ops) - - np.testing.assert_array_almost_equal( - result.eigenvalues.real, self.h2_energy_excited, decimal=2 - ) - self.assertEqual(len(result.aux_operator_eigenvalues), 2) - # expectation values - self.assertAlmostEqual(result.aux_operator_eigenvalues[0][0][0], 2, places=2) - self.assertAlmostEqual(result.aux_operator_eigenvalues[0][1][0], 0, places=2) - # standard deviations - self.assertAlmostEqual(result.aux_operator_eigenvalues[0][1][1], 0.0) - self.assertAlmostEqual(result.aux_operator_eigenvalues[0][1][1], 0.0) - - # Go again with additional None and zero operators - extra_ops = [*aux_ops, None, 0] - with self.assertWarns(DeprecationWarning): - result = vqd.compute_eigenvalues(self.h2_op, aux_operators=extra_ops) - - np.testing.assert_array_almost_equal( - result.eigenvalues.real, self.h2_energy_excited, decimal=2 - ) - self.assertEqual(len(result.aux_operator_eigenvalues), 2) - # expectation values - self.assertAlmostEqual(result.aux_operator_eigenvalues[0][0][0], 2, places=2) - self.assertAlmostEqual(result.aux_operator_eigenvalues[0][1][0], 0, places=2) - self.assertEqual(result.aux_operator_eigenvalues[0][2][0], 0.0) - self.assertEqual(result.aux_operator_eigenvalues[0][3][0], 0.0) - # standard deviations - self.assertAlmostEqual(result.aux_operator_eigenvalues[0][0][1], 0.0) - self.assertAlmostEqual(result.aux_operator_eigenvalues[0][1][1], 0.0) - self.assertEqual(result.aux_operator_eigenvalues[0][3][1], 0.0) - - def test_aux_operators_dict(self): - """Test dictionary compatibility of aux_operators""" - wavefunction = self.ry_wavefunction - - with self.assertWarns(DeprecationWarning): - vqd = VQD(ansatz=wavefunction, quantum_instance=self.statevector_simulator) - - # Start with an empty dictionary - with self.assertWarns(DeprecationWarning): - result = vqd.compute_eigenvalues(self.h2_op, aux_operators={}) - - np.testing.assert_array_almost_equal( - result.eigenvalues.real, self.h2_energy_excited, decimal=2 - ) - self.assertIsNone(result.aux_operator_eigenvalues) - - # Go again with two auxiliary operators - with self.assertWarns(DeprecationWarning): - aux_op1 = PauliSumOp.from_list([("II", 2.0)]) - aux_op2 = PauliSumOp.from_list([("II", 0.5), ("ZZ", 0.5), ("YY", 0.5), ("XX", -0.5)]) - aux_ops = {"aux_op1": aux_op1, "aux_op2": aux_op2} - with self.assertWarns(DeprecationWarning): - result = vqd.compute_eigenvalues(self.h2_op, aux_operators=aux_ops) - - self.assertEqual(len(result.eigenvalues), 2) - self.assertEqual(len(result.eigenstates), 2) - self.assertEqual(result.eigenvalues.dtype, np.complex128) - self.assertAlmostEqual(result.eigenvalues[0], -1.85727503) - self.assertEqual(len(result.aux_operator_eigenvalues), 2) - self.assertEqual(len(result.aux_operator_eigenvalues[0]), 2) - # expectation values - self.assertAlmostEqual(result.aux_operator_eigenvalues[0]["aux_op1"][0], 2, places=6) - self.assertAlmostEqual(result.aux_operator_eigenvalues[0]["aux_op2"][0], 0, places=1) - # standard deviations - self.assertAlmostEqual(result.aux_operator_eigenvalues[0]["aux_op1"][1], 0.0) - self.assertAlmostEqual(result.aux_operator_eigenvalues[0]["aux_op2"][1], 0.0) - - # Go again with additional None and zero operators - extra_ops = {**aux_ops, "None_operator": None, "zero_operator": 0} - with self.assertWarns(DeprecationWarning): - result = vqd.compute_eigenvalues(self.h2_op, aux_operators=extra_ops) - self.assertEqual(len(result.eigenvalues), 2) - self.assertEqual(len(result.eigenstates), 2) - self.assertEqual(result.eigenvalues.dtype, np.complex128) - self.assertAlmostEqual(result.eigenvalues[0], -1.85727503) - self.assertEqual(len(result.aux_operator_eigenvalues), 2) - self.assertEqual(len(result.aux_operator_eigenvalues[0]), 3) - # expectation values - self.assertAlmostEqual(result.aux_operator_eigenvalues[0]["aux_op1"][0], 2, places=6) - self.assertAlmostEqual(result.aux_operator_eigenvalues[0]["aux_op2"][0], 0, places=6) - self.assertEqual(result.aux_operator_eigenvalues[0]["zero_operator"][0], 0.0) - self.assertTrue("None_operator" not in result.aux_operator_eigenvalues[0].keys()) - # standard deviations - self.assertAlmostEqual(result.aux_operator_eigenvalues[0]["aux_op1"][1], 0.0) - self.assertAlmostEqual(result.aux_operator_eigenvalues[0]["aux_op2"][1], 0.0) - self.assertAlmostEqual(result.aux_operator_eigenvalues[0]["zero_operator"][1], 0.0) - - def test_aux_operator_std_dev_pauli(self): - """Test non-zero standard deviations of aux operators with PauliExpectation.""" - wavefunction = self.ry_wavefunction - - with self.assertWarns(DeprecationWarning): - vqd = VQD( - ansatz=wavefunction, - expectation=PauliExpectation(), - initial_point=[ - 1.70256666, - -5.34843975, - -0.39542903, - 5.99477786, - -2.74374986, - -4.85284669, - 0.2442925, - -1.51638917, - ], - optimizer=COBYLA(maxiter=0), - quantum_instance=self.qasm_simulator, - ) - - with self.assertWarns(DeprecationWarning): - # Go again with two auxiliary operators - aux_op1 = PauliSumOp.from_list([("II", 2.0)]) - aux_op2 = PauliSumOp.from_list([("II", 0.5), ("ZZ", 0.5), ("YY", 0.5), ("XX", -0.5)]) - aux_ops = [aux_op1, aux_op2] - with self.assertWarns(DeprecationWarning): - result = vqd.compute_eigenvalues(self.h2_op, aux_operators=aux_ops) - - self.assertEqual(len(result.aux_operator_eigenvalues), 2) - # expectation values - self.assertAlmostEqual(result.aux_operator_eigenvalues[0][0][0], 2.0, places=1) - self.assertAlmostEqual( - result.aux_operator_eigenvalues[0][1][0], 0.0019531249999999445, places=1 - ) - # standard deviations - self.assertAlmostEqual(result.aux_operator_eigenvalues[0][0][1], 0.0) - self.assertAlmostEqual( - result.aux_operator_eigenvalues[0][1][1], 0.015183867579396111, places=1 - ) - - # Go again with additional None and zero operators - aux_ops = [*aux_ops, None, 0] - with self.assertWarns(DeprecationWarning): - result = vqd.compute_eigenvalues(self.h2_op, aux_operators=aux_ops) - - self.assertEqual(len(result.aux_operator_eigenvalues[0]), 4) - # expectation values - self.assertAlmostEqual(result.aux_operator_eigenvalues[0][0][0], 2.0, places=1) - self.assertAlmostEqual( - result.aux_operator_eigenvalues[0][1][0], 0.0019531249999999445, places=1 - ) - self.assertEqual(result.aux_operator_eigenvalues[0][2][0], 0.0) - self.assertEqual(result.aux_operator_eigenvalues[0][3][0], 0.0) - # # standard deviations - self.assertAlmostEqual(result.aux_operator_eigenvalues[0][0][1], 0.0) - self.assertAlmostEqual( - result.aux_operator_eigenvalues[0][1][1], 0.01548658094658011, places=1 - ) - self.assertAlmostEqual(result.aux_operator_eigenvalues[0][2][1], 0.0) - self.assertAlmostEqual(result.aux_operator_eigenvalues[0][3][1], 0.0) - - @unittest.skipUnless(has_aer(), "qiskit-aer doesn't appear to be installed.") - def test_aux_operator_std_dev_aer_pauli(self): - """Test non-zero standard deviations of aux operators with AerPauliExpectation.""" - wavefunction = self.ry_wavefunction - with self.assertWarns(DeprecationWarning): - vqd = VQD( - ansatz=wavefunction, - expectation=AerPauliExpectation(), - optimizer=COBYLA(maxiter=0), - quantum_instance=QuantumInstance( - backend=Aer.get_backend("qasm_simulator"), - shots=1, - seed_simulator=algorithm_globals.random_seed, - seed_transpiler=algorithm_globals.random_seed, - ), - ) - - with self.assertWarns(DeprecationWarning): - # Go again with two auxiliary operators - aux_op1 = PauliSumOp.from_list([("II", 2.0)]) - aux_op2 = PauliSumOp.from_list([("II", 0.5), ("ZZ", 0.5), ("YY", 0.5), ("XX", -0.5)]) - aux_ops = [aux_op1, aux_op2] - with self.assertWarns(DeprecationWarning): - result = vqd.compute_eigenvalues(self.h2_op, aux_operators=aux_ops) - self.assertEqual(len(result.aux_operator_eigenvalues), 2) - # expectation values - self.assertAlmostEqual(result.aux_operator_eigenvalues[0][0][0], 2.0, places=1) - self.assertAlmostEqual( - result.aux_operator_eigenvalues[0][1][0], 0.6698863565455391, places=1 - ) - # standard deviations - self.assertAlmostEqual(result.aux_operator_eigenvalues[0][0][1], 0.0) - self.assertAlmostEqual(result.aux_operator_eigenvalues[0][1][1], 0.0, places=6) - - # Go again with additional None and zero operators - aux_ops = [*aux_ops, None, 0] - with self.assertWarns(DeprecationWarning): - result = vqd.compute_eigenvalues(self.h2_op, aux_operators=aux_ops) - self.assertEqual(len(result.aux_operator_eigenvalues[-1]), 4) - # expectation values - self.assertAlmostEqual(result.aux_operator_eigenvalues[0][0][0], 2.0, places=6) - self.assertAlmostEqual( - result.aux_operator_eigenvalues[0][1][0], 0.6036400943063891, places=6 - ) - self.assertEqual(result.aux_operator_eigenvalues[0][2][0], 0.0) - self.assertEqual(result.aux_operator_eigenvalues[0][3][0], 0.0) - # standard deviations - self.assertAlmostEqual(result.aux_operator_eigenvalues[0][0][1], 0.0) - self.assertAlmostEqual(result.aux_operator_eigenvalues[0][1][1], 0.0, places=6) - self.assertAlmostEqual(result.aux_operator_eigenvalues[0][2][1], 0.0) - self.assertAlmostEqual(result.aux_operator_eigenvalues[0][3][1], 0.0) - - -if __name__ == "__main__": - unittest.main() diff --git a/test/python/algorithms/test_vqe.py b/test/python/algorithms/test_vqe.py deleted file mode 100644 index 781728400f2b..000000000000 --- a/test/python/algorithms/test_vqe.py +++ /dev/null @@ -1,856 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2018, 2023. -# -# 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 VQE""" - -import logging -import unittest -import warnings - -from test.python.algorithms import QiskitAlgorithmsTestCase -from test.python.transpiler._dummy_passes import DummyAP - -from functools import partial -import numpy as np -from scipy.optimize import minimize as scipy_minimize -from ddt import data, ddt, unpack - -from qiskit import BasicAer, QuantumCircuit -from qiskit.algorithms import VQE, AlgorithmError -from qiskit.algorithms.optimizers import ( - CG, - COBYLA, - L_BFGS_B, - P_BFGS, - QNSPSA, - SLSQP, - SPSA, - TNC, - OptimizerResult, -) -from qiskit.circuit.library import EfficientSU2, RealAmplitudes, TwoLocal -from qiskit.exceptions import MissingOptionalLibraryError -from qiskit.opflow import ( - AerPauliExpectation, - Gradient, - I, - MatrixExpectation, - PauliExpectation, - PauliSumOp, - PrimitiveOp, - TwoQubitReduction, - X, - Z, -) -from qiskit.quantum_info import Statevector -from qiskit.transpiler import PassManager, PassManagerConfig -from qiskit.transpiler.preset_passmanagers import level_1_pass_manager -from qiskit.utils import QuantumInstance, algorithm_globals, optionals - -logger = "LocalLogger" - - -class LogPass(DummyAP): - """A dummy analysis pass that logs when executed""" - - def __init__(self, message): - super().__init__() - self.message = message - - def run(self, dag): - logging.getLogger(logger).info(self.message) - - -# pylint: disable=invalid-name, unused-argument -def _mock_optimizer(fun, x0, jac=None, bounds=None) -> OptimizerResult: - """A mock of a callable that can be used as minimizer in the VQE.""" - result = OptimizerResult() - result.x = np.zeros_like(x0) - result.fun = fun(result.x) - result.nit = 0 - return result - - -@ddt -class TestVQE(QiskitAlgorithmsTestCase): - """Test VQE""" - - def setUp(self): - super().setUp() - self.seed = 50 - with warnings.catch_warnings(): - warnings.filterwarnings("ignore", category=DeprecationWarning) - algorithm_globals.random_seed = self.seed - 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") - - with self.assertWarns(DeprecationWarning): - self.h2_op = ( - -1.052373245772859 * (I ^ I) - + 0.39793742484318045 * (I ^ Z) - - 0.39793742484318045 * (Z ^ I) - - 0.01128010425623538 * (Z ^ Z) - + 0.18093119978423156 * (X ^ X) - ) - self.qasm_simulator = QuantumInstance( - BasicAer.get_backend("qasm_simulator"), - shots=1024, - seed_simulator=self.seed, - seed_transpiler=self.seed, - ) - self.statevector_simulator = QuantumInstance( - BasicAer.get_backend("statevector_simulator"), - shots=1, - seed_simulator=self.seed, - seed_transpiler=self.seed, - ) - - def test_basic_aer_statevector(self): - """Test the VQE on BasicAer's statevector simulator.""" - wavefunction = self.ryrz_wavefunction - with self.assertWarns(DeprecationWarning): - vqe = VQE( - ansatz=wavefunction, - optimizer=L_BFGS_B(), - quantum_instance=QuantumInstance( - BasicAer.get_backend("statevector_simulator"), - basis_gates=["u1", "u2", "u3", "cx", "id"], - coupling_map=[[0, 1]], - seed_simulator=algorithm_globals.random_seed, - seed_transpiler=algorithm_globals.random_seed, - ), - ) - result = vqe.compute_minimum_eigenvalue(operator=self.h2_op) - - with self.subTest(msg="test eigenvalue"): - self.assertAlmostEqual(result.eigenvalue.real, self.h2_energy) - - with self.subTest(msg="test dimension of optimal point"): - self.assertEqual(len(result.optimal_point), 16) - - with self.subTest(msg="assert cost_function_evals is set"): - self.assertIsNotNone(result.cost_function_evals) - - with self.subTest(msg="assert optimizer_time is set"): - self.assertIsNotNone(result.optimizer_time) - - def test_circuit_input(self): - """Test running the VQE on a plain QuantumCircuit object.""" - wavefunction = QuantumCircuit(2).compose(EfficientSU2(2)) - optimizer = SLSQP(maxiter=50) - - with self.assertWarns(DeprecationWarning): - vqe = VQE( - ansatz=wavefunction, - optimizer=optimizer, - quantum_instance=self.statevector_simulator, - ) - result = vqe.compute_minimum_eigenvalue(operator=self.h2_op) - - self.assertAlmostEqual(result.eigenvalue.real, self.h2_energy, places=5) - - @data( - (MatrixExpectation(), 1), - (AerPauliExpectation(), 1), - (PauliExpectation(), 2), - ) - @unpack - def test_construct_circuit(self, expectation, num_circuits): - """Test construct circuits returns QuantumCircuits and the right number of them.""" - try: - wavefunction = EfficientSU2(2, reps=1) - - with self.assertWarns(DeprecationWarning): - vqe = VQE(ansatz=wavefunction, expectation=expectation) - params = [0] * wavefunction.num_parameters - circuits = vqe.construct_circuit(parameter=params, operator=self.h2_op) - - self.assertEqual(len(circuits), num_circuits) - for circuit in circuits: - self.assertIsInstance(circuit, QuantumCircuit) - except MissingOptionalLibraryError as ex: - self.skipTest(str(ex)) - return - - def test_missing_varform_params(self): - """Test specifying a variational form with no parameters raises an error.""" - circuit = QuantumCircuit(self.h2_op.num_qubits) - - with self.assertWarns(DeprecationWarning): - vqe = VQE( - ansatz=circuit, quantum_instance=BasicAer.get_backend("statevector_simulator") - ) - with self.assertRaises(RuntimeError): - vqe.compute_minimum_eigenvalue(operator=self.h2_op) - - @data( - (SLSQP(maxiter=50), 5, 4), - (SPSA(maxiter=150), 2, 2), # max_evals_grouped=n or =2 if n>2 - ) - @unpack - def test_max_evals_grouped(self, optimizer, places, max_evals_grouped): - """VQE Optimizers test""" - with self.assertWarns(DeprecationWarning): - vqe = VQE( - ansatz=self.ryrz_wavefunction, - optimizer=optimizer, - max_evals_grouped=max_evals_grouped, - quantum_instance=self.statevector_simulator, - ) - result = vqe.compute_minimum_eigenvalue(operator=self.h2_op) - - self.assertAlmostEqual(result.eigenvalue.real, self.h2_energy, places=places) - - def test_basic_aer_qasm(self): - """Test the VQE on BasicAer's QASM simulator.""" - optimizer = SPSA(maxiter=300, last_avg=5) - wavefunction = self.ry_wavefunction - - with self.assertWarns(DeprecationWarning): - vqe = VQE( - ansatz=wavefunction, - optimizer=optimizer, - max_evals_grouped=1, - quantum_instance=self.qasm_simulator, - ) - result = vqe.compute_minimum_eigenvalue(operator=self.h2_op) - - self.assertAlmostEqual(result.eigenvalue.real, -1.86823, places=2) - - def test_qasm_eigenvector_normalized(self): - """Test VQE with qasm_simulator returns normalized eigenvector.""" - wavefunction = self.ry_wavefunction - with self.assertWarns(DeprecationWarning): - vqe = VQE(ansatz=wavefunction, quantum_instance=self.qasm_simulator) - result = vqe.compute_minimum_eigenvalue(operator=self.h2_op) - - amplitudes = list(result.eigenstate.values()) - self.assertAlmostEqual(np.linalg.norm(amplitudes), 1.0, places=4) - - @unittest.skipUnless(optionals.HAS_AER, "Qiskit aer is required to run these tests") - def test_with_aer_statevector(self): - """Test VQE with Aer's statevector_simulator.""" - from qiskit_aer import AerSimulator - - backend = AerSimulator(method="statevector") - wavefunction = self.ry_wavefunction - optimizer = L_BFGS_B() - - with self.assertWarns(DeprecationWarning): - quantum_instance = QuantumInstance( - backend, - seed_simulator=algorithm_globals.random_seed, - seed_transpiler=algorithm_globals.random_seed, - ) - - with self.assertWarns(DeprecationWarning): - vqe = VQE( - ansatz=wavefunction, - optimizer=optimizer, - max_evals_grouped=1, - quantum_instance=quantum_instance, - ) - result = vqe.compute_minimum_eigenvalue(operator=self.h2_op) - - self.assertAlmostEqual(result.eigenvalue.real, self.h2_energy, places=6) - - @unittest.skipUnless(optionals.HAS_AER, "Qiskit aer is required to run these tests") - def test_with_aer_qasm(self): - """Test VQE with Aer's qasm_simulator.""" - from qiskit_aer import AerSimulator - - backend = AerSimulator() - optimizer = SPSA(maxiter=200, last_avg=5) - wavefunction = self.ry_wavefunction - - with self.assertWarns(DeprecationWarning): - quantum_instance = QuantumInstance( - backend, - seed_simulator=algorithm_globals.random_seed, - seed_transpiler=algorithm_globals.random_seed, - ) - - with self.assertWarns(DeprecationWarning): - vqe = VQE( - ansatz=wavefunction, - optimizer=optimizer, - expectation=PauliExpectation(), - quantum_instance=quantum_instance, - ) - result = vqe.compute_minimum_eigenvalue(operator=self.h2_op) - - self.assertAlmostEqual(result.eigenvalue.real, -1.86305, places=2) - - @unittest.skipUnless(optionals.HAS_AER, "Qiskit aer is required to run these tests") - def test_with_aer_qasm_snapshot_mode(self): - """Test the VQE using Aer's qasm_simulator snapshot mode.""" - from qiskit_aer import AerSimulator - - backend = AerSimulator() - optimizer = L_BFGS_B() - wavefunction = self.ry_wavefunction - - with self.assertWarns(DeprecationWarning): - quantum_instance = QuantumInstance( - backend, - shots=1, - seed_simulator=algorithm_globals.random_seed, - seed_transpiler=algorithm_globals.random_seed, - ) - - with self.assertWarns(DeprecationWarning): - vqe = VQE( - ansatz=wavefunction, - optimizer=optimizer, - expectation=AerPauliExpectation(), - quantum_instance=quantum_instance, - ) - result = vqe.compute_minimum_eigenvalue(operator=self.h2_op) - - self.assertAlmostEqual(result.eigenvalue.real, self.h2_energy, places=6) - - @unittest.skipUnless(optionals.HAS_AER, "Qiskit aer is required to run these tests") - @data( - CG(maxiter=1), - L_BFGS_B(maxfun=1), - P_BFGS(maxfun=1, max_processes=0), - SLSQP(maxiter=1), - TNC(maxiter=1), - ) - def test_with_gradient(self, optimizer): - """Test VQE using Gradient().""" - from qiskit_aer import AerSimulator - - with self.assertWarns(DeprecationWarning): - quantum_instance = QuantumInstance( - backend=AerSimulator(), - shots=1, - seed_simulator=algorithm_globals.random_seed, - seed_transpiler=algorithm_globals.random_seed, - ) - with self.assertWarns(DeprecationWarning): - vqe = VQE( - ansatz=self.ry_wavefunction, - optimizer=optimizer, - gradient=Gradient(), - expectation=AerPauliExpectation(), - quantum_instance=quantum_instance, - max_evals_grouped=1000, - ) - vqe.compute_minimum_eigenvalue(operator=self.h2_op) - - def test_with_two_qubit_reduction(self): - """Test the VQE using TwoQubitReduction.""" - - with self.assertWarns(DeprecationWarning): - qubit_op = PauliSumOp.from_list( - [ - ("IIII", -0.8105479805373266), - ("IIIZ", 0.17218393261915552), - ("IIZZ", -0.22575349222402472), - ("IZZI", 0.1721839326191556), - ("ZZII", -0.22575349222402466), - ("IIZI", 0.1209126326177663), - ("IZZZ", 0.16892753870087912), - ("IXZX", -0.045232799946057854), - ("ZXIX", 0.045232799946057854), - ("IXIX", 0.045232799946057854), - ("ZXZX", -0.045232799946057854), - ("ZZIZ", 0.16614543256382414), - ("IZIZ", 0.16614543256382414), - ("ZZZZ", 0.17464343068300453), - ("ZIZI", 0.1209126326177663), - ] - ) - tapered_qubit_op = TwoQubitReduction(num_particles=2).convert(qubit_op) - - for simulator in [self.qasm_simulator, self.statevector_simulator]: - with self.subTest(f"Test for {simulator}."), self.assertWarns(DeprecationWarning): - vqe = VQE( - self.ry_wavefunction, - SPSA(maxiter=300, last_avg=5), - quantum_instance=simulator, - ) - result = vqe.compute_minimum_eigenvalue(tapered_qubit_op) - energy = -1.868 if simulator == self.qasm_simulator else self.h2_energy - self.assertAlmostEqual(result.eigenvalue.real, energy, places=2) - - def test_callback(self): - """Test the callback on VQE.""" - history = {"eval_count": [], "parameters": [], "mean": [], "std": []} - - def store_intermediate_result(eval_count, parameters, mean, std): - history["eval_count"].append(eval_count) - history["parameters"].append(parameters) - history["mean"].append(mean) - history["std"].append(std) - - optimizer = COBYLA(maxiter=3) - wavefunction = self.ry_wavefunction - - with self.assertWarns(DeprecationWarning): - vqe = VQE( - ansatz=wavefunction, - optimizer=optimizer, - callback=store_intermediate_result, - quantum_instance=self.qasm_simulator, - ) - vqe.compute_minimum_eigenvalue(operator=self.h2_op) - - self.assertTrue(all(isinstance(count, int) for count in history["eval_count"])) - self.assertTrue(all(isinstance(mean, float) for mean in history["mean"])) - self.assertTrue(all(isinstance(std, float) for std in history["std"])) - for params in history["parameters"]: - self.assertTrue(all(isinstance(param, float) for param in params)) - - def test_reuse(self): - """Test re-using a VQE algorithm instance.""" - - with self.assertWarns(DeprecationWarning): - vqe = VQE() - with self.subTest(msg="assert running empty raises AlgorithmError"): - with self.assertWarns(DeprecationWarning), self.assertRaises(AlgorithmError): - _ = vqe.compute_minimum_eigenvalue(operator=self.h2_op) - - ansatz = TwoLocal(rotation_blocks=["ry", "rz"], entanglement_blocks="cz") - vqe.ansatz = ansatz - with self.subTest(msg="assert missing operator raises AlgorithmError"): - with self.assertWarns(DeprecationWarning), self.assertRaises(AlgorithmError): - _ = vqe.compute_minimum_eigenvalue(operator=self.h2_op) - - with self.assertWarns(DeprecationWarning): - vqe.expectation = MatrixExpectation() - vqe.quantum_instance = self.statevector_simulator - - with self.subTest(msg="assert VQE works once all info is available"), self.assertWarns( - DeprecationWarning - ): - result = vqe.compute_minimum_eigenvalue(operator=self.h2_op) - self.assertAlmostEqual(result.eigenvalue.real, self.h2_energy, places=5) - - with self.assertWarns(DeprecationWarning): - operator = PrimitiveOp( - np.array([[1, 0, 0, 0], [0, -1, 0, 0], [0, 0, 2, 0], [0, 0, 0, 3]]) - ) - - with self.subTest(msg="assert minimum eigensolver interface works"), self.assertWarns( - DeprecationWarning - ): - result = vqe.compute_minimum_eigenvalue(operator=operator) - self.assertAlmostEqual(result.eigenvalue.real, -1.0, places=5) - - def test_vqe_optimizer(self): - """Test running same VQE twice to re-use optimizer, then switch optimizer""" - with self.assertWarns(DeprecationWarning): - vqe = VQE( - optimizer=SLSQP(), - quantum_instance=QuantumInstance(BasicAer.get_backend("statevector_simulator")), - ) - - def run_check(): - with self.assertWarns(DeprecationWarning): - result = vqe.compute_minimum_eigenvalue(operator=self.h2_op) - - self.assertAlmostEqual(result.eigenvalue.real, -1.85727503, places=5) - - run_check() - - with self.subTest("Optimizer re-use"): - run_check() - - with self.subTest("Optimizer replace"): - vqe.optimizer = L_BFGS_B() - run_check() - - @data(MatrixExpectation(), None) - def test_backend_change(self, user_expectation): - """Test that VQE works when backend changes.""" - - with self.assertWarns(DeprecationWarning): - vqe = VQE( - ansatz=TwoLocal(rotation_blocks=["ry", "rz"], entanglement_blocks="cz"), - optimizer=SLSQP(maxiter=2), - expectation=user_expectation, - quantum_instance=BasicAer.get_backend("statevector_simulator"), - ) - result0 = vqe.compute_minimum_eigenvalue(operator=self.h2_op) - - if user_expectation is not None: - with self.subTest("User expectation kept."): - self.assertEqual(vqe.expectation, user_expectation) - - # works also if no expectation is set, since it will be determined automatically - with self.assertWarns(DeprecationWarning): - vqe.quantum_instance = BasicAer.get_backend("qasm_simulator") - result1 = vqe.compute_minimum_eigenvalue(operator=self.h2_op) - - if user_expectation is not None: - with self.subTest("Change backend with user expectation, it is kept."): - self.assertEqual(vqe.expectation, user_expectation) - - with self.subTest("Check results."): - self.assertEqual(len(result0.optimal_point), len(result1.optimal_point)) - - def test_batch_evaluate_with_qnspsa(self): - """Test batch evaluating with QNSPSA works.""" - ansatz = TwoLocal(2, rotation_blocks=["ry", "rz"], entanglement_blocks="cz") - - wrapped_backend = BasicAer.get_backend("qasm_simulator") - inner_backend = BasicAer.get_backend("statevector_simulator") - - callcount = {"count": 0} - - def wrapped_run(circuits, **kwargs): - kwargs["callcount"]["count"] += 1 - return inner_backend.run(circuits) - - wrapped_backend.run = partial(wrapped_run, callcount=callcount) - - with self.assertWarns(DeprecationWarning): - fidelity = QNSPSA.get_fidelity(ansatz, backend=wrapped_backend) - qnspsa = QNSPSA(fidelity, maxiter=5) - - with self.assertWarns(DeprecationWarning): - vqe = VQE( - ansatz=ansatz, - optimizer=qnspsa, - max_evals_grouped=100, - quantum_instance=wrapped_backend, - ) - _ = vqe.compute_minimum_eigenvalue(Z ^ Z) - - # 1 calibration + 1 stddev estimation + 1 initial blocking - # + 5 (1 loss + 1 fidelity + 1 blocking) + 1 return loss + 1 VQE eval - expected = 1 + 1 + 1 + 5 * 3 + 1 + 1 - - self.assertEqual(callcount["count"], expected) - - def test_set_ansatz_to_none(self): - """Tests that setting the ansatz to None results in the default behavior""" - - with self.assertWarns(DeprecationWarning): - vqe = VQE( - ansatz=self.ryrz_wavefunction, - optimizer=L_BFGS_B(), - quantum_instance=self.statevector_simulator, - ) - - vqe.ansatz = None - self.assertIsInstance(vqe.ansatz, RealAmplitudes) - - def test_set_optimizer_to_none(self): - """Tests that setting the optimizer to None results in the default behavior""" - - with self.assertWarns(DeprecationWarning): - vqe = VQE( - ansatz=self.ryrz_wavefunction, - optimizer=L_BFGS_B(), - quantum_instance=self.statevector_simulator, - ) - - vqe.optimizer = None - self.assertIsInstance(vqe.optimizer, SLSQP) - - def test_optimizer_scipy_callable(self): - """Test passing a SciPy optimizer directly as callable.""" - - with self.assertWarns(DeprecationWarning): - vqe = VQE( - optimizer=partial(scipy_minimize, method="L-BFGS-B", options={"maxiter": 2}), - quantum_instance=self.statevector_simulator, - ) - result = vqe.compute_minimum_eigenvalue(Z) - - self.assertEqual(result.cost_function_evals, 20) - - def test_optimizer_callable(self): - """Test passing a optimizer directly as callable.""" - ansatz = RealAmplitudes(1, reps=1) - with self.assertWarns(DeprecationWarning): - vqe = VQE( - ansatz=ansatz, - optimizer=_mock_optimizer, - quantum_instance=self.statevector_simulator, - ) - result = vqe.compute_minimum_eigenvalue(Z) - self.assertTrue(np.all(result.optimal_point == np.zeros(ansatz.num_parameters))) - - def test_aux_operators_list(self): - """Test list-based aux_operators.""" - wavefunction = self.ry_wavefunction - - # Start with an empty list - with self.assertWarns(DeprecationWarning): - vqe = VQE(ansatz=wavefunction, quantum_instance=self.statevector_simulator) - - # Start with an empty list - result = vqe.compute_minimum_eigenvalue(self.h2_op, aux_operators=[]) - - self.assertAlmostEqual(result.eigenvalue.real, self.h2_energy, places=6) - self.assertIsNone(result.aux_operator_eigenvalues) - - # Go again with two auxiliary operators - with self.assertWarns(DeprecationWarning): - aux_op1 = PauliSumOp.from_list([("II", 2.0)]) - aux_op2 = PauliSumOp.from_list([("II", 0.5), ("ZZ", 0.5), ("YY", 0.5), ("XX", -0.5)]) - aux_ops = [aux_op1, aux_op2] - - with self.assertWarns(DeprecationWarning): - result = vqe.compute_minimum_eigenvalue(self.h2_op, aux_operators=aux_ops) - - self.assertAlmostEqual(result.eigenvalue.real, self.h2_energy, places=6) - self.assertEqual(len(result.aux_operator_eigenvalues), 2) - # expectation values - self.assertAlmostEqual(result.aux_operator_eigenvalues[0][0], 2, places=6) - self.assertAlmostEqual(result.aux_operator_eigenvalues[1][0], 0, places=6) - # standard deviations - self.assertAlmostEqual(result.aux_operator_eigenvalues[0][1], 0.0) - self.assertAlmostEqual(result.aux_operator_eigenvalues[1][1], 0.0) - - # Go again with additional None and zero operators - extra_ops = [*aux_ops, None, 0] - with self.assertWarns(DeprecationWarning): - result = vqe.compute_minimum_eigenvalue(self.h2_op, aux_operators=extra_ops) - - self.assertAlmostEqual(result.eigenvalue.real, self.h2_energy, places=6) - self.assertEqual(len(result.aux_operator_eigenvalues), 4) - # expectation values - self.assertAlmostEqual(result.aux_operator_eigenvalues[0][0], 2, places=6) - self.assertAlmostEqual(result.aux_operator_eigenvalues[1][0], 0, places=6) - self.assertEqual(result.aux_operator_eigenvalues[2][0], 0.0) - self.assertEqual(result.aux_operator_eigenvalues[3][0], 0.0) - # standard deviations - self.assertAlmostEqual(result.aux_operator_eigenvalues[0][1], 0.0) - self.assertAlmostEqual(result.aux_operator_eigenvalues[1][1], 0.0) - self.assertAlmostEqual(result.aux_operator_eigenvalues[2][1], 0.0) - self.assertAlmostEqual(result.aux_operator_eigenvalues[3][1], 0.0) - - def test_aux_operators_dict(self): - """Test dictionary compatibility of aux_operators""" - wavefunction = self.ry_wavefunction - - with self.assertWarns(DeprecationWarning): - vqe = VQE(ansatz=wavefunction, quantum_instance=self.statevector_simulator) - - # Start with an empty dictionary - with self.assertWarns(DeprecationWarning): - result = vqe.compute_minimum_eigenvalue(self.h2_op, aux_operators={}) - - self.assertAlmostEqual(result.eigenvalue.real, self.h2_energy, places=6) - self.assertIsNone(result.aux_operator_eigenvalues) - - # Go again with two auxiliary operators - with self.assertWarns(DeprecationWarning): - aux_op1 = PauliSumOp.from_list([("II", 2.0)]) - aux_op2 = PauliSumOp.from_list([("II", 0.5), ("ZZ", 0.5), ("YY", 0.5), ("XX", -0.5)]) - aux_ops = {"aux_op1": aux_op1, "aux_op2": aux_op2} - - with self.assertWarns(DeprecationWarning): - result = vqe.compute_minimum_eigenvalue(self.h2_op, aux_operators=aux_ops) - - self.assertAlmostEqual(result.eigenvalue.real, self.h2_energy, places=6) - self.assertEqual(len(result.aux_operator_eigenvalues), 2) - # expectation values - self.assertAlmostEqual(result.aux_operator_eigenvalues["aux_op1"][0], 2, places=6) - self.assertAlmostEqual(result.aux_operator_eigenvalues["aux_op2"][0], 0, places=6) - # standard deviations - self.assertAlmostEqual(result.aux_operator_eigenvalues["aux_op1"][1], 0.0) - self.assertAlmostEqual(result.aux_operator_eigenvalues["aux_op2"][1], 0.0) - - # Go again with additional None and zero operators - extra_ops = {**aux_ops, "None_operator": None, "zero_operator": 0} - with self.assertWarns(DeprecationWarning): - result = vqe.compute_minimum_eigenvalue(self.h2_op, aux_operators=extra_ops) - - self.assertAlmostEqual(result.eigenvalue.real, self.h2_energy, places=6) - self.assertEqual(len(result.aux_operator_eigenvalues), 3) - # expectation values - self.assertAlmostEqual(result.aux_operator_eigenvalues["aux_op1"][0], 2, places=6) - self.assertAlmostEqual(result.aux_operator_eigenvalues["aux_op2"][0], 0, places=6) - self.assertEqual(result.aux_operator_eigenvalues["zero_operator"][0], 0.0) - self.assertTrue("None_operator" not in result.aux_operator_eigenvalues.keys()) - # standard deviations - self.assertAlmostEqual(result.aux_operator_eigenvalues["aux_op1"][1], 0.0) - self.assertAlmostEqual(result.aux_operator_eigenvalues["aux_op2"][1], 0.0) - self.assertAlmostEqual(result.aux_operator_eigenvalues["zero_operator"][1], 0.0) - - def test_aux_operator_std_dev_pauli(self): - """Test non-zero standard deviations of aux operators with PauliExpectation.""" - wavefunction = self.ry_wavefunction - with self.assertWarns(DeprecationWarning): - vqe = VQE( - ansatz=wavefunction, - expectation=PauliExpectation(), - optimizer=COBYLA(maxiter=0), - quantum_instance=self.qasm_simulator, - ) - - with self.assertWarns(DeprecationWarning): - # Go again with two auxiliary operators - aux_op1 = PauliSumOp.from_list([("II", 2.0)]) - aux_op2 = PauliSumOp.from_list([("II", 0.5), ("ZZ", 0.5), ("YY", 0.5), ("XX", -0.5)]) - aux_ops = [aux_op1, aux_op2] - - with self.assertWarns(DeprecationWarning): - result = vqe.compute_minimum_eigenvalue(self.h2_op, aux_operators=aux_ops) - - self.assertEqual(len(result.aux_operator_eigenvalues), 2) - # expectation values - self.assertAlmostEqual(result.aux_operator_eigenvalues[0][0], 2.0, places=6) - self.assertAlmostEqual(result.aux_operator_eigenvalues[1][0], 0.6796875, places=6) - # standard deviations - self.assertAlmostEqual(result.aux_operator_eigenvalues[0][1], 0.0) - self.assertAlmostEqual(result.aux_operator_eigenvalues[1][1], 0.02534712219145965, places=6) - - # Go again with additional None and zero operators - aux_ops = [*aux_ops, None, 0] - - with self.assertWarns(DeprecationWarning): - result = vqe.compute_minimum_eigenvalue(self.h2_op, aux_operators=aux_ops) - - self.assertEqual(len(result.aux_operator_eigenvalues), 4) - # expectation values - self.assertAlmostEqual(result.aux_operator_eigenvalues[0][0], 2.0, places=6) - self.assertAlmostEqual(result.aux_operator_eigenvalues[1][0], 0.57421875, places=6) - self.assertEqual(result.aux_operator_eigenvalues[2][0], 0.0) - self.assertEqual(result.aux_operator_eigenvalues[3][0], 0.0) - # # standard deviations - self.assertAlmostEqual(result.aux_operator_eigenvalues[0][1], 0.0) - self.assertAlmostEqual( - result.aux_operator_eigenvalues[1][1], 0.026562146577166837, places=6 - ) - self.assertAlmostEqual(result.aux_operator_eigenvalues[2][1], 0.0) - self.assertAlmostEqual(result.aux_operator_eigenvalues[3][1], 0.0) - - @unittest.skipUnless(optionals.HAS_AER, "Qiskit aer is required to run these tests") - def test_aux_operator_std_dev_aer_pauli(self): - """Test non-zero standard deviations of aux operators with AerPauliExpectation.""" - from qiskit_aer import AerSimulator - - wavefunction = self.ry_wavefunction - with self.assertWarns(DeprecationWarning): - vqe = VQE( - ansatz=wavefunction, - expectation=AerPauliExpectation(), - optimizer=COBYLA(maxiter=0), - quantum_instance=QuantumInstance( - backend=AerSimulator(), - shots=1, - seed_simulator=algorithm_globals.random_seed, - seed_transpiler=algorithm_globals.random_seed, - ), - ) - with self.assertWarns(DeprecationWarning): - # Go again with two auxiliary operators - aux_op1 = PauliSumOp.from_list([("II", 2.0)]) - aux_op2 = PauliSumOp.from_list([("II", 0.5), ("ZZ", 0.5), ("YY", 0.5), ("XX", -0.5)]) - aux_ops = [aux_op1, aux_op2] - - with self.assertWarns(DeprecationWarning): - result = vqe.compute_minimum_eigenvalue(self.h2_op, aux_operators=aux_ops) - - self.assertEqual(len(result.aux_operator_eigenvalues), 2) - # expectation values - self.assertAlmostEqual(result.aux_operator_eigenvalues[0][0], 2.0, places=6) - self.assertAlmostEqual(result.aux_operator_eigenvalues[1][0], 0.6698863565455391, places=6) - # standard deviations - self.assertAlmostEqual(result.aux_operator_eigenvalues[0][1], 0.0) - self.assertAlmostEqual(result.aux_operator_eigenvalues[1][1], 0.0, places=6) - - # Go again with additional None and zero operators - aux_ops = [*aux_ops, None, 0] - with self.assertWarns(DeprecationWarning): - result = vqe.compute_minimum_eigenvalue(self.h2_op, aux_operators=aux_ops) - - self.assertEqual(len(result.aux_operator_eigenvalues), 4) - # expectation values - self.assertAlmostEqual(result.aux_operator_eigenvalues[0][0], 2.0, places=6) - self.assertAlmostEqual(result.aux_operator_eigenvalues[1][0], 0.6036400943063891, places=6) - self.assertEqual(result.aux_operator_eigenvalues[2][0], 0.0) - self.assertEqual(result.aux_operator_eigenvalues[3][0], 0.0) - # standard deviations - self.assertAlmostEqual(result.aux_operator_eigenvalues[0][1], 0.0) - self.assertAlmostEqual(result.aux_operator_eigenvalues[1][1], 0.0, places=6) - self.assertAlmostEqual(result.aux_operator_eigenvalues[2][1], 0.0) - self.assertAlmostEqual(result.aux_operator_eigenvalues[3][1], 0.0) - - def test_2step_transpile(self): - """Test the two-step transpiler pass.""" - # count how often the pass for parameterized circuits is called - pre_counter = LogPass("pre_passmanager") - pre_pass = PassManager(pre_counter) - config = PassManagerConfig(basis_gates=["u3", "cx"]) - pre_pass += level_1_pass_manager(config) - - # ... and the pass for bound circuits - bound_counter = LogPass("bound_pass_manager") - bound_pass = PassManager(bound_counter) - - optimizer = SPSA(maxiter=5, learning_rate=0.01, perturbation=0.01) - - with self.assertWarns(DeprecationWarning): - quantum_instance = QuantumInstance( - backend=BasicAer.get_backend("statevector_simulator"), - basis_gates=["u3", "cx"], - pass_manager=pre_pass, - bound_pass_manager=bound_pass, - ) - - with self.assertWarns(DeprecationWarning): - vqe = VQE(optimizer=optimizer, quantum_instance=quantum_instance) - with self.assertLogs(logger, level="INFO") as cm: - _ = vqe.compute_minimum_eigenvalue(Z) - - expected = [ - "pre_passmanager", - "bound_pass_manager", - "bound_pass_manager", - "bound_pass_manager", - "bound_pass_manager", - "bound_pass_manager", - "bound_pass_manager", - "bound_pass_manager", - "bound_pass_manager", - "bound_pass_manager", - "bound_pass_manager", - "bound_pass_manager", - "pre_passmanager", - "bound_pass_manager", - ] - self.assertEqual([record.message for record in cm.records], expected) - - def test_construct_eigenstate_from_optpoint(self): - """Test constructing the eigenstate from the optimal point, if the default ansatz is used.""" - - # use Hamiltonian yielding more than 11 parameters in the default ansatz - with self.assertWarns(DeprecationWarning): - hamiltonian = Z ^ Z ^ Z - - optimizer = SPSA(maxiter=1, learning_rate=0.01, perturbation=0.01) - - with self.assertWarns(DeprecationWarning): - quantum_instance = QuantumInstance( - backend=BasicAer.get_backend("statevector_simulator"), basis_gates=["u3", "cx"] - ) - - with self.assertWarns(DeprecationWarning): - vqe = VQE(optimizer=optimizer, quantum_instance=quantum_instance) - result = vqe.compute_minimum_eigenvalue(hamiltonian) - - optimal_circuit = vqe.ansatz.assign_parameters(result.optimal_point) - self.assertTrue(Statevector(result.eigenstate).equiv(optimal_circuit)) - - -if __name__ == "__main__": - unittest.main() diff --git a/test/python/algorithms/time_evolvers/__init__.py b/test/python/algorithms/time_evolvers/__init__.py deleted file mode 100644 index fdb172d367f0..000000000000 --- a/test/python/algorithms/time_evolvers/__init__.py +++ /dev/null @@ -1,11 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2022. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. diff --git a/test/python/algorithms/time_evolvers/classical_methods/__init__.py b/test/python/algorithms/time_evolvers/classical_methods/__init__.py deleted file mode 100644 index fdb172d367f0..000000000000 --- a/test/python/algorithms/time_evolvers/classical_methods/__init__.py +++ /dev/null @@ -1,11 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2022. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. diff --git a/test/python/algorithms/time_evolvers/classical_methods/test_scipy_imaginary_evolver.py b/test/python/algorithms/time_evolvers/classical_methods/test_scipy_imaginary_evolver.py deleted file mode 100644 index 5671bf221284..000000000000 --- a/test/python/algorithms/time_evolvers/classical_methods/test_scipy_imaginary_evolver.py +++ /dev/null @@ -1,183 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2022, 2023. -# -# 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 Classical Imaginary Evolver.""" -import unittest -from test.python.algorithms import QiskitAlgorithmsTestCase -from ddt import data, ddt, unpack -import numpy as np -from qiskit.algorithms.time_evolvers.time_evolution_problem import TimeEvolutionProblem - -from qiskit.quantum_info.states.statevector import Statevector -from qiskit.quantum_info import SparsePauliOp - -from qiskit import QuantumCircuit -from qiskit.algorithms import SciPyImaginaryEvolver - -from qiskit.opflow import PauliSumOp - - -@ddt -class TestSciPyImaginaryEvolver(QiskitAlgorithmsTestCase): - """Test SciPy Imaginary Evolver.""" - - def create_hamiltonian_lattice(self, num_sites: int) -> SparsePauliOp: - """Creates an Ising Hamiltonian on a lattice.""" - j_const = 0.1 - g_const = -1.0 - - zz_op = ["I" * i + "ZZ" + "I" * (num_sites - i - 2) for i in range(num_sites - 1)] - x_op = ["I" * i + "X" + "I" * (num_sites - i - 1) for i in range(num_sites)] - return SparsePauliOp(zz_op) * j_const + SparsePauliOp(x_op) * g_const - - @data( - (Statevector.from_label("0"), 100, SparsePauliOp("X"), Statevector.from_label("-")), - (Statevector.from_label("0"), 100, SparsePauliOp("-X"), Statevector.from_label("+")), - ) - @unpack - def test_evolve( - self, - initial_state: Statevector, - tau: float, - hamiltonian: SparsePauliOp, - expected_state: Statevector, - ): - """Initializes a classical imaginary evolver and evolves a state to find the ground state. - It compares the solution with the first eigenstate of the hamiltonian. - """ - expected_state_matrix = expected_state.data - - evolution_problem = TimeEvolutionProblem(hamiltonian, tau, initial_state) - classic_evolver = SciPyImaginaryEvolver(num_timesteps=300) - result = classic_evolver.evolve(evolution_problem) - - with self.subTest("Amplitudes"): - np.testing.assert_allclose( - np.absolute(result.evolved_state.data), - np.absolute(expected_state_matrix), - atol=1e-10, - rtol=0, - ) - - with self.subTest("Phases"): - np.testing.assert_allclose( - np.angle(result.evolved_state.data), - np.angle(expected_state_matrix), - atol=1e-10, - rtol=0, - ) - - @data( - ( - Statevector.from_label("0" * 5), - SparsePauliOp.from_sparse_list([("X", [i], 1) for i in range(5)], num_qubits=5), - 5, - ), - (Statevector.from_label("0"), SparsePauliOp("X"), 1), - ) - @unpack - def test_observables( - self, initial_state: Statevector, hamiltonian: SparsePauliOp, nqubits: int - ): - """Tests if the observables are properly evaluated at each timestep.""" - - time_ev = 5.0 - observables = {"Energy": hamiltonian, "Z": SparsePauliOp("Z" * nqubits)} - evolution_problem = TimeEvolutionProblem( - hamiltonian, time_ev, initial_state, aux_operators=observables - ) - - classic_evolver = SciPyImaginaryEvolver(num_timesteps=300) - result = classic_evolver.evolve(evolution_problem) - - z_mean, z_std = result.observables["Z"] - - time_vector = result.times - expected_z = 1 / (np.cosh(time_vector) ** 2 + np.sinh(time_vector) ** 2) - expected_z_std = np.zeros_like(expected_z) - - np.testing.assert_allclose(z_mean, expected_z**nqubits, atol=1e-10, rtol=0) - np.testing.assert_allclose(z_std, expected_z_std, atol=1e-10, rtol=0) - - def test_quantum_circuit_initial_state(self): - """Tests if the system can be evolved with a quantum circuit as an initial state.""" - qc = QuantumCircuit(3) - qc.h(0) - qc.cx(0, range(1, 3)) - - evolution_problem = TimeEvolutionProblem( - hamiltonian=SparsePauliOp("X" * 3), time=1.0, initial_state=qc - ) - classic_evolver = SciPyImaginaryEvolver(num_timesteps=5) - result = classic_evolver.evolve(evolution_problem) - self.assertEqual(result.evolved_state, Statevector(qc)) - - def test_paulisumop_hamiltonian(self): - """Tests if the hamiltonian can be a PauliSumOp""" - with self.assertWarns(DeprecationWarning): - hamiltonian = PauliSumOp.from_list( - [ - ("XI", 1), - ("IX", 1), - ] - ) - observable = PauliSumOp.from_list([("ZZ", 1)]) - evolution_problem = TimeEvolutionProblem( - hamiltonian=hamiltonian, - time=1.0, - initial_state=Statevector.from_label("00"), - aux_operators={"ZZ": observable}, - ) - classic_evolver = SciPyImaginaryEvolver(num_timesteps=5) - result = classic_evolver.evolve(evolution_problem) - expected = 1 / (np.cosh(1.0) ** 2 + np.sinh(1.0) ** 2) - np.testing.assert_almost_equal(result.aux_ops_evaluated["ZZ"][0], expected**2) - - def test_error_time_dependency(self): - """Tests if an error is raised for a time dependent Hamiltonian.""" - evolution_problem = TimeEvolutionProblem( - hamiltonian=SparsePauliOp("X" * 3), - time=1.0, - initial_state=Statevector.from_label("0" * 3), - t_param=0, - ) - classic_evolver = SciPyImaginaryEvolver(num_timesteps=5) - with self.assertRaises(ValueError): - classic_evolver.evolve(evolution_problem) - - def test_no_time_steps(self): - """Tests if the evolver handles some edge cases related to the number of timesteps.""" - evolution_problem = TimeEvolutionProblem( - hamiltonian=SparsePauliOp("X"), - time=1.0, - initial_state=Statevector.from_label("0"), - aux_operators={"Energy": SparsePauliOp("X")}, - ) - - with self.subTest("0 timesteps"): - with self.assertRaises(ValueError): - classic_evolver = SciPyImaginaryEvolver(num_timesteps=0) - classic_evolver.evolve(evolution_problem) - - with self.subTest("1 timestep"): - classic_evolver = SciPyImaginaryEvolver(num_timesteps=1) - result = classic_evolver.evolve(evolution_problem) - np.testing.assert_equal(result.times, np.array([0.0, 1.0])) - - with self.subTest("Negative timesteps"): - with self.assertRaises(ValueError): - classic_evolver = SciPyImaginaryEvolver(num_timesteps=-5) - classic_evolver.evolve(evolution_problem) - - -if __name__ == "__main__": - unittest.main() diff --git a/test/python/algorithms/time_evolvers/classical_methods/test_scipy_real_evolver.py b/test/python/algorithms/time_evolvers/classical_methods/test_scipy_real_evolver.py deleted file mode 100644 index 40fb1e1e1102..000000000000 --- a/test/python/algorithms/time_evolvers/classical_methods/test_scipy_real_evolver.py +++ /dev/null @@ -1,154 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2022. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. - -"""Test Classical Real Evolver.""" -import unittest -from test.python.algorithms import QiskitAlgorithmsTestCase -from ddt import data, ddt, unpack -import numpy as np -from qiskit import QuantumCircuit, QuantumRegister -from qiskit.algorithms import SciPyRealEvolver, TimeEvolutionProblem -from qiskit.quantum_info import Statevector, SparsePauliOp - - -def zero(n): - """Auxiliary function to create an initial state on n qubits.""" - qr = QuantumRegister(n) - qc = QuantumCircuit(qr) - return Statevector(qc) - - -def one(n): - """Auxiliary function to create an initial state on n qubits.""" - qr = QuantumRegister(n) - qc = QuantumCircuit(qr) - qc.x(qr) - return Statevector(qc) - - -@ddt -class TestClassicalRealEvolver(QiskitAlgorithmsTestCase): - """Test Classical Real Evolver.""" - - @data( - (one(1), np.pi / 2, SparsePauliOp("X"), -1.0j * zero(1)), - ( - one(1).expand(zero(1)), - np.pi / 2, - SparsePauliOp(["XX", "YY"], [0.5, 0.5]), - -1.0j * zero(1).expand(one(1)), - ), - ( - one(1).expand(zero(1)), - np.pi / 4, - SparsePauliOp(["XX", "YY"], [0.5, 0.5]), - ((one(1).expand(zero(1)) - 1.0j * zero(1).expand(one(1)))) / np.sqrt(2), - ), - (zero(12), np.pi / 2, SparsePauliOp("X" * 12), -1.0j * (one(12))), - ) - @unpack - def test_evolve( - self, - initial_state: Statevector, - time_ev: float, - hamiltonian: SparsePauliOp, - expected_state: Statevector, - ): - """Initializes a classical real evolver and evolves a state.""" - evolution_problem = TimeEvolutionProblem(hamiltonian, time_ev, initial_state) - classic_evolver = SciPyRealEvolver(num_timesteps=1) - result = classic_evolver.evolve(evolution_problem) - - np.testing.assert_allclose( - result.evolved_state.data, - expected_state.data, - atol=1e-10, - rtol=0, - ) - - def test_observables(self): - """Tests if the observables are properly evaluated at each timestep.""" - - initial_state = zero(1) - time_ev = 10.0 - hamiltonian = SparsePauliOp("X") - observables = {"Energy": SparsePauliOp("X"), "Z": SparsePauliOp("Z")} - evolution_problem = TimeEvolutionProblem( - hamiltonian, time_ev, initial_state, aux_operators=observables - ) - classic_evolver = SciPyRealEvolver(num_timesteps=10) - result = classic_evolver.evolve(evolution_problem) - - z_mean, z_std = result.observables["Z"] - - timesteps = z_mean.shape[0] - time_vector = np.linspace(0, time_ev, timesteps) - expected_z = 1 - 2 * (np.sin(time_vector)) ** 2 - expected_z_std = np.zeros_like(expected_z) - - np.testing.assert_allclose(z_mean, expected_z, atol=1e-10, rtol=0) - np.testing.assert_allclose(z_std, expected_z_std, atol=1e-10, rtol=0) - np.testing.assert_equal(time_vector, result.times) - - def test_quantum_circuit_initial_state(self): - """Tests if the system can be evolved with a quantum circuit as an initial state.""" - qc = QuantumCircuit(3) - qc.h(0) - qc.cx(0, range(1, 3)) - - evolution_problem = TimeEvolutionProblem( - hamiltonian=SparsePauliOp("X" * 3), time=2 * np.pi, initial_state=qc - ) - classic_evolver = SciPyRealEvolver(num_timesteps=500) - result = classic_evolver.evolve(evolution_problem) - np.testing.assert_almost_equal( - result.evolved_state.data, - np.array([1, 0, 0, 0, 0, 0, 0, 1]) / np.sqrt(2), - decimal=10, - ) - - def test_error_time_dependency(self): - """Tests if an error is raised for time dependent hamiltonian.""" - evolution_problem = TimeEvolutionProblem( - hamiltonian=SparsePauliOp("X" * 3), time=1.0, initial_state=zero(3), t_param=0 - ) - classic_evolver = SciPyRealEvolver(num_timesteps=5) - with self.assertRaises(ValueError): - classic_evolver.evolve(evolution_problem) - - def test_no_time_steps(self): - """Tests if the evolver handles some edge cases related to the number of timesteps.""" - evolution_problem = TimeEvolutionProblem( - hamiltonian=SparsePauliOp("X"), - time=1.0, - initial_state=zero(1), - aux_operators={"Energy": SparsePauliOp("X")}, - ) - - with self.subTest("0 timesteps"): - with self.assertRaises(ValueError): - classic_evolver = SciPyRealEvolver(num_timesteps=0) - classic_evolver.evolve(evolution_problem) - - with self.subTest("1 timestep"): - classic_evolver = SciPyRealEvolver(num_timesteps=1) - result = classic_evolver.evolve(evolution_problem) - np.testing.assert_equal(result.times, np.array([0.0, 1.0])) - - with self.subTest("Negative timesteps"): - with self.assertRaises(ValueError): - classic_evolver = SciPyRealEvolver(num_timesteps=-5) - classic_evolver.evolve(evolution_problem) - - -if __name__ == "__main__": - unittest.main() diff --git a/test/python/algorithms/time_evolvers/test_pvqd.py b/test/python/algorithms/time_evolvers/test_pvqd.py deleted file mode 100644 index fc48e7b7159e..000000000000 --- a/test/python/algorithms/time_evolvers/test_pvqd.py +++ /dev/null @@ -1,342 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2018, 2022. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. - -"""Tests for PVQD.""" -import unittest -import warnings - -from test.python.algorithms import QiskitAlgorithmsTestCase -from functools import partial - -import numpy as np -from ddt import data, ddt, unpack - -from qiskit import QiskitError -from qiskit.algorithms.time_evolvers import TimeEvolutionProblem -from qiskit.algorithms.optimizers import L_BFGS_B, SPSA, GradientDescent, OptimizerResult -from qiskit.algorithms.state_fidelities import ComputeUncompute -from qiskit.algorithms.time_evolvers.pvqd import PVQD -from qiskit.circuit import Gate, Parameter, QuantumCircuit -from qiskit.circuit.library import EfficientSU2 -from qiskit.opflow import PauliSumOp -from qiskit.primitives import Estimator, Sampler -from qiskit.quantum_info import Pauli, SparsePauliOp -from qiskit.test import QiskitTestCase -from qiskit.utils import algorithm_globals - - -# pylint: disable=unused-argument, invalid-name -def gradient_supplied(fun, x0, jac, info): - """A mock optimizer that checks whether the gradient is supported or not.""" - result = OptimizerResult() - result.x = x0 - result.fun = 0 - info["has_gradient"] = jac is not None - - return result - - -class WhatAmI(Gate): - """A custom opaque gate that can be inverted but not decomposed.""" - - def __init__(self, angle): - super().__init__(name="whatami", num_qubits=2, params=[angle]) - - def inverse(self): - return WhatAmI(-self.params[0]) - - -@ddt -class TestPVQD(QiskitAlgorithmsTestCase): - """Tests for the pVQD algorithm.""" - - def setUp(self): - super().setUp() - self.hamiltonian = 0.1 * SparsePauliOp([Pauli("ZZ"), Pauli("IX"), Pauli("XI")]) - self.observable = Pauli("ZZ") - self.ansatz = EfficientSU2(2, reps=1) - self.initial_parameters = np.zeros(self.ansatz.num_parameters) - with warnings.catch_warnings(): - warnings.filterwarnings("ignore", category=DeprecationWarning) - algorithm_globals.random_seed = 123 - - @data(("ising", True, 2), ("pauli", False, None), ("pauli_sum_op", True, 2)) - @unpack - def test_pvqd(self, hamiltonian_type, gradient, num_timesteps): - """Test a simple evolution.""" - time = 0.02 - - if hamiltonian_type == "ising": - hamiltonian = self.hamiltonian - elif hamiltonian_type == "pauli_sum_op": - with self.assertWarns(DeprecationWarning): - hamiltonian = PauliSumOp(self.hamiltonian) - else: # hamiltonian_type == "pauli": - hamiltonian = Pauli("XX") - - # parse input arguments - if gradient: - optimizer = GradientDescent(maxiter=1) - else: - optimizer = L_BFGS_B(maxiter=1) - - sampler = Sampler() - estimator = Estimator() - fidelity_primitive = ComputeUncompute(sampler) - - # run pVQD keeping track of the energy and the magnetization - pvqd = PVQD( - fidelity_primitive, - self.ansatz, - self.initial_parameters, - estimator, - optimizer=optimizer, - num_timesteps=num_timesteps, - ) - problem = TimeEvolutionProblem( - hamiltonian, time, aux_operators=[hamiltonian, self.observable] - ) - result = pvqd.evolve(problem) - - self.assertTrue(len(result.fidelities) == 3) - self.assertTrue(np.all(result.times == [0.0, 0.01, 0.02])) - self.assertTrue(np.asarray(result.observables).shape == (3, 2)) - num_parameters = self.ansatz.num_parameters - self.assertTrue( - len(result.parameters) == 3 - and np.all([len(params) == num_parameters for params in result.parameters]) - ) - - def test_step(self): - """Test calling the step method directly.""" - sampler = Sampler() - estimator = Estimator() - fidelity_primitive = ComputeUncompute(sampler) - pvqd = PVQD( - fidelity_primitive, - self.ansatz, - self.initial_parameters, - estimator, - optimizer=L_BFGS_B(maxiter=100), - ) - - # perform optimization for a timestep of 0, then the optimal parameters are the current - # ones and the fidelity is 1 - theta_next, fidelity = pvqd.step( - self.hamiltonian, - self.ansatz, - self.initial_parameters, - dt=0.0, - initial_guess=np.zeros_like(self.initial_parameters), - ) - - self.assertTrue(np.allclose(theta_next, self.initial_parameters)) - self.assertAlmostEqual(fidelity, 1) - - def test_get_loss(self): - """Test getting the loss function directly.""" - - sampler = Sampler() - estimator = Estimator() - fidelity_primitive = ComputeUncompute(sampler) - - pvqd = PVQD( - fidelity_primitive, - self.ansatz, - self.initial_parameters, - estimator, - use_parameter_shift=False, - ) - - theta = np.ones(self.ansatz.num_parameters) - loss, gradient = pvqd.get_loss( - self.hamiltonian, self.ansatz, dt=0.0, current_parameters=theta - ) - - displacement = np.arange(self.ansatz.num_parameters) - - with self.subTest(msg="check gradient is None"): - self.assertIsNone(gradient) - - with self.subTest(msg="check loss works"): - self.assertGreater(loss(displacement), 0) - self.assertAlmostEqual(loss(np.zeros_like(theta)), 0) - - def test_invalid_num_timestep(self): - """Test raises if the num_timestep is not positive.""" - sampler = Sampler() - estimator = Estimator() - fidelity_primitive = ComputeUncompute(sampler) - pvqd = PVQD( - fidelity_primitive, - self.ansatz, - self.initial_parameters, - estimator, - optimizer=L_BFGS_B(), - num_timesteps=0, - ) - problem = TimeEvolutionProblem( - self.hamiltonian, time=0.01, aux_operators=[self.hamiltonian, self.observable] - ) - - with self.assertRaises(ValueError): - _ = pvqd.evolve(problem) - - def test_initial_guess_and_observables(self): - """Test doing no optimizations stays at initial guess.""" - initial_guess = np.zeros(self.ansatz.num_parameters) - sampler = Sampler() - estimator = Estimator() - fidelity_primitive = ComputeUncompute(sampler) - - pvqd = PVQD( - fidelity_primitive, - self.ansatz, - self.initial_parameters, - estimator, - optimizer=SPSA(maxiter=0, learning_rate=0.1, perturbation=0.01), - num_timesteps=10, - initial_guess=initial_guess, - ) - problem = TimeEvolutionProblem( - self.hamiltonian, time=0.1, aux_operators=[self.hamiltonian, self.observable] - ) - - result = pvqd.evolve(problem) - - observables = result.aux_ops_evaluated - self.assertEqual(observables[0], 0.1) # expected energy - self.assertEqual(observables[1], 1) # expected magnetization - - def test_zero_parameters(self): - """Test passing an ansatz with zero parameters raises an error.""" - problem = TimeEvolutionProblem(self.hamiltonian, time=0.02) - sampler = Sampler() - fidelity_primitive = ComputeUncompute(sampler) - - pvqd = PVQD( - fidelity_primitive, - QuantumCircuit(2), - np.array([]), - optimizer=SPSA(maxiter=10, learning_rate=0.1, perturbation=0.01), - ) - - with self.assertRaises(QiskitError): - _ = pvqd.evolve(problem) - - def test_initial_state_raises(self): - """Test passing an initial state raises an error for now.""" - initial_state = QuantumCircuit(2) - initial_state.x(0) - - problem = TimeEvolutionProblem( - self.hamiltonian, - time=0.02, - initial_state=initial_state, - ) - - sampler = Sampler() - fidelity_primitive = ComputeUncompute(sampler) - - pvqd = PVQD( - fidelity_primitive, - self.ansatz, - self.initial_parameters, - optimizer=SPSA(maxiter=0, learning_rate=0.1, perturbation=0.01), - ) - - with self.assertRaises(NotImplementedError): - _ = pvqd.evolve(problem) - - def test_aux_ops_raises(self): - """Test passing auxiliary operators with no estimator raises an error.""" - - problem = TimeEvolutionProblem( - self.hamiltonian, time=0.02, aux_operators=[self.hamiltonian, self.observable] - ) - - sampler = Sampler() - fidelity_primitive = ComputeUncompute(sampler) - - pvqd = PVQD( - fidelity_primitive, - self.ansatz, - self.initial_parameters, - optimizer=SPSA(maxiter=0, learning_rate=0.1, perturbation=0.01), - ) - - with self.assertRaises(ValueError): - _ = pvqd.evolve(problem) - - -class TestPVQDUtils(QiskitTestCase): - """Test some utility functions for PVQD.""" - - def setUp(self): - super().setUp() - self.hamiltonian = 0.1 * SparsePauliOp([Pauli("ZZ"), Pauli("IX"), Pauli("XI")]) - self.ansatz = EfficientSU2(2, reps=1) - - def test_gradient_supported(self): - """Test the gradient support is correctly determined.""" - # gradient supported here - wrapped = EfficientSU2(2) # a circuit wrapped into a big instruction - plain = wrapped.decompose() # a plain circuit with already supported instructions - - # gradients not supported on the following circuits - x = Parameter("x") - duplicated = QuantumCircuit(2) - duplicated.rx(x, 0) - duplicated.rx(x, 1) - - needs_chainrule = QuantumCircuit(2) - needs_chainrule.rx(2 * x, 0) - - custom_gate = WhatAmI(x) - unsupported = QuantumCircuit(2) - unsupported.append(custom_gate, [0, 1]) - - tests = [ - (wrapped, True), # tuple: (circuit, gradient support) - (plain, True), - (duplicated, False), - (needs_chainrule, False), - (unsupported, False), - ] - - # used to store the info if a gradient callable is passed into the - # optimizer of not - info = {"has_gradient": None} - optimizer = partial(gradient_supplied, info=info) - - sampler = Sampler() - estimator = Estimator() - fidelity_primitive = ComputeUncompute(sampler) - - pvqd = PVQD( - fidelity=fidelity_primitive, - ansatz=None, - initial_parameters=np.array([]), - estimator=estimator, - optimizer=optimizer, - ) - problem = TimeEvolutionProblem(self.hamiltonian, time=0.01) - for circuit, expected_support in tests: - with self.subTest(circuit=circuit, expected_support=expected_support): - pvqd.ansatz = circuit - pvqd.initial_parameters = np.zeros(circuit.num_parameters) - _ = pvqd.evolve(problem) - self.assertEqual(info["has_gradient"], expected_support) - - -if __name__ == "__main__": - unittest.main() diff --git a/test/python/algorithms/time_evolvers/test_time_evolution_problem.py b/test/python/algorithms/time_evolvers/test_time_evolution_problem.py deleted file mode 100644 index 1982fb203749..000000000000 --- a/test/python/algorithms/time_evolvers/test_time_evolution_problem.py +++ /dev/null @@ -1,98 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2022, 2023. -# -# 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 evolver problem class.""" -import unittest -from test.python.algorithms import QiskitAlgorithmsTestCase -from ddt import data, ddt -from numpy.testing import assert_raises -from qiskit import QuantumCircuit -from qiskit.algorithms import TimeEvolutionProblem -from qiskit.quantum_info import Pauli, SparsePauliOp, Statevector -from qiskit.circuit import Parameter -from qiskit.opflow import Y, Z, One, X, Zero, PauliSumOp - - -@ddt -class TestTimeEvolutionProblem(QiskitAlgorithmsTestCase): - """Test evolver problem class.""" - - def test_init_default(self): - """Tests that all default fields are initialized correctly.""" - hamiltonian = Y - time = 2.5 - initial_state = One - - evo_problem = TimeEvolutionProblem(hamiltonian, time, initial_state) - - expected_hamiltonian = Y - expected_time = 2.5 - expected_initial_state = One - expected_aux_operators = None - expected_t_param = None - expected_param_value_dict = None - - self.assertEqual(evo_problem.hamiltonian, expected_hamiltonian) - self.assertEqual(evo_problem.time, expected_time) - self.assertEqual(evo_problem.initial_state, expected_initial_state) - self.assertEqual(evo_problem.aux_operators, expected_aux_operators) - self.assertEqual(evo_problem.t_param, expected_t_param) - self.assertEqual(evo_problem.param_value_map, expected_param_value_dict) - - @data(QuantumCircuit(1), Statevector([1, 0])) - def test_init_all(self, initial_state): - """Tests that all fields are initialized correctly.""" - t_parameter = Parameter("t") - with self.assertWarns(DeprecationWarning): - hamiltonian = t_parameter * Z + Y - time = 2 - aux_operators = [X, Y] - param_value_dict = {t_parameter: 3.2} - - evo_problem = TimeEvolutionProblem( - hamiltonian, - time, - initial_state, - aux_operators, - t_param=t_parameter, - param_value_map=param_value_dict, - ) - - with self.assertWarns(DeprecationWarning): - expected_hamiltonian = Y + t_parameter * Z - expected_time = 2 - expected_type = QuantumCircuit - expected_aux_operators = [X, Y] - expected_t_param = t_parameter - expected_param_value_dict = {t_parameter: 3.2} - - with self.assertWarns(DeprecationWarning): - self.assertEqual(evo_problem.hamiltonian, expected_hamiltonian) - self.assertEqual(evo_problem.time, expected_time) - self.assertEqual(type(evo_problem.initial_state), expected_type) - self.assertEqual(evo_problem.aux_operators, expected_aux_operators) - self.assertEqual(evo_problem.t_param, expected_t_param) - self.assertEqual(evo_problem.param_value_map, expected_param_value_dict) - - def test_validate_params(self): - """Tests expected errors are thrown on parameters mismatch.""" - param_x = Parameter("x") - with self.subTest(msg="Parameter missing in dict."): - with self.assertWarns(DeprecationWarning): - hamiltonian = PauliSumOp(SparsePauliOp([Pauli("X"), Pauli("Y")]), param_x) - evolution_problem = TimeEvolutionProblem(hamiltonian, 2, Zero) - with assert_raises(ValueError): - evolution_problem.validate_params() - - -if __name__ == "__main__": - unittest.main() diff --git a/test/python/algorithms/time_evolvers/test_time_evolution_result.py b/test/python/algorithms/time_evolvers/test_time_evolution_result.py deleted file mode 100644 index 26f21ba93627..000000000000 --- a/test/python/algorithms/time_evolvers/test_time_evolution_result.py +++ /dev/null @@ -1,47 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2022. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. -"""Class for testing evolution result.""" -import unittest -from test.python.algorithms import QiskitAlgorithmsTestCase -from qiskit.algorithms import TimeEvolutionResult -from qiskit.opflow import Zero - - -class TestTimeEvolutionResult(QiskitAlgorithmsTestCase): - """Class for testing evolution result and relevant metadata.""" - - def test_init_state(self): - """Tests that a class is initialized correctly with an evolved_state.""" - evolved_state = Zero - evo_result = TimeEvolutionResult(evolved_state=evolved_state) - - expected_state = Zero - expected_aux_ops_evaluated = None - - self.assertEqual(evo_result.evolved_state, expected_state) - self.assertEqual(evo_result.aux_ops_evaluated, expected_aux_ops_evaluated) - - def test_init_observable(self): - """Tests that a class is initialized correctly with an evolved_observable.""" - evolved_state = Zero - evolved_aux_ops_evaluated = [(5j, 5j), (1.0, 8j), (5 + 1j, 6 + 1j)] - evo_result = TimeEvolutionResult(evolved_state, evolved_aux_ops_evaluated) - - expected_state = Zero - expected_aux_ops_evaluated = [(5j, 5j), (1.0, 8j), (5 + 1j, 6 + 1j)] - - self.assertEqual(evo_result.evolved_state, expected_state) - self.assertEqual(evo_result.aux_ops_evaluated, expected_aux_ops_evaluated) - - -if __name__ == "__main__": - unittest.main() diff --git a/test/python/algorithms/time_evolvers/test_trotter_qrte.py b/test/python/algorithms/time_evolvers/test_trotter_qrte.py deleted file mode 100644 index b8a14f0affeb..000000000000 --- a/test/python/algorithms/time_evolvers/test_trotter_qrte.py +++ /dev/null @@ -1,273 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2021, 2023. -# -# 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 TrotterQRTE.""" - -import unittest -import warnings - -from test.python.algorithms import QiskitAlgorithmsTestCase -from ddt import ddt, data, unpack -import numpy as np -from scipy.linalg import expm -from numpy.testing import assert_raises - -from qiskit.algorithms.time_evolvers import TimeEvolutionProblem, TrotterQRTE -from qiskit.primitives import Estimator -from qiskit import QuantumCircuit -from qiskit.circuit.library import ZGate -from qiskit.quantum_info import Statevector, Pauli, SparsePauliOp -from qiskit.utils import algorithm_globals -from qiskit.circuit import Parameter -from qiskit.opflow import PauliSumOp, X, MatrixOp -from qiskit.synthesis import SuzukiTrotter, QDrift - - -@ddt -class TestTrotterQRTE(QiskitAlgorithmsTestCase): - """TrotterQRTE tests.""" - - def setUp(self): - super().setUp() - self.seed = 50 - with warnings.catch_warnings(): - warnings.filterwarnings("ignore", category=DeprecationWarning) - algorithm_globals.random_seed = self.seed - - @data( - ( - None, - Statevector([0.29192658 - 0.45464871j, 0.70807342 - 0.45464871j]), - ), - ( - SuzukiTrotter(), - Statevector([0.29192658 - 0.84147098j, 0.0 - 0.45464871j]), - ), - ) - @unpack - def test_trotter_qrte_trotter_single_qubit(self, product_formula, expected_state): - """Test for default TrotterQRTE on a single qubit.""" - with self.assertWarns(DeprecationWarning): - operator = PauliSumOp(SparsePauliOp([Pauli("X"), Pauli("Z")])) - initial_state = QuantumCircuit(1) - time = 1 - evolution_problem = TimeEvolutionProblem(operator, time, initial_state) - - trotter_qrte = TrotterQRTE(product_formula=product_formula) - evolution_result_state_circuit = trotter_qrte.evolve(evolution_problem).evolved_state - - np.testing.assert_array_almost_equal( - Statevector.from_instruction(evolution_result_state_circuit).data, expected_state.data - ) - - @data((SparsePauliOp(["X", "Z"]), None), (SparsePauliOp(["X", "Z"]), Parameter("t"))) - @unpack - def test_trotter_qrte_trotter(self, operator, t_param): - """Test for default TrotterQRTE on a single qubit with auxiliary operators.""" - if not t_param is None: - operator = SparsePauliOp(operator.paulis, np.array([t_param, 1])) - - # LieTrotter with 1 rep - aux_ops = [Pauli("X"), Pauli("Y")] - - initial_state = QuantumCircuit(1) - time = 3 - num_timesteps = 2 - evolution_problem = TimeEvolutionProblem( - operator, time, initial_state, aux_ops, t_param=t_param - ) - estimator = Estimator() - - expected_psi, expected_observables_result = self._get_expected_trotter_qrte( - operator, - time, - num_timesteps, - initial_state, - aux_ops, - t_param, - ) - - expected_evolved_state = Statevector(expected_psi) - - with warnings.catch_warnings(): - warnings.filterwarnings("ignore", category=DeprecationWarning) - algorithm_globals.random_seed = 0 - trotter_qrte = TrotterQRTE(estimator=estimator, num_timesteps=num_timesteps) - evolution_result = trotter_qrte.evolve(evolution_problem) - - np.testing.assert_array_almost_equal( - Statevector.from_instruction(evolution_result.evolved_state).data, - expected_evolved_state.data, - ) - - aux_ops_result = evolution_result.aux_ops_evaluated - expected_aux_ops_result = [ - (expected_observables_result[-1][0], {"variance": 0, "shots": 0}), - (expected_observables_result[-1][1], {"variance": 0, "shots": 0}), - ] - - means = [element[0] for element in aux_ops_result] - expected_means = [element[0] for element in expected_aux_ops_result] - np.testing.assert_array_almost_equal(means, expected_means) - - vars_and_shots = [element[1] for element in aux_ops_result] - expected_vars_and_shots = [element[1] for element in expected_aux_ops_result] - - observables_result = evolution_result.observables - expected_observables_result = [ - [(o, {"variance": 0, "shots": 0}) for o in eor] for eor in expected_observables_result - ] - - means = [sub_element[0] for element in observables_result for sub_element in element] - expected_means = [ - sub_element[0] for element in expected_observables_result for sub_element in element - ] - np.testing.assert_array_almost_equal(means, expected_means) - - for computed, expected in zip(vars_and_shots, expected_vars_and_shots): - self.assertAlmostEqual(computed.pop("variance", 0), expected["variance"], 2) - self.assertEqual(computed.pop("shots", 0), expected["shots"]) - - @data( - ( - PauliSumOp(SparsePauliOp([Pauli("XY"), Pauli("YX")])), - Statevector([-0.41614684 + 0.0j, 0.0 + 0.0j, 0.0 + 0.0j, 0.90929743 + 0.0j]), - ), - ( - PauliSumOp(SparsePauliOp([Pauli("ZZ"), Pauli("ZI"), Pauli("IZ")])), - Statevector([-0.9899925 - 0.14112001j, 0.0 + 0.0j, 0.0 + 0.0j, 0.0 + 0.0j]), - ), - ( - Pauli("YY"), - Statevector([0.54030231 + 0.0j, 0.0 + 0.0j, 0.0 + 0.0j, 0.0 + 0.84147098j]), - ), - ) - @unpack - def test_trotter_qrte_trotter_two_qubits(self, operator, expected_state): - """Test for TrotterQRTE on two qubits with various types of a Hamiltonian.""" - # LieTrotter with 1 rep - initial_state = QuantumCircuit(2) - evolution_problem = TimeEvolutionProblem(operator, 1, initial_state) - - trotter_qrte = TrotterQRTE() - evolution_result = trotter_qrte.evolve(evolution_problem) - - np.testing.assert_array_almost_equal( - Statevector.from_instruction(evolution_result.evolved_state).data, expected_state.data - ) - - @data( - (QuantumCircuit(1), Statevector([0.23071786 - 0.69436148j, 0.4646314 - 0.49874749j])), - ( - QuantumCircuit(1).compose(ZGate(), [0]), - Statevector([0.23071786 - 0.69436148j, 0.4646314 - 0.49874749j]), - ), - ) - @unpack - def test_trotter_qrte_qdrift(self, initial_state, expected_state): - """Test for TrotterQRTE with QDrift.""" - with self.assertWarns(DeprecationWarning): - operator = PauliSumOp(SparsePauliOp([Pauli("X"), Pauli("Z")])) - time = 1 - evolution_problem = TimeEvolutionProblem(operator, time, initial_state) - - trotter_qrte = TrotterQRTE(product_formula=QDrift(seed=0)) - evolution_result = trotter_qrte.evolve(evolution_problem) - - np.testing.assert_array_almost_equal( - Statevector.from_instruction(evolution_result.evolved_state).data, - expected_state.data, - ) - - @data((Parameter("t"), {}), (None, {Parameter("x"): 2}), (None, None)) - @unpack - def test_trotter_qrte_trotter_param_errors(self, t_param, param_value_dict): - """Test TrotterQRTE with raising errors for parameters.""" - with self.assertWarns(DeprecationWarning): - operator = Parameter("t") * PauliSumOp(SparsePauliOp([Pauli("X")])) + PauliSumOp( - SparsePauliOp([Pauli("Z")]) - ) - initial_state = QuantumCircuit(1) - self._run_error_test(initial_state, operator, None, None, t_param, param_value_dict) - - @data(([Pauli("X"), Pauli("Y")], None)) - @unpack - def test_trotter_qrte_trotter_aux_ops_errors(self, aux_ops, estimator): - """Test TrotterQRTE with raising errors.""" - with self.assertWarns(DeprecationWarning): - operator = PauliSumOp(SparsePauliOp([Pauli("X")])) + PauliSumOp( - SparsePauliOp([Pauli("Z")]) - ) - initial_state = QuantumCircuit(1) - self._run_error_test(initial_state, operator, aux_ops, estimator, None, None) - - @data( - (X, QuantumCircuit(1)), - (MatrixOp([[1, 1], [0, 1]]), QuantumCircuit(1)), - (PauliSumOp(SparsePauliOp([Pauli("X")])) + PauliSumOp(SparsePauliOp([Pauli("Z")])), None), - ( - SparsePauliOp([Pauli("X"), Pauli("Z")], np.array([Parameter("a"), Parameter("b")])), - QuantumCircuit(1), - ), - ) - @unpack - def test_trotter_qrte_trotter_hamiltonian_errors(self, operator, initial_state): - """Test TrotterQRTE with raising errors for evolution problem content.""" - self._run_error_test(initial_state, operator, None, None, None, None) - - @staticmethod - def _run_error_test(initial_state, operator, aux_ops, estimator, t_param, param_value_dict): - time = 1 - with warnings.catch_warnings(): - warnings.filterwarnings("ignore", category=DeprecationWarning) - algorithm_globals.random_seed = 0 - trotter_qrte = TrotterQRTE(estimator=estimator) - with assert_raises(ValueError): - evolution_problem = TimeEvolutionProblem( - operator, - time, - initial_state, - aux_ops, - t_param=t_param, - param_value_map=param_value_dict, - ) - _ = trotter_qrte.evolve(evolution_problem) - - @staticmethod - def _get_expected_trotter_qrte(operator, time, num_timesteps, init_state, observables, t_param): - """Compute reference values for Trotter evolution via exact matrix exponentiation.""" - dt = time / num_timesteps - observables = [obs.to_matrix() for obs in observables] - - psi = Statevector(init_state).data - if t_param is None: - ops = [Pauli(op).to_matrix() * np.real(coeff) for op, coeff in operator.to_list()] - - observable_results = [] - observable_results.append([np.real(np.conj(psi).dot(obs).dot(psi)) for obs in observables]) - - for n in range(num_timesteps): - if t_param is not None: - time_value = (n + 1) * dt - bound = operator.assign_parameters([time_value]) - ops = [Pauli(op).to_matrix() * np.real(coeff) for op, coeff in bound.to_list()] - for op in ops: - psi = expm(-1j * op * dt).dot(psi) - observable_results.append( - [np.real(np.conj(psi).dot(obs).dot(psi)) for obs in observables] - ) - - return psi, observable_results - - -if __name__ == "__main__": - unittest.main() diff --git a/test/python/algorithms/time_evolvers/variational/__init__.py b/test/python/algorithms/time_evolvers/variational/__init__.py deleted file mode 100644 index 26f7536d3514..000000000000 --- a/test/python/algorithms/time_evolvers/variational/__init__.py +++ /dev/null @@ -1,11 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2023. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. diff --git a/test/python/algorithms/time_evolvers/variational/solvers/__init__.py b/test/python/algorithms/time_evolvers/variational/solvers/__init__.py deleted file mode 100644 index 26f7536d3514..000000000000 --- a/test/python/algorithms/time_evolvers/variational/solvers/__init__.py +++ /dev/null @@ -1,11 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2023. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. diff --git a/test/python/algorithms/time_evolvers/variational/solvers/expected_results/__init__.py b/test/python/algorithms/time_evolvers/variational/solvers/expected_results/__init__.py deleted file mode 100644 index 62ef1f76c6f4..000000000000 --- a/test/python/algorithms/time_evolvers/variational/solvers/expected_results/__init__.py +++ /dev/null @@ -1,12 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2023. -# -# 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. -"""Stores expected results that are lengthy.""" diff --git a/test/python/algorithms/time_evolvers/variational/solvers/expected_results/test_varqte_linear_solver_expected_1.py b/test/python/algorithms/time_evolvers/variational/solvers/expected_results/test_varqte_linear_solver_expected_1.py deleted file mode 100644 index aba6b4006f37..000000000000 --- a/test/python/algorithms/time_evolvers/variational/solvers/expected_results/test_varqte_linear_solver_expected_1.py +++ /dev/null @@ -1,182 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2023. -# -# 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. -"""Stores expected results that are lengthy.""" -expected_metric_res_1 = [ - [ - 2.50000000e-01 + 0.0j, - -3.85185989e-33 + 0.0j, - -1.38777878e-17 + 0.0j, - -1.38777878e-17 + 0.0j, - -3.85185989e-33 + 0.0j, - -3.85185989e-33 + 0.0j, - -1.38777878e-17 + 0.0j, - -1.38777878e-17 + 0.0j, - 2.50000000e-01 + 0.0j, - -2.77500000e-17 + 0.0j, - 4.85000000e-17 + 0.0j, - 4.77630626e-32 + 0.0j, - ], - [ - -3.85185989e-33 + 0.0j, - 2.50000000e-01 + 0.0j, - -1.38777878e-17 + 0.0j, - -1.38777878e-17 + 0.0j, - -3.85185989e-33 + 0.0j, - 2.50000000e-01 + 0.0j, - -1.38777878e-17 + 0.0j, - -1.38777878e-17 + 0.0j, - -3.85185989e-33 + 0.0j, - 2.50000000e-01 + 0.0j, - 4.85334346e-32 + 0.0j, - 4.17500000e-17 + 0.0j, - ], - [ - -1.38777878e-17 + 0.0j, - -1.38777878e-17 + 0.0j, - 0.00000000e00 + 0.0j, - 0.00000000e00 + 0.0j, - -1.38777878e-17 + 0.0j, - -1.38777878e-17 + 0.0j, - 0.00000000e00 + 0.0j, - 0.00000000e00 + 0.0j, - -1.38777878e-17 + 0.0j, - -7.00000000e-18 + 0.0j, - 1.38006319e-17 + 0.0j, - -1.39493681e-17 + 0.0j, - ], - [ - -1.38777878e-17 + 0.0j, - -1.38777878e-17 + 0.0j, - 0.00000000e00 + 0.0j, - 0.00000000e00 + 0.0j, - -1.38777878e-17 + 0.0j, - -1.38777878e-17 + 0.0j, - 0.00000000e00 + 0.0j, - 0.00000000e00 + 0.0j, - -1.38777878e-17 + 0.0j, - -7.00000000e-18 + 0.0j, - 1.38006319e-17 + 0.0j, - -1.39493681e-17 + 0.0j, - ], - [ - -3.85185989e-33 + 0.0j, - -3.85185989e-33 + 0.0j, - -1.38777878e-17 + 0.0j, - -1.38777878e-17 + 0.0j, - 2.50000000e-01 + 0.0j, - -3.85185989e-33 + 0.0j, - -1.38777878e-17 + 0.0j, - -1.38777878e-17 + 0.0j, - -3.85185989e-33 + 0.0j, - 0.00000000e00 + 0.0j, - 4.85334346e-32 + 0.0j, - -7.00000000e-18 + 0.0j, - ], - [ - -3.85185989e-33 + 0.0j, - 2.50000000e-01 + 0.0j, - -1.38777878e-17 + 0.0j, - -1.38777878e-17 + 0.0j, - -3.85185989e-33 + 0.0j, - 2.50000000e-01 + 0.0j, - -1.38777878e-17 + 0.0j, - -1.38777878e-17 + 0.0j, - -3.85185989e-33 + 0.0j, - 2.50000000e-01 + 0.0j, - 4.85334346e-32 + 0.0j, - 4.17500000e-17 + 0.0j, - ], - [ - -1.38777878e-17 + 0.0j, - -1.38777878e-17 + 0.0j, - 0.00000000e00 + 0.0j, - 0.00000000e00 + 0.0j, - -1.38777878e-17 + 0.0j, - -1.38777878e-17 + 0.0j, - 0.00000000e00 + 0.0j, - 0.00000000e00 + 0.0j, - -1.38777878e-17 + 0.0j, - -7.00000000e-18 + 0.0j, - 1.38006319e-17 + 0.0j, - -1.39493681e-17 + 0.0j, - ], - [ - -1.38777878e-17 + 0.0j, - -1.38777878e-17 + 0.0j, - 0.00000000e00 + 0.0j, - 0.00000000e00 + 0.0j, - -1.38777878e-17 + 0.0j, - -1.38777878e-17 + 0.0j, - 0.00000000e00 + 0.0j, - 0.00000000e00 + 0.0j, - -1.38777878e-17 + 0.0j, - -7.00000000e-18 + 0.0j, - 1.38006319e-17 + 0.0j, - -1.39493681e-17 + 0.0j, - ], - [ - 2.50000000e-01 + 0.0j, - -3.85185989e-33 + 0.0j, - -1.38777878e-17 + 0.0j, - -1.38777878e-17 + 0.0j, - -3.85185989e-33 + 0.0j, - -3.85185989e-33 + 0.0j, - -1.38777878e-17 + 0.0j, - -1.38777878e-17 + 0.0j, - 2.50000000e-01 + 0.0j, - -2.77500000e-17 + 0.0j, - 4.85000000e-17 + 0.0j, - -7.00000000e-18 + 0.0j, - ], - [ - -2.77500000e-17 + 0.0j, - 2.50000000e-01 + 0.0j, - -7.00000000e-18 + 0.0j, - -7.00000000e-18 + 0.0j, - 0.00000000e00 + 0.0j, - 2.50000000e-01 + 0.0j, - -7.00000000e-18 + 0.0j, - -7.00000000e-18 + 0.0j, - -2.77500000e-17 + 0.0j, - 2.50000000e-01 + 0.0j, - 0.00000000e00 + 0.0j, - 4.17500000e-17 + 0.0j, - ], - [ - 4.85000000e-17 + 0.0j, - 4.85334346e-32 + 0.0j, - 1.38006319e-17 + 0.0j, - 1.38006319e-17 + 0.0j, - 4.85334346e-32 + 0.0j, - 4.85334346e-32 + 0.0j, - 1.38006319e-17 + 0.0j, - 1.38006319e-17 + 0.0j, - 4.85000000e-17 + 0.0j, - 0.00000000e00 + 0.0j, - 2.50000000e-01 + 0.0j, - -2.77500000e-17 + 0.0j, - ], - [ - 4.77630626e-32 + 0.0j, - 4.17500000e-17 + 0.0j, - -1.39493681e-17 + 0.0j, - -1.39493681e-17 + 0.0j, - -7.00000000e-18 + 0.0j, - 4.17500000e-17 + 0.0j, - -1.39493681e-17 + 0.0j, - -1.39493681e-17 + 0.0j, - -7.00000000e-18 + 0.0j, - 4.17500000e-17 + 0.0j, - -2.77500000e-17 + 0.0j, - 2.50000000e-01 + 0.0j, - ], -] diff --git a/test/python/algorithms/time_evolvers/variational/solvers/ode/__init__.py b/test/python/algorithms/time_evolvers/variational/solvers/ode/__init__.py deleted file mode 100644 index 26f7536d3514..000000000000 --- a/test/python/algorithms/time_evolvers/variational/solvers/ode/__init__.py +++ /dev/null @@ -1,11 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2023. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. diff --git a/test/python/algorithms/time_evolvers/variational/solvers/ode/test_forward_euler_solver.py b/test/python/algorithms/time_evolvers/variational/solvers/ode/test_forward_euler_solver.py deleted file mode 100644 index 802f930931d5..000000000000 --- a/test/python/algorithms/time_evolvers/variational/solvers/ode/test_forward_euler_solver.py +++ /dev/null @@ -1,47 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2023. -# -# 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 Forward Euler solver.""" - -import unittest -from test.python.algorithms import QiskitAlgorithmsTestCase -import numpy as np -from ddt import ddt, data, unpack -from scipy.integrate import solve_ivp - -from qiskit.algorithms.time_evolvers.variational.solvers.ode.forward_euler_solver import ( - ForwardEulerSolver, -) - - -@ddt -class TestForwardEulerSolver(QiskitAlgorithmsTestCase): - """Test Forward Euler solver.""" - - @unpack - @data((4, 16), (16, 35.52713678800501), (320, 53.261108839604795)) - def test_solve(self, timesteps, expected_result): - """Test Forward Euler solver for a simple ODE.""" - - y0 = [1] - - # pylint: disable=unused-argument - def func(time, y): - return y - - t_span = [0.0, 4.0] - sol1 = solve_ivp(func, t_span, y0, method=ForwardEulerSolver, num_t_steps=timesteps) - np.testing.assert_equal(sol1.y[-1][-1], expected_result) - - -if __name__ == "__main__": - unittest.main() diff --git a/test/python/algorithms/time_evolvers/variational/solvers/ode/test_ode_function.py b/test/python/algorithms/time_evolvers/variational/solvers/ode/test_ode_function.py deleted file mode 100644 index e680f45063e5..000000000000 --- a/test/python/algorithms/time_evolvers/variational/solvers/ode/test_ode_function.py +++ /dev/null @@ -1,147 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2023. -# -# 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 ODE function generator.""" - -import unittest - -from test.python.algorithms import QiskitAlgorithmsTestCase -import numpy as np -from qiskit.quantum_info import SparsePauliOp -from qiskit.circuit import Parameter -from qiskit.algorithms.time_evolvers.variational.solvers.var_qte_linear_solver import ( - VarQTELinearSolver, -) -from qiskit.algorithms.time_evolvers.variational.solvers.ode.ode_function import ( - OdeFunction, -) -from qiskit.algorithms.time_evolvers.variational import ( - ImaginaryMcLachlanPrinciple, -) -from qiskit.circuit.library import EfficientSU2 - - -class TestOdeFunctionGenerator(QiskitAlgorithmsTestCase): - """Test ODE function generator.""" - - def test_var_qte_ode_function(self): - """Test ODE function generator.""" - observable = SparsePauliOp.from_list( - [ - ("II", 0.2252), - ("ZZ", 0.5716), - ("IZ", 0.3435), - ("ZI", -0.4347), - ("YY", 0.091), - ("XX", 0.091), - ] - ) - - d = 2 - ansatz = EfficientSU2(observable.num_qubits, reps=d) - - # Define a set of initial parameters - parameters = list(ansatz.parameters) - - param_dict = {param: np.pi / 4 for param in parameters} - - var_principle = ImaginaryMcLachlanPrinciple() - - t_param = None - linear_solver = None - linear_solver = VarQTELinearSolver( - var_principle, - observable, - ansatz, - parameters, - t_param, - linear_solver, - ) - - time = 2 - ode_function_generator = OdeFunction(linear_solver, t_param=None, param_dict=param_dict) - - qte_ode_function = ode_function_generator.var_qte_ode_function(time, param_dict.values()) - - expected_qte_ode_function = [ - 0.442145, - -0.022081, - 0.106223, - -0.117468, - 0.251233, - 0.321256, - -0.062728, - -0.036209, - -0.509219, - -0.183459, - -0.050739, - -0.093163, - ] - - np.testing.assert_array_almost_equal(expected_qte_ode_function, qte_ode_function) - - def test_var_qte_ode_function_time_param(self): - """Test ODE function generator with time param.""" - t_param = Parameter("t") - - observable = SparsePauliOp( - ["II", "ZZ", "IZ", "ZI", "YY", "XX"], - np.array([t_param, 0.5716, 0.3435, -0.4347, 0.091, 0.091]), - ) - - d = 2 - ansatz = EfficientSU2(observable.num_qubits, reps=d) - - # Define a set of initial parameters - parameters = list(ansatz.parameters) - - param_dict = {param: np.pi / 4 for param in parameters} - - var_principle = ImaginaryMcLachlanPrinciple() - - time = 2 - - linear_solver = None - varqte_linear_solver = VarQTELinearSolver( - var_principle, - observable, - ansatz, - parameters, - t_param, - linear_solver, - ) - ode_function_generator = OdeFunction( - varqte_linear_solver, t_param=t_param, param_dict=param_dict - ) - - qte_ode_function = ode_function_generator.var_qte_ode_function(time, param_dict.values()) - - expected_qte_ode_function = [ - 0.442145, - -0.022081, - 0.106223, - -0.117468, - 0.251233, - 0.321256, - -0.062728, - -0.036209, - -0.509219, - -0.183459, - -0.050739, - -0.093163, - ] - - np.testing.assert_array_almost_equal(expected_qte_ode_function, qte_ode_function, decimal=5) - - -if __name__ == "__main__": - unittest.main() diff --git a/test/python/algorithms/time_evolvers/variational/solvers/ode/test_var_qte_ode_solver.py b/test/python/algorithms/time_evolvers/variational/solvers/ode/test_var_qte_ode_solver.py deleted file mode 100644 index 4492b3cc9eaf..000000000000 --- a/test/python/algorithms/time_evolvers/variational/solvers/ode/test_var_qte_ode_solver.py +++ /dev/null @@ -1,127 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2023. -# -# 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 solver of ODEs.""" - -import unittest -from test.python.algorithms import QiskitAlgorithmsTestCase -from ddt import ddt, data, unpack -import numpy as np - -from qiskit.quantum_info import SparsePauliOp -from qiskit.algorithms.time_evolvers.variational.solvers.ode.forward_euler_solver import ( - ForwardEulerSolver, -) -from qiskit.algorithms.time_evolvers.variational.solvers.var_qte_linear_solver import ( - VarQTELinearSolver, -) -from qiskit.algorithms.time_evolvers.variational.solvers.ode.var_qte_ode_solver import ( - VarQTEOdeSolver, -) -from qiskit.algorithms.time_evolvers.variational.solvers.ode.ode_function import ( - OdeFunction, -) -from qiskit.algorithms.time_evolvers.variational import ( - ImaginaryMcLachlanPrinciple, -) -from qiskit.circuit.library import EfficientSU2 - - -@ddt -class TestVarQTEOdeSolver(QiskitAlgorithmsTestCase): - """Test solver of ODEs.""" - - @data( - ( - "RK45", - [ - -0.30076755873631345, - -0.8032811383782005, - 1.1674108371914734e-15, - 3.2293849116821145e-16, - 2.541585055586039, - 1.155475184255733, - -2.966331417968169e-16, - 9.604292449638343e-17, - ], - ), - ( - ForwardEulerSolver, - [ - -3.2707e-01, - -8.0960e-01, - 3.4323e-16, - 8.9034e-17, - 2.5290e00, - 1.1563e00, - 3.0227e-16, - -2.2769e-16, - ], - ), - ) - @unpack - def test_run_no_backend(self, ode_solver, expected_result): - """Test ODE solver with no backend.""" - observable = SparsePauliOp.from_list( - [ - ("II", 0.2252), - ("ZZ", 0.5716), - ("IZ", 0.3435), - ("ZI", -0.4347), - ("YY", 0.091), - ("XX", 0.091), - ] - ) - - d = 1 - ansatz = EfficientSU2(observable.num_qubits, reps=d) - - # Define a set of initial parameters - parameters = list(ansatz.parameters) - - init_param_values = np.zeros(len(parameters)) - for i in range(ansatz.num_qubits): - init_param_values[-(ansatz.num_qubits + i + 1)] = np.pi / 2 - - param_dict = dict(zip(parameters, init_param_values)) - - var_principle = ImaginaryMcLachlanPrinciple() - - time = 1 - - t_param = None - - linear_solver = None - linear_solver = VarQTELinearSolver( - var_principle, - observable, - ansatz, - parameters, - t_param, - linear_solver, - ) - ode_function_generator = OdeFunction(linear_solver, param_dict, t_param) - - var_qte_ode_solver = VarQTEOdeSolver( - list(param_dict.values()), - ode_function_generator, - ode_solver=ode_solver, - num_timesteps=25, - ) - - result, _, _ = var_qte_ode_solver.run(time) - - np.testing.assert_array_almost_equal(result, expected_result, decimal=4) - - -if __name__ == "__main__": - unittest.main() diff --git a/test/python/algorithms/time_evolvers/variational/solvers/test_varqte_linear_solver.py b/test/python/algorithms/time_evolvers/variational/solvers/test_varqte_linear_solver.py deleted file mode 100644 index 7b842b95cac0..000000000000 --- a/test/python/algorithms/time_evolvers/variational/solvers/test_varqte_linear_solver.py +++ /dev/null @@ -1,112 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2023. -# -# 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 solver of linear equations.""" - -import unittest -from test.python.algorithms import QiskitAlgorithmsTestCase - -# fmt: off -from test.python.algorithms.time_evolvers.variational.solvers.expected_results.\ - test_varqte_linear_solver_expected_1 import expected_metric_res_1 -# fmt: on - -import numpy as np - -from qiskit.quantum_info import SparsePauliOp -from qiskit.algorithms.time_evolvers.variational import ( - ImaginaryMcLachlanPrinciple, -) -from qiskit.algorithms.time_evolvers.variational.solvers.var_qte_linear_solver import ( - VarQTELinearSolver, -) -from qiskit.circuit.library import EfficientSU2 - - -class TestVarQTELinearSolver(QiskitAlgorithmsTestCase): - """Test solver of linear equations.""" - - def test_solve_lse(self): - """Test SLE solver.""" - - observable = SparsePauliOp.from_list( - [ - ("II", 0.2252), - ("ZZ", 0.5716), - ("IZ", 0.3435), - ("ZI", -0.4347), - ("YY", 0.091), - ("XX", 0.091), - ] - ) - - d = 2 - ansatz = EfficientSU2(observable.num_qubits, reps=d) - - parameters = list(ansatz.parameters) - init_param_values = np.zeros(len(parameters)) - for i in range(ansatz.num_qubits): - init_param_values[-(ansatz.num_qubits + i + 1)] = np.pi / 2 - - param_dict = dict(zip(parameters, init_param_values)) - - var_principle = ImaginaryMcLachlanPrinciple() - t_param = None - linear_solver = None - linear_solver = VarQTELinearSolver( - var_principle, - observable, - ansatz, - parameters, - t_param, - linear_solver, - ) - - nat_grad_res, metric_res, grad_res = linear_solver.solve_lse(param_dict) - - expected_nat_grad_res = [ - 3.43500000e-01, - -2.89800000e-01, - 2.43575264e-16, - 1.31792695e-16, - -9.61200000e-01, - -2.89800000e-01, - 1.27493709e-17, - 1.12587456e-16, - 3.43500000e-01, - -2.89800000e-01, - 3.69914720e-17, - 1.95052083e-17, - ] - - expected_grad_res = [ - (0.17174999999999926 - 0j), - (-0.21735000000000085 + 0j), - (4.114902862895087e-17 - 0j), - (4.114902862895087e-17 - 0j), - (-0.24030000000000012 + 0j), - (-0.21735000000000085 + 0j), - (4.114902862895087e-17 - 0j), - (4.114902862895087e-17 - 0j), - (0.17174999999999918 - 0j), - (-0.21735000000000076 + 0j), - (1.7789936190837538e-17 - 0j), - (-8.319872568662832e-17 + 0j), - ] - - np.testing.assert_array_almost_equal(nat_grad_res, expected_nat_grad_res, decimal=4) - np.testing.assert_array_almost_equal(grad_res, expected_grad_res, decimal=4) - np.testing.assert_array_almost_equal(metric_res, expected_metric_res_1, decimal=4) - - -if __name__ == "__main__": - unittest.main() diff --git a/test/python/algorithms/time_evolvers/variational/test_var_qite.py b/test/python/algorithms/time_evolvers/variational/test_var_qite.py deleted file mode 100644 index b2e431f09f42..000000000000 --- a/test/python/algorithms/time_evolvers/variational/test_var_qite.py +++ /dev/null @@ -1,333 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2023. -# -# 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 Variational Quantum Imaginary Time Evolution algorithm.""" - -import unittest -import warnings - -from test.python.algorithms import QiskitAlgorithmsTestCase -from ddt import ddt -import numpy as np - -from qiskit import QuantumCircuit -from qiskit.circuit import Parameter -from qiskit.algorithms.gradients import LinCombQGT, LinCombEstimatorGradient -from qiskit.primitives import Estimator -from qiskit.quantum_info import SparsePauliOp, Pauli -from qiskit.utils import algorithm_globals -from qiskit.algorithms import TimeEvolutionProblem, VarQITE -from qiskit.algorithms.time_evolvers.variational import ( - ImaginaryMcLachlanPrinciple, -) -from qiskit.circuit.library import EfficientSU2 -from qiskit.quantum_info import Statevector - - -@ddt -class TestVarQITE(QiskitAlgorithmsTestCase): - """Test Variational Quantum Imaginary Time Evolution algorithm.""" - - def setUp(self): - super().setUp() - self.seed = 11 - np.random.seed(self.seed) - - def test_run_d_1_with_aux_ops(self): - """Test VarQITE for d = 1 and t = 1 with evaluating auxiliary operator and the Forward - Euler solver..""" - - observable = SparsePauliOp.from_list( - [ - ("II", 0.2252), - ("ZZ", 0.5716), - ("IZ", 0.3435), - ("ZI", -0.4347), - ("YY", 0.091), - ("XX", 0.091), - ] - ) - aux_ops = [Pauli("XX"), Pauli("YZ")] - d = 1 - ansatz = EfficientSU2(observable.num_qubits, reps=d) - - parameters = list(ansatz.parameters) - init_param_values = np.zeros(len(parameters)) - for i in range(len(parameters)): - init_param_values[i] = np.pi / 2 - init_param_values[0] = 1 - time = 1 - - evolution_problem = TimeEvolutionProblem(observable, time, aux_operators=aux_ops) - - thetas_expected = [ - 0.87984606025879, - 2.04681975664763, - 2.68980594039104, - 2.75915988512186, - 2.38796546567171, - 1.78144857115127, - 2.13109162826101, - 1.9259609596416, - ] - - thetas_expected_shots = [ - 0.9392668013702317, - 1.8756706968454864, - 2.6915067128662398, - 2.655420131540562, - 2.174687086978046, - 1.6997059390911056, - 1.8056912289547045, - 1.939353810908912, - ] - - with self.subTest(msg="Test exact backend."): - with warnings.catch_warnings(): - warnings.filterwarnings("ignore", category=DeprecationWarning) - algorithm_globals.random_seed = self.seed - estimator = Estimator() - qgt = LinCombQGT(estimator) - gradient = LinCombEstimatorGradient(estimator) - var_principle = ImaginaryMcLachlanPrinciple(qgt, gradient) - - var_qite = VarQITE( - ansatz, init_param_values, var_principle, estimator, num_timesteps=25 - ) - evolution_result = var_qite.evolve(evolution_problem) - - aux_ops = evolution_result.aux_ops_evaluated - - parameter_values = evolution_result.parameter_values[-1] - - expected_aux_ops = (-0.2177982985749799, 0.2556790598588627) - - for i, parameter_value in enumerate(parameter_values): - np.testing.assert_almost_equal( - float(parameter_value), thetas_expected[i], decimal=2 - ) - - np.testing.assert_array_almost_equal( - [result[0] for result in aux_ops], expected_aux_ops - ) - - with self.subTest(msg="Test shot-based backend."): - with warnings.catch_warnings(): - warnings.filterwarnings("ignore", category=DeprecationWarning) - algorithm_globals.random_seed = self.seed - - estimator = Estimator(options={"shots": 4096, "seed": self.seed}) - qgt = LinCombQGT(estimator) - gradient = LinCombEstimatorGradient(estimator) - var_principle = ImaginaryMcLachlanPrinciple(qgt, gradient) - - var_qite = VarQITE( - ansatz, init_param_values, var_principle, estimator, num_timesteps=25 - ) - evolution_result = var_qite.evolve(evolution_problem) - - aux_ops = evolution_result.aux_ops_evaluated - - parameter_values = evolution_result.parameter_values[-1] - - expected_aux_ops = (-0.24629853310903974, 0.2518122871921184) - - for i, parameter_value in enumerate(parameter_values): - np.testing.assert_almost_equal( - float(parameter_value), thetas_expected_shots[i], decimal=2 - ) - - np.testing.assert_array_almost_equal( - [result[0] for result in aux_ops], expected_aux_ops - ) - - def test_run_d_1_t_7(self): - """Test VarQITE for d = 1 and t = 7 with RK45 ODE solver.""" - - observable = SparsePauliOp.from_list( - [ - ("II", 0.2252), - ("ZZ", 0.5716), - ("IZ", 0.3435), - ("ZI", -0.4347), - ("YY", 0.091), - ("XX", 0.091), - ] - ) - - d = 1 - ansatz = EfficientSU2(observable.num_qubits, reps=d) - - parameters = list(ansatz.parameters) - init_param_values = np.zeros(len(parameters)) - for i in range(len(parameters)): - init_param_values[i] = np.pi / 2 - init_param_values[0] = 1 - var_principle = ImaginaryMcLachlanPrinciple() - - time = 7 - var_qite = VarQITE( - ansatz, init_param_values, var_principle, ode_solver="RK45", num_timesteps=25 - ) - - thetas_expected = [ - 0.828917365718767, - 1.88481074798033, - 3.14111335991238, - 3.14125849601269, - 2.33768562678401, - 1.78670990729437, - 2.04214275514208, - 2.04009918594422, - ] - - self._test_helper(observable, thetas_expected, time, var_qite, 2) - - def test_run_d_2(self): - """Test VarQITE for d = 2 and t = 1 with RK45 ODE solver.""" - - observable = SparsePauliOp.from_list( - [ - ("II", 0.2252), - ("ZZ", 0.5716), - ("IZ", 0.3435), - ("ZI", -0.4347), - ("YY", 0.091), - ("XX", 0.091), - ] - ) - d = 2 - ansatz = EfficientSU2(observable.num_qubits, reps=d) - - parameters = list(ansatz.parameters) - init_param_values = np.zeros(len(parameters)) - for i in range(len(parameters)): - init_param_values[i] = np.pi / 4 - - var_principle = ImaginaryMcLachlanPrinciple() - - time = 1 - var_qite = VarQITE( - ansatz, init_param_values, var_principle, ode_solver="RK45", num_timesteps=25 - ) - - thetas_expected = [ - 1.29495364023786, - 1.08970061333559, - 0.667488228710748, - 0.500122687902944, - 1.4377736672043, - 1.22881086103085, - 0.729773048146251, - 1.01698854755226, - 0.050807780587492, - 0.294828474947149, - 0.839305697704923, - 0.663689581255428, - ] - - self._test_helper(observable, thetas_expected, time, var_qite, 4) - - def test_run_d_1_time_dependent(self): - """Test VarQITE for d = 1 and a time-dependent Hamiltonian with the Forward Euler solver.""" - t_param = Parameter("t") - time = 1 - observable = SparsePauliOp(["I", "Z"], np.array([0, t_param])) - - x, y, z = [Parameter(s) for s in "xyz"] - ansatz = QuantumCircuit(1) - ansatz.rz(x, 0) - ansatz.ry(y, 0) - ansatz.rz(z, 0) - - parameters = list(ansatz.parameters) - init_param_values = np.zeros(len(parameters)) - x_val = 0 - y_val = np.pi / 2 - z_val = 0 - - init_param_values[0] = x_val - init_param_values[1] = y_val - init_param_values[2] = z_val - - evolution_problem = TimeEvolutionProblem(observable, time, t_param=t_param) - - thetas_expected = [1.83881002737137e-18, 2.43224994794434, -3.05311331771918e-18] - - thetas_expected_shots = [1.83881002737137e-18, 2.43224994794434, -3.05311331771918e-18] - - state_expected = Statevector([0.34849948 + 0.0j, 0.93730897 + 0.0j]).to_dict() - # the expected final state is Statevector([0.34849948+0.j, 0.93730897+0.j]) - - with self.subTest(msg="Test exact backend."): - with warnings.catch_warnings(): - warnings.filterwarnings("ignore", category=DeprecationWarning) - algorithm_globals.random_seed = self.seed - estimator = Estimator() - var_principle = ImaginaryMcLachlanPrinciple() - - var_qite = VarQITE( - ansatz, init_param_values, var_principle, estimator, num_timesteps=100 - ) - evolution_result = var_qite.evolve(evolution_problem) - evolved_state = evolution_result.evolved_state - parameter_values = evolution_result.parameter_values[-1] - - for key, evolved_value in Statevector(evolved_state).to_dict().items(): - # np.allclose works with complex numbers - self.assertTrue(np.allclose(evolved_value, state_expected[key], 1e-02)) - - for i, parameter_value in enumerate(parameter_values): - np.testing.assert_almost_equal( - float(parameter_value), thetas_expected[i], decimal=2 - ) - - with self.subTest(msg="Test shot-based backend."): - with warnings.catch_warnings(): - warnings.filterwarnings("ignore", category=DeprecationWarning) - algorithm_globals.random_seed = self.seed - - estimator = Estimator(options={"shots": 4 * 4096, "seed": self.seed}) - var_principle = ImaginaryMcLachlanPrinciple() - - var_qite = VarQITE( - ansatz, init_param_values, var_principle, estimator, num_timesteps=100 - ) - - evolution_result = var_qite.evolve(evolution_problem) - - evolved_state = evolution_result.evolved_state - - parameter_values = evolution_result.parameter_values[-1] - - for key, evolved_value in Statevector(evolved_state).to_dict().items(): - # np.allclose works with complex numbers - self.assertTrue(np.allclose(evolved_value, state_expected[key], 1e-02)) - - for i, parameter_value in enumerate(parameter_values): - np.testing.assert_almost_equal( - float(parameter_value), thetas_expected_shots[i], decimal=2 - ) - - def _test_helper(self, observable, thetas_expected, time, var_qite, decimal): - evolution_problem = TimeEvolutionProblem(observable, time) - evolution_result = var_qite.evolve(evolution_problem) - parameter_values = evolution_result.parameter_values[-1] - - for i, parameter_value in enumerate(parameter_values): - np.testing.assert_almost_equal( - float(parameter_value), thetas_expected[i], decimal=decimal - ) - - -if __name__ == "__main__": - unittest.main() diff --git a/test/python/algorithms/time_evolvers/variational/test_var_qrte.py b/test/python/algorithms/time_evolvers/variational/test_var_qrte.py deleted file mode 100644 index e652125728b0..000000000000 --- a/test/python/algorithms/time_evolvers/variational/test_var_qrte.py +++ /dev/null @@ -1,319 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2023. -# -# 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 Variational Quantum Real Time Evolution algorithm.""" - -import unittest -import warnings -from test.python.algorithms import QiskitAlgorithmsTestCase - -from ddt import ddt -import numpy as np - -from qiskit import QuantumCircuit -from qiskit.circuit import Parameter, ParameterVector -from qiskit.algorithms.gradients import LinCombQGT, DerivativeType, LinCombEstimatorGradient -from qiskit.primitives import Estimator -from qiskit.utils import algorithm_globals -from qiskit.quantum_info import SparsePauliOp, Pauli, Statevector -from qiskit.algorithms import TimeEvolutionProblem, VarQRTE -from qiskit.algorithms.time_evolvers.variational import ( - RealMcLachlanPrinciple, -) -from qiskit.circuit.library import EfficientSU2 - - -@ddt -class TestVarQRTE(QiskitAlgorithmsTestCase): - """Test Variational Quantum Real Time Evolution algorithm.""" - - def setUp(self): - super().setUp() - self.seed = 11 - np.random.seed(self.seed) - - def test_time_dependent_hamiltonian(self): - """Simple test case with a time dependent Hamiltonian.""" - t_param = Parameter("t") - hamiltonian = SparsePauliOp(["Z"], np.array(t_param)) - - x = ParameterVector("x", 3) - circuit = QuantumCircuit(1) - circuit.rz(x[0], 0) - circuit.ry(x[1], 0) - circuit.rz(x[2], 0) - - initial_parameters = np.array([0, np.pi / 2, 0]) - - def expected_state(time): - # possible with pen and paper as the Hamiltonian is diagonal - return 1 / np.sqrt(2) * np.array([np.exp(-0.5j * time**2), np.exp(0.5j * time**2)]) - - final_time = 0.75 - evolution_problem = TimeEvolutionProblem(hamiltonian, t_param=t_param, time=final_time) - estimator = Estimator() - varqrte = VarQRTE(circuit, initial_parameters, estimator=estimator) - - result = varqrte.evolve(evolution_problem) - - final_parameters = result.parameter_values[-1] - final_state = Statevector(circuit.assign_parameters(final_parameters)).to_dict() - final_expected_state = expected_state(final_time) - - for key, expected_value in final_state.items(): - self.assertTrue(np.allclose(final_expected_state[int(key)], expected_value, 1e-02)) - - def test_run_d_1_with_aux_ops(self): - """Test VarQRTE for d = 1 and t = 0.1 with evaluating auxiliary operators and the Forward - Euler solver.""" - observable = SparsePauliOp.from_list( - [ - ("II", 0.2252), - ("ZZ", 0.5716), - ("IZ", 0.3435), - ("ZI", -0.4347), - ("YY", 0.091), - ("XX", 0.091), - ] - ) - aux_ops = [Pauli("XX"), Pauli("YZ")] - d = 1 - ansatz = EfficientSU2(observable.num_qubits, reps=d) - - parameters = list(ansatz.parameters) - init_param_values = np.zeros(len(parameters)) - for i in range(len(parameters)): - init_param_values[i] = np.pi / 2 - init_param_values[0] = 1 - - time = 0.1 - - evolution_problem = TimeEvolutionProblem(observable, time, aux_operators=aux_ops) - - thetas_expected = [ - 0.886841151529636, - 1.53852629218265, - 1.57099556659882, - 1.5889216657174, - 1.5996487153364, - 1.57018939515742, - 1.63950719260698, - 1.53853696496673, - ] - - thetas_expected_shots = [ - 0.886975892820015, - 1.53822607733397, - 1.57058096749141, - 1.59023223608564, - 1.60105707043745, - 1.57018042397236, - 1.64010900210835, - 1.53959523034133, - ] - - with self.subTest(msg="Test exact backend."): - with warnings.catch_warnings(): - warnings.filterwarnings("ignore", category=DeprecationWarning) - algorithm_globals.random_seed = self.seed - estimator = Estimator() - qgt = LinCombQGT(estimator) - gradient = LinCombEstimatorGradient(estimator, derivative_type=DerivativeType.IMAG) - var_principle = RealMcLachlanPrinciple(qgt, gradient) - - var_qrte = VarQRTE( - ansatz, init_param_values, var_principle, estimator, num_timesteps=25 - ) - evolution_result = var_qrte.evolve(evolution_problem) - - aux_ops = evolution_result.aux_ops_evaluated - - parameter_values = evolution_result.parameter_values[-1] - - expected_aux_ops = [0.06836996703935797, 0.7711574493422457] - - for i, parameter_value in enumerate(parameter_values): - np.testing.assert_almost_equal( - float(parameter_value), thetas_expected[i], decimal=2 - ) - - np.testing.assert_array_almost_equal( - [result[0] for result in aux_ops], expected_aux_ops - ) - - with self.subTest(msg="Test shot-based backend."): - with warnings.catch_warnings(): - warnings.filterwarnings("ignore", category=DeprecationWarning) - algorithm_globals.random_seed = self.seed - - estimator = Estimator(options={"shots": 4 * 4096, "seed": self.seed}) - qgt = LinCombQGT(estimator) - gradient = LinCombEstimatorGradient(estimator, derivative_type=DerivativeType.IMAG) - var_principle = RealMcLachlanPrinciple(qgt, gradient) - - var_qrte = VarQRTE( - ansatz, init_param_values, var_principle, estimator, num_timesteps=25 - ) - evolution_result = var_qrte.evolve(evolution_problem) - - aux_ops = evolution_result.aux_ops_evaluated - - parameter_values = evolution_result.parameter_values[-1] - - expected_aux_ops = [ - 0.070436, - 0.777938, - ] - - for i, parameter_value in enumerate(parameter_values): - np.testing.assert_almost_equal( - float(parameter_value), thetas_expected_shots[i], decimal=2 - ) - - np.testing.assert_array_almost_equal( - [result[0] for result in aux_ops], expected_aux_ops, decimal=2 - ) - - def test_run_d_2(self): - """Test VarQRTE for d = 2 and t = 1 with RK45 ODE solver.""" - - observable = SparsePauliOp.from_list( - [ - ("II", 0.2252), - ("ZZ", 0.5716), - ("IZ", 0.3435), - ("ZI", -0.4347), - ("YY", 0.091), - ("XX", 0.091), - ] - ) - d = 2 - ansatz = EfficientSU2(observable.num_qubits, reps=d) - - parameters = list(ansatz.parameters) - init_param_values = np.zeros(len(parameters)) - for i in range(len(parameters)): - init_param_values[i] = np.pi / 4 - estimator = Estimator() - qgt = LinCombQGT(estimator) - gradient = LinCombEstimatorGradient(estimator, derivative_type=DerivativeType.IMAG) - - var_principle = RealMcLachlanPrinciple(qgt, gradient) - - param_dict = dict(zip(parameters, init_param_values)) - - time = 1 - var_qrte = VarQRTE(ansatz, param_dict, var_principle, ode_solver="RK45", num_timesteps=25) - - thetas_expected = [ - 0.348407744196573, - 0.919404626262464, - 1.18189219371626, - 0.771011177789998, - 0.734384256533924, - 0.965289520781899, - 1.14441687204195, - 1.17231927568571, - 1.03014771379412, - 0.867266309056347, - 0.699606368428206, - 0.610788576398685, - ] - - self._test_helper(observable, thetas_expected, time, var_qrte) - - def test_run_d_1_time_dependent(self): - """Test VarQRTE for d = 1 and a time-dependent Hamiltonian with the Forward Euler solver.""" - t_param = Parameter("t") - time = 1 - observable = SparsePauliOp(["I", "Z"], np.array([0, t_param])) - - x, y, z = [Parameter(s) for s in "xyz"] - ansatz = QuantumCircuit(1) - ansatz.rz(x, 0) - ansatz.ry(y, 0) - ansatz.rz(z, 0) - - parameters = list(ansatz.parameters) - init_param_values = np.zeros(len(parameters)) - x_val = 0 - y_val = np.pi / 2 - z_val = 0 - - init_param_values[0] = x_val - init_param_values[1] = y_val - init_param_values[2] = z_val - - evolution_problem = TimeEvolutionProblem(observable, time, t_param=t_param) - - thetas_expected = [1.27675647831902e-18, 1.5707963267949, 0.990000000000001] - - thetas_expected_shots = [0.00534345821469238, 1.56260960200375, 0.990017403734316] - - # the expected final state is Statevector([0.62289306-0.33467034j, 0.62289306+0.33467034j]) - - with self.subTest(msg="Test exact backend."): - with warnings.catch_warnings(): - warnings.filterwarnings("ignore", category=DeprecationWarning) - algorithm_globals.random_seed = self.seed - estimator = Estimator() - qgt = LinCombQGT(estimator) - gradient = LinCombEstimatorGradient(estimator, derivative_type=DerivativeType.IMAG) - var_principle = RealMcLachlanPrinciple(qgt, gradient) - - var_qrte = VarQRTE( - ansatz, init_param_values, var_principle, estimator, num_timesteps=100 - ) - evolution_result = var_qrte.evolve(evolution_problem) - - parameter_values = evolution_result.parameter_values[-1] - - for i, parameter_value in enumerate(parameter_values): - np.testing.assert_almost_equal( - float(parameter_value), thetas_expected[i], decimal=2 - ) - - with self.subTest(msg="Test shot-based backend."): - with warnings.catch_warnings(): - warnings.filterwarnings("ignore", category=DeprecationWarning) - algorithm_globals.random_seed = self.seed - - estimator = Estimator(options={"shots": 4 * 4096, "seed": self.seed}) - qgt = LinCombQGT(estimator) - gradient = LinCombEstimatorGradient(estimator, derivative_type=DerivativeType.IMAG) - var_principle = RealMcLachlanPrinciple(qgt, gradient) - - var_qrte = VarQRTE( - ansatz, init_param_values, var_principle, estimator, num_timesteps=100 - ) - - evolution_result = var_qrte.evolve(evolution_problem) - - parameter_values = evolution_result.parameter_values[-1] - - for i, parameter_value in enumerate(parameter_values): - np.testing.assert_almost_equal( - float(parameter_value), thetas_expected_shots[i], decimal=2 - ) - - def _test_helper(self, observable, thetas_expected, time, var_qrte): - evolution_problem = TimeEvolutionProblem(observable, time) - evolution_result = var_qrte.evolve(evolution_problem) - - parameter_values = evolution_result.parameter_values[-1] - - for i, parameter_value in enumerate(parameter_values): - np.testing.assert_almost_equal(float(parameter_value), thetas_expected[i], decimal=4) - - -if __name__ == "__main__": - unittest.main() diff --git a/test/python/algorithms/time_evolvers/variational/test_var_qte.py b/test/python/algorithms/time_evolvers/variational/test_var_qte.py deleted file mode 100644 index 4b92e4e460d0..000000000000 --- a/test/python/algorithms/time_evolvers/variational/test_var_qte.py +++ /dev/null @@ -1,84 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2023. -# -# 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 Variational Quantum Real Time Evolution algorithm.""" - -import unittest - -from test.python.algorithms import QiskitAlgorithmsTestCase -from numpy.testing import assert_raises -from ddt import data, ddt -import numpy as np - -from qiskit.algorithms.time_evolvers.variational.var_qte import VarQTE -from qiskit.circuit import Parameter - - -@ddt -class TestVarQTE(QiskitAlgorithmsTestCase): - """Test Variational Quantum Time Evolution class methods.""" - - def setUp(self): - super().setUp() - self._parameters1 = [Parameter("a"), Parameter("b"), Parameter("c")] - - @data([1.4, 2, 3], np.asarray([1.4, 2, 3])) - def test_create_init_state_param_dict(self, param_values): - """Tests if a correct dictionary is created.""" - expected = dict(zip(self._parameters1, param_values)) - with self.subTest("Parameters values given as a list test."): - result = VarQTE._create_init_state_param_dict(param_values, self._parameters1) - np.testing.assert_equal(result, expected) - with self.subTest("Parameters values given as a dictionary test."): - result = VarQTE._create_init_state_param_dict( - dict(zip(self._parameters1, param_values)), self._parameters1 - ) - np.testing.assert_equal(result, expected) - with self.subTest("Parameters values given as a superset dictionary test."): - expected = dict( - zip( - [self._parameters1[0], self._parameters1[2]], [param_values[0], param_values[2]] - ) - ) - result = VarQTE._create_init_state_param_dict( - dict(zip(self._parameters1, param_values)), - [self._parameters1[0], self._parameters1[2]], - ) - np.testing.assert_equal(result, expected) - - @data([1.4, 2], np.asarray([1.4, 3]), {}, []) - def test_create_init_state_param_dict_errors_list(self, param_values): - """Tests if an error is raised.""" - with assert_raises(ValueError): - _ = VarQTE._create_init_state_param_dict(param_values, self._parameters1) - - @data([1.4, 2], np.asarray([1.4, 3])) - def test_create_init_state_param_dict_errors_subset(self, param_values): - """Tests if an error is raised if subset of parameters provided.""" - param_values_dict = dict(zip([self._parameters1[0], self._parameters1[2]], param_values)) - with assert_raises(ValueError): - _ = VarQTE._create_init_state_param_dict(param_values_dict, self._parameters1) - - @data("s") - def test_create_init_state_param_dict_errors_value(self, param_values): - """Tests if an error is raised if wrong input.""" - with assert_raises(ValueError): - _ = VarQTE._create_init_state_param_dict(param_values, self._parameters1) - - @data(Parameter("x"), 5) - def test_create_init_state_param_dict_errors_type(self, param_values): - """Tests if an error is raised if wrong input type.""" - with assert_raises(TypeError): - _ = VarQTE._create_init_state_param_dict(param_values, self._parameters1) - - -if __name__ == "__main__": - unittest.main() diff --git a/test/python/algorithms/time_evolvers/variational/variational_principles/__init__.py b/test/python/algorithms/time_evolvers/variational/variational_principles/__init__.py deleted file mode 100644 index 26f7536d3514..000000000000 --- a/test/python/algorithms/time_evolvers/variational/variational_principles/__init__.py +++ /dev/null @@ -1,11 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2023. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. diff --git a/test/python/algorithms/time_evolvers/variational/variational_principles/expected_results/__init__.py b/test/python/algorithms/time_evolvers/variational/variational_principles/expected_results/__init__.py deleted file mode 100644 index 62ef1f76c6f4..000000000000 --- a/test/python/algorithms/time_evolvers/variational/variational_principles/expected_results/__init__.py +++ /dev/null @@ -1,12 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2023. -# -# 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. -"""Stores expected results that are lengthy.""" diff --git a/test/python/algorithms/time_evolvers/variational/variational_principles/expected_results/test_imaginary_mc_lachlan_variational_principle_expected1.py b/test/python/algorithms/time_evolvers/variational/variational_principles/expected_results/test_imaginary_mc_lachlan_variational_principle_expected1.py deleted file mode 100644 index a26cb1b8726b..000000000000 --- a/test/python/algorithms/time_evolvers/variational/variational_principles/expected_results/test_imaginary_mc_lachlan_variational_principle_expected1.py +++ /dev/null @@ -1,182 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2023. -# -# 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. -"""Stores expected results that are lengthy.""" -expected_bound_metric_tensor_1 = [ - [ - 2.50000000e-01 + 0.0j, - 1.59600000e-33 + 0.0j, - 5.90075760e-18 + 0.0j, - -8.49242405e-19 + 0.0j, - 8.83883476e-02 + 0.0j, - 1.33253788e-17 + 0.0j, - 6.25000000e-02 + 0.0j, - 1.40000000e-17 + 0.0j, - -1.41735435e-01 + 0.0j, - 3.12500000e-02 + 0.0j, - 1.00222087e-01 + 0.0j, - -3.12500000e-02 + 0.0j, - ], - [ - 1.59600000e-33 + 0.0j, - 2.50000000e-01 + 0.0j, - 1.34350288e-17 + 0.0j, - 6.43502884e-18 + 0.0j, - -8.83883476e-02 + 0.0j, - 1.25000000e-01 + 0.0j, - 6.25000000e-02 + 0.0j, - 1.25000000e-01 + 0.0j, - -8.45970869e-02 + 0.0j, - 7.54441738e-02 + 0.0j, - 1.48207521e-01 + 0.0j, - 2.00444174e-01 + 0.0j, - ], - [ - 5.90075760e-18 + 0.0j, - 1.34350288e-17 + 0.0j, - 1.25000000e-01 + 0.0j, - -1.38777878e-17 + 0.0j, - -4.41941738e-02 + 0.0j, - 6.25000000e-02 + 0.0j, - 1.19638348e-01 + 0.0j, - 6.25000000e-02 + 0.0j, - -5.14514565e-02 + 0.0j, - 6.89720869e-02 + 0.0j, - 1.04933262e-02 + 0.0j, - -6.89720869e-02 + 0.0j, - ], - [ - -8.49242405e-19 + 0.0j, - 6.43502884e-18 + 0.0j, - -1.38777878e-17 + 0.0j, - 1.25000000e-01 + 0.0j, - -4.41941738e-02 + 0.0j, - -6.25000000e-02 + 0.0j, - 3.12500000e-02 + 0.0j, - 1.25000000e-01 + 0.0j, - 5.14514565e-02 + 0.0j, - -6.89720869e-02 + 0.0j, - 7.81250000e-03 + 0.0j, - 1.94162607e-02 + 0.0j, - ], - [ - 8.83883476e-02 + 0.0j, - -8.83883476e-02 + 0.0j, - -4.41941738e-02 + 0.0j, - -4.41941738e-02 + 0.0j, - 2.34375000e-01 + 0.0j, - -1.10485435e-01 + 0.0j, - -2.02014565e-02 + 0.0j, - -4.41941738e-02 + 0.0j, - 1.49547935e-02 + 0.0j, - -2.24896848e-02 + 0.0j, - -1.42172278e-03 + 0.0j, - -1.23822206e-01 + 0.0j, - ], - [ - 1.33253788e-17 + 0.0j, - 1.25000000e-01 + 0.0j, - 6.25000000e-02 + 0.0j, - -6.25000000e-02 + 0.0j, - -1.10485435e-01 + 0.0j, - 2.18750000e-01 + 0.0j, - -2.68082618e-03 + 0.0j, - -1.59099026e-17 + 0.0j, - -1.57197815e-01 + 0.0j, - 2.53331304e-02 + 0.0j, - 9.82311963e-03 + 0.0j, - 1.06138957e-01 + 0.0j, - ], - [ - 6.25000000e-02 + 0.0j, - 6.25000000e-02 + 0.0j, - 1.19638348e-01 + 0.0j, - 3.12500000e-02 + 0.0j, - -2.02014565e-02 + 0.0j, - -2.68082618e-03 + 0.0j, - 2.23881674e-01 + 0.0j, - 1.37944174e-01 + 0.0j, - -3.78033966e-02 + 0.0j, - 1.58423239e-01 + 0.0j, - 1.34535646e-01 + 0.0j, - -5.49651086e-02 + 0.0j, - ], - [ - 1.40000000e-17 + 0.0j, - 1.25000000e-01 + 0.0j, - 6.25000000e-02 + 0.0j, - 1.25000000e-01 + 0.0j, - -4.41941738e-02 + 0.0j, - -1.59099026e-17 + 0.0j, - 1.37944174e-01 + 0.0j, - 2.50000000e-01 + 0.0j, - -2.10523539e-17 + 0.0j, - 1.15574269e-17 + 0.0j, - 9.75412607e-02 + 0.0j, - 5.71383476e-02 + 0.0j, - ], - [ - -1.41735435e-01 + 0.0j, - -8.45970869e-02 + 0.0j, - -5.14514565e-02 + 0.0j, - 5.14514565e-02 + 0.0j, - 1.49547935e-02 + 0.0j, - -1.57197815e-01 + 0.0j, - -3.78033966e-02 + 0.0j, - -2.10523539e-17 + 0.0j, - 1.95283753e-01 + 0.0j, - -3.82941440e-02 + 0.0j, - -6.11392595e-02 + 0.0j, - -4.51588288e-02 + 0.0j, - ], - [ - 3.12500000e-02 + 0.0j, - 7.54441738e-02 + 0.0j, - 6.89720869e-02 + 0.0j, - -6.89720869e-02 + 0.0j, - -2.24896848e-02 + 0.0j, - 2.53331304e-02 + 0.0j, - 1.58423239e-01 + 0.0j, - 1.15574269e-17 + 0.0j, - -3.82941440e-02 + 0.0j, - 2.17629701e-01 + 0.0j, - 1.32431810e-01 + 0.0j, - -1.91961467e-02 + 0.0j, - ], - [ - 1.00222087e-01 + 0.0j, - 1.48207521e-01 + 0.0j, - 1.04933262e-02 + 0.0j, - 7.81250000e-03 + 0.0j, - -1.42172278e-03 + 0.0j, - 9.82311963e-03 + 0.0j, - 1.34535646e-01 + 0.0j, - 9.75412607e-02 + 0.0j, - -6.11392595e-02 + 0.0j, - 1.32431810e-01 + 0.0j, - 1.81683746e-01 + 0.0j, - 7.28902444e-02 + 0.0j, - ], - [ - -3.12500000e-02 + 0.0j, - 2.00444174e-01 + 0.0j, - -6.89720869e-02 + 0.0j, - 1.94162607e-02 + 0.0j, - -1.23822206e-01 + 0.0j, - 1.06138957e-01 + 0.0j, - -5.49651086e-02 + 0.0j, - 5.71383476e-02 + 0.0j, - -4.51588288e-02 + 0.0j, - -1.91961467e-02 + 0.0j, - 7.28902444e-02 + 0.0j, - 2.38616353e-01 + 0.0j, - ], -] diff --git a/test/python/algorithms/time_evolvers/variational/variational_principles/expected_results/test_imaginary_mc_lachlan_variational_principle_expected2.py b/test/python/algorithms/time_evolvers/variational/variational_principles/expected_results/test_imaginary_mc_lachlan_variational_principle_expected2.py deleted file mode 100644 index 3a46f371787c..000000000000 --- a/test/python/algorithms/time_evolvers/variational/variational_principles/expected_results/test_imaginary_mc_lachlan_variational_principle_expected2.py +++ /dev/null @@ -1,182 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2023. -# -# 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. -"""Stores expected results that are lengthy.""" -expected_bound_metric_tensor_2 = [ - [ - 2.50000000e-01 + 0.0j, - 1.59600000e-33 + 0.0j, - 5.90075760e-18 + 0.0j, - -8.49242405e-19 + 0.0j, - 8.83883476e-02 + 0.0j, - 1.33253788e-17 + 0.0j, - 6.25000000e-02 + 0.0j, - 1.40000000e-17 + 0.0j, - -1.41735435e-01 + 0.0j, - 3.12500000e-02 + 0.0j, - 1.00222087e-01 + 0.0j, - -3.12500000e-02 + 0.0j, - ], - [ - 1.59600000e-33 + 0.0j, - 2.50000000e-01 + 0.0j, - 1.34350288e-17 + 0.0j, - 6.43502884e-18 + 0.0j, - -8.83883476e-02 + 0.0j, - 1.25000000e-01 + 0.0j, - 6.25000000e-02 + 0.0j, - 1.25000000e-01 + 0.0j, - -8.45970869e-02 + 0.0j, - 7.54441738e-02 + 0.0j, - 1.48207521e-01 + 0.0j, - 2.00444174e-01 + 0.0j, - ], - [ - 5.90075760e-18 + 0.0j, - 1.34350288e-17 + 0.0j, - 1.25000000e-01 + 0.0j, - -1.38777878e-17 + 0.0j, - -4.41941738e-02 + 0.0j, - 6.25000000e-02 + 0.0j, - 1.19638348e-01 + 0.0j, - 6.25000000e-02 + 0.0j, - -5.14514565e-02 + 0.0j, - 6.89720869e-02 + 0.0j, - 1.04933262e-02 + 0.0j, - -6.89720869e-02 + 0.0j, - ], - [ - -8.49242405e-19 + 0.0j, - 6.43502884e-18 + 0.0j, - -1.38777878e-17 + 0.0j, - 1.25000000e-01 + 0.0j, - -4.41941738e-02 + 0.0j, - -6.25000000e-02 + 0.0j, - 3.12500000e-02 + 0.0j, - 1.25000000e-01 + 0.0j, - 5.14514565e-02 + 0.0j, - -6.89720869e-02 + 0.0j, - 7.81250000e-03 + 0.0j, - 1.94162607e-02 + 0.0j, - ], - [ - 8.83883476e-02 + 0.0j, - -8.83883476e-02 + 0.0j, - -4.41941738e-02 + 0.0j, - -4.41941738e-02 + 0.0j, - 2.34375000e-01 + 0.0j, - -1.10485435e-01 + 0.0j, - -2.02014565e-02 + 0.0j, - -4.41941738e-02 + 0.0j, - 1.49547935e-02 + 0.0j, - -2.24896848e-02 + 0.0j, - -1.42172278e-03 + 0.0j, - -1.23822206e-01 + 0.0j, - ], - [ - 1.33253788e-17 + 0.0j, - 1.25000000e-01 + 0.0j, - 6.25000000e-02 + 0.0j, - -6.25000000e-02 + 0.0j, - -1.10485435e-01 + 0.0j, - 2.18750000e-01 + 0.0j, - -2.68082618e-03 + 0.0j, - -1.59099026e-17 + 0.0j, - -1.57197815e-01 + 0.0j, - 2.53331304e-02 + 0.0j, - 9.82311963e-03 + 0.0j, - 1.06138957e-01 + 0.0j, - ], - [ - 6.25000000e-02 + 0.0j, - 6.25000000e-02 + 0.0j, - 1.19638348e-01 + 0.0j, - 3.12500000e-02 + 0.0j, - -2.02014565e-02 + 0.0j, - -2.68082618e-03 + 0.0j, - 2.23881674e-01 + 0.0j, - 1.37944174e-01 + 0.0j, - -3.78033966e-02 + 0.0j, - 1.58423239e-01 + 0.0j, - 1.34535646e-01 + 0.0j, - -5.49651086e-02 + 0.0j, - ], - [ - 1.40000000e-17 + 0.0j, - 1.25000000e-01 + 0.0j, - 6.25000000e-02 + 0.0j, - 1.25000000e-01 + 0.0j, - -4.41941738e-02 + 0.0j, - -1.59099026e-17 + 0.0j, - 1.37944174e-01 + 0.0j, - 2.50000000e-01 + 0.0j, - -2.10523539e-17 + 0.0j, - 1.15574269e-17 + 0.0j, - 9.75412607e-02 + 0.0j, - 5.71383476e-02 + 0.0j, - ], - [ - -1.41735435e-01 + 0.0j, - -8.45970869e-02 + 0.0j, - -5.14514565e-02 + 0.0j, - 5.14514565e-02 + 0.0j, - 1.49547935e-02 + 0.0j, - -1.57197815e-01 + 0.0j, - -3.78033966e-02 + 0.0j, - -2.10523539e-17 + 0.0j, - 1.95283753e-01 + 0.0j, - -3.82941440e-02 + 0.0j, - -6.11392595e-02 + 0.0j, - -4.51588288e-02 + 0.0j, - ], - [ - 3.12500000e-02 + 0.0j, - 7.54441738e-02 + 0.0j, - 6.89720869e-02 + 0.0j, - -6.89720869e-02 + 0.0j, - -2.24896848e-02 + 0.0j, - 2.53331304e-02 + 0.0j, - 1.58423239e-01 + 0.0j, - 1.15574269e-17 + 0.0j, - -3.82941440e-02 + 0.0j, - 2.17629701e-01 + 0.0j, - 1.32431810e-01 + 0.0j, - -1.91961467e-02 + 0.0j, - ], - [ - 1.00222087e-01 + 0.0j, - 1.48207521e-01 + 0.0j, - 1.04933262e-02 + 0.0j, - 7.81250000e-03 + 0.0j, - -1.42172278e-03 + 0.0j, - 9.82311963e-03 + 0.0j, - 1.34535646e-01 + 0.0j, - 9.75412607e-02 + 0.0j, - -6.11392595e-02 + 0.0j, - 1.32431810e-01 + 0.0j, - 1.81683746e-01 + 0.0j, - 7.28902444e-02 + 0.0j, - ], - [ - -3.12500000e-02 + 0.0j, - 2.00444174e-01 + 0.0j, - -6.89720869e-02 + 0.0j, - 1.94162607e-02 + 0.0j, - -1.23822206e-01 + 0.0j, - 1.06138957e-01 + 0.0j, - -5.49651086e-02 + 0.0j, - 5.71383476e-02 + 0.0j, - -4.51588288e-02 + 0.0j, - -1.91961467e-02 + 0.0j, - 7.28902444e-02 + 0.0j, - 2.38616353e-01 + 0.0j, - ], -] diff --git a/test/python/algorithms/time_evolvers/variational/variational_principles/expected_results/test_imaginary_mc_lachlan_variational_principle_expected3.py b/test/python/algorithms/time_evolvers/variational/variational_principles/expected_results/test_imaginary_mc_lachlan_variational_principle_expected3.py deleted file mode 100644 index 4790482e0db9..000000000000 --- a/test/python/algorithms/time_evolvers/variational/variational_principles/expected_results/test_imaginary_mc_lachlan_variational_principle_expected3.py +++ /dev/null @@ -1,182 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2023. -# -# 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. -"""Stores expected results that are lengthy.""" -expected_bound_metric_tensor_3 = [ - [ - -1.21000000e-34 + 0.00e00j, - 1.21000000e-34 + 2.50e-19j, - 1.76776695e-01 - 1.00e-18j, - -1.40000000e-17 + 0.00e00j, - -6.25000000e-02 + 0.00e00j, - 8.83883476e-02 - 1.25e-18j, - 1.69194174e-01 + 2.25e-18j, - 8.83883476e-02 - 2.50e-19j, - -7.27633476e-02 + 0.00e00j, - 9.75412607e-02 + 7.50e-19j, - 1.48398042e-02 - 1.75e-18j, - -9.75412607e-02 + 3.75e-18j, - ], - [ - 1.21000000e-34 + 2.50e-19j, - -1.21000000e-34 + 0.00e00j, - 1.10000000e-34 + 2.75e-18j, - 1.76776695e-01 - 2.25e-18j, - -6.25000000e-02 + 0.00e00j, - -8.83883476e-02 + 4.00e-18j, - 4.41941738e-02 - 1.25e-18j, - 1.76776695e-01 - 2.50e-19j, - 7.27633476e-02 - 7.50e-19j, - -9.75412607e-02 - 7.50e-19j, - 1.10485435e-02 - 7.50e-19j, - 2.74587393e-02 + 2.50e-19j, - ], - [ - 1.76776695e-01 - 1.00e-18j, - 1.10000000e-34 + 2.75e-18j, - -1.25000000e-01 + 0.00e00j, - -1.25000000e-01 + 0.00e00j, - -1.06694174e-01 + 1.25e-18j, - -6.25000000e-02 + 1.75e-18j, - -1.01332521e-01 + 7.50e-19j, - 4.67500000e-17 - 7.50e-19j, - 1.75206304e-02 + 5.00e-19j, - -8.57075215e-02 - 1.00e-18j, - -1.63277304e-01 + 1.00e-18j, - -1.56250000e-02 + 0.00e00j, - ], - [ - -1.40000000e-17 + 0.00e00j, - 1.76776695e-01 - 2.25e-18j, - -1.25000000e-01 + 0.00e00j, - -1.25000000e-01 + 0.00e00j, - 1.83058262e-02 - 1.50e-18j, - -1.50888348e-01 - 1.50e-18j, - -1.01332521e-01 + 2.50e-19j, - -8.83883476e-02 - 1.00e-18j, - -2.28822827e-02 - 1.00e-18j, - -1.16957521e-01 + 1.00e-18j, - -1.97208130e-01 + 0.00e00j, - -1.79457521e-01 + 1.25e-18j, - ], - [ - -6.25000000e-02 + 0.00e00j, - -6.25000000e-02 + 0.00e00j, - -1.06694174e-01 + 1.25e-18j, - 1.83058262e-02 - 1.50e-18j, - -1.56250000e-02 + 0.00e00j, - -2.20970869e-02 - 2.00e-18j, - 1.48992717e-01 - 1.00e-18j, - 2.60000000e-17 - 1.50e-18j, - -6.69614673e-02 - 5.00e-19j, - 2.00051576e-01 + 5.00e-19j, - 1.13640168e-01 + 1.25e-18j, - -4.83780325e-02 - 1.00e-18j, - ], - [ - 8.83883476e-02 - 1.25e-18j, - -8.83883476e-02 + 4.00e-18j, - -6.25000000e-02 + 1.75e-18j, - -1.50888348e-01 - 1.50e-18j, - -2.20970869e-02 - 2.00e-18j, - -3.12500000e-02 + 0.00e00j, - -2.85691738e-02 + 4.25e-18j, - 1.76776695e-01 + 0.00e00j, - 5.52427173e-03 + 1.00e-18j, - -1.29346478e-01 + 5.00e-19j, - -4.81004238e-02 + 4.25e-18j, - 5.27918696e-02 + 2.50e-19j, - ], - [ - 1.69194174e-01 + 2.25e-18j, - 4.41941738e-02 - 1.25e-18j, - -1.01332521e-01 + 7.50e-19j, - -1.01332521e-01 + 2.50e-19j, - 1.48992717e-01 - 1.00e-18j, - -2.85691738e-02 + 4.25e-18j, - -2.61183262e-02 + 0.00e00j, - -6.88900000e-33 + 0.00e00j, - 6.62099510e-02 - 1.00e-18j, - -2.90767610e-02 + 1.75e-18j, - -1.24942505e-01 + 0.00e00j, - -1.72430217e-02 + 2.50e-19j, - ], - [ - 8.83883476e-02 - 2.50e-19j, - 1.76776695e-01 - 2.50e-19j, - 4.67500000e-17 - 7.50e-19j, - -8.83883476e-02 - 1.00e-18j, - 2.60000000e-17 - 1.50e-18j, - 1.76776695e-01 + 0.00e00j, - -6.88900000e-33 + 0.00e00j, - -6.88900000e-33 + 0.00e00j, - 1.79457521e-01 - 1.75e-18j, - -5.33470869e-02 + 2.00e-18j, - -9.56456304e-02 + 3.00e-18j, - -1.32582521e-01 + 2.50e-19j, - ], - [ - -7.27633476e-02 + 0.00e00j, - 7.27633476e-02 - 7.50e-19j, - 1.75206304e-02 + 5.00e-19j, - -2.28822827e-02 - 1.00e-18j, - -6.69614673e-02 - 5.00e-19j, - 5.52427173e-03 + 1.00e-18j, - 6.62099510e-02 - 1.00e-18j, - 1.79457521e-01 - 1.75e-18j, - -5.47162473e-02 + 0.00e00j, - -4.20854047e-02 + 4.00e-18j, - -7.75494553e-02 - 2.50e-18j, - -2.49573723e-02 + 7.50e-19j, - ], - [ - 9.75412607e-02 + 7.50e-19j, - -9.75412607e-02 - 7.50e-19j, - -8.57075215e-02 - 1.00e-18j, - -1.16957521e-01 + 1.00e-18j, - 2.00051576e-01 + 5.00e-19j, - -1.29346478e-01 + 5.00e-19j, - -2.90767610e-02 + 1.75e-18j, - -5.33470869e-02 + 2.00e-18j, - -4.20854047e-02 + 4.00e-18j, - -3.23702991e-02 + 0.00e00j, - -4.70257118e-02 + 0.00e00j, - 1.22539288e-01 - 2.25e-18j, - ], - [ - 1.48398042e-02 - 1.75e-18j, - 1.10485435e-02 - 7.50e-19j, - -1.63277304e-01 + 1.00e-18j, - -1.97208130e-01 + 0.00e00j, - 1.13640168e-01 + 1.25e-18j, - -4.81004238e-02 + 4.25e-18j, - -1.24942505e-01 + 0.00e00j, - -9.56456304e-02 + 3.00e-18j, - -7.75494553e-02 - 2.50e-18j, - -4.70257118e-02 + 0.00e00j, - -6.83162540e-02 + 0.00e00j, - -2.78870598e-02 + 0.00e00j, - ], - [ - -9.75412607e-02 + 3.75e-18j, - 2.74587393e-02 + 2.50e-19j, - -1.56250000e-02 + 0.00e00j, - -1.79457521e-01 + 1.25e-18j, - -4.83780325e-02 - 1.00e-18j, - 5.27918696e-02 + 2.50e-19j, - -1.72430217e-02 + 2.50e-19j, - -1.32582521e-01 + 2.50e-19j, - -2.49573723e-02 + 7.50e-19j, - 1.22539288e-01 - 2.25e-18j, - -2.78870598e-02 + 0.00e00j, - -1.13836467e-02 + 0.00e00j, - ], -] diff --git a/test/python/algorithms/time_evolvers/variational/variational_principles/imaginary/__init__.py b/test/python/algorithms/time_evolvers/variational/variational_principles/imaginary/__init__.py deleted file mode 100644 index 26f7536d3514..000000000000 --- a/test/python/algorithms/time_evolvers/variational/variational_principles/imaginary/__init__.py +++ /dev/null @@ -1,11 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2023. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. diff --git a/test/python/algorithms/time_evolvers/variational/variational_principles/imaginary/test_imaginary_mc_lachlan_principle.py b/test/python/algorithms/time_evolvers/variational/variational_principles/imaginary/test_imaginary_mc_lachlan_principle.py deleted file mode 100644 index 8bb0f0d7c20d..000000000000 --- a/test/python/algorithms/time_evolvers/variational/variational_principles/imaginary/test_imaginary_mc_lachlan_principle.py +++ /dev/null @@ -1,115 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2023. -# -# 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 imaginary McLachlan's variational principle.""" - -import unittest - -# fmt: off -from test.python.algorithms.time_evolvers.variational.variational_principles.expected_results.\ - test_imaginary_mc_lachlan_variational_principle_expected1 import expected_bound_metric_tensor_1 -# fmt: on -from test.python.algorithms import QiskitAlgorithmsTestCase -import numpy as np - -from qiskit.quantum_info import SparsePauliOp -from qiskit.algorithms.time_evolvers.variational import ( - ImaginaryMcLachlanPrinciple, -) -from qiskit.circuit.library import EfficientSU2 -from qiskit.algorithms.gradients import LinCombEstimatorGradient, DerivativeType -from qiskit.primitives import Estimator - - -class TestImaginaryMcLachlanPrinciple(QiskitAlgorithmsTestCase): - """Test imaginary McLachlan's variational principle.""" - - def test_calc_metric_tensor(self): - """Test calculating a metric tensor.""" - observable = SparsePauliOp.from_list( - [ - ("II", 0.2252), - ("ZZ", 0.5716), - ("IZ", 0.3435), - ("ZI", -0.4347), - ("YY", 0.091), - ("XX", 0.091), - ] - ) - - d = 2 - ansatz = EfficientSU2(observable.num_qubits, reps=d) - - # Define a set of initial parameters - parameters = list(ansatz.parameters) - param_dict = {param: np.pi / 4 for param in parameters} - var_principle = ImaginaryMcLachlanPrinciple() - - bound_metric_tensor = var_principle.metric_tensor(ansatz, list(param_dict.values())) - - np.testing.assert_almost_equal(bound_metric_tensor, expected_bound_metric_tensor_1) - - def test_calc_calc_evolution_gradient(self): - """Test calculating evolution gradient.""" - observable = SparsePauliOp.from_list( - [ - ("II", 0.2252), - ("ZZ", 0.5716), - ("IZ", 0.3435), - ("ZI", -0.4347), - ("YY", 0.091), - ("XX", 0.091), - ] - ) - - d = 2 - ansatz = EfficientSU2(observable.num_qubits, reps=d) - - # Define a set of initial parameters - parameters = list(ansatz.parameters) - param_dict = {param: np.pi / 4 for param in parameters} - var_principle = ImaginaryMcLachlanPrinciple() - - bound_evolution_gradient = var_principle.evolution_gradient( - observable, ansatz, list(param_dict.values()), parameters - ) - - expected_evolution_gradient = [ - (0.19308934095957098 - 1.4e-17j), - (0.007027674650099142 - 0j), - (0.03192524520091862 - 0j), - (-0.06810314606309673 - 1e-18j), - (0.07590371669521798 - 7e-18j), - (0.11891968269385343 + 1.5e-18j), - (-0.0012030273438232639 + 0j), - (-0.049885258804562266 + 1.8500000000000002e-17j), - (-0.20178860797540302 - 5e-19j), - (-0.0052269232310933195 + 1e-18j), - (0.022892905637005266 - 3e-18j), - (-0.022892905637005294 + 3.5e-18j), - ] - - np.testing.assert_almost_equal(bound_evolution_gradient, expected_evolution_gradient) - - def test_gradient_setting(self): - """Test reactions to wrong gradient settings..""" - estimator = Estimator() - gradient = LinCombEstimatorGradient(estimator, derivative_type=DerivativeType.IMAG) - - with self.assertWarns(Warning): - var_principle = ImaginaryMcLachlanPrinciple(gradient=gradient) - - np.testing.assert_equal(var_principle.gradient._derivative_type, DerivativeType.REAL) - - -if __name__ == "__main__": - unittest.main() diff --git a/test/python/algorithms/time_evolvers/variational/variational_principles/real/__init__.py b/test/python/algorithms/time_evolvers/variational/variational_principles/real/__init__.py deleted file mode 100644 index 26f7536d3514..000000000000 --- a/test/python/algorithms/time_evolvers/variational/variational_principles/real/__init__.py +++ /dev/null @@ -1,11 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2023. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. diff --git a/test/python/algorithms/time_evolvers/variational/variational_principles/real/test_real_mc_lachlan_principle.py b/test/python/algorithms/time_evolvers/variational/variational_principles/real/test_real_mc_lachlan_principle.py deleted file mode 100644 index 5a314979d5de..000000000000 --- a/test/python/algorithms/time_evolvers/variational/variational_principles/real/test_real_mc_lachlan_principle.py +++ /dev/null @@ -1,120 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2023. -# -# 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 real McLachlan's variational principle.""" - -import unittest - -from test.python.algorithms import QiskitAlgorithmsTestCase - -# fmt: off -from test.python.algorithms.time_evolvers.variational.variational_principles.expected_results.\ - test_imaginary_mc_lachlan_variational_principle_expected2 import expected_bound_metric_tensor_2 -# fmt: on -import numpy as np - -from qiskit.quantum_info import SparsePauliOp -from qiskit.algorithms.time_evolvers.variational import ( - RealMcLachlanPrinciple, -) -from qiskit.circuit.library import EfficientSU2 -from qiskit.algorithms.gradients import LinCombEstimatorGradient, DerivativeType -from qiskit.primitives import Estimator - - -class TestRealMcLachlanPrinciple(QiskitAlgorithmsTestCase): - """Test real McLachlan's variational principle.""" - - def test_calc_calc_metric_tensor(self): - """Test calculating a metric tensor.""" - observable = SparsePauliOp.from_list( - [ - ("II", 0.2252), - ("ZZ", 0.5716), - ("IZ", 0.3435), - ("ZI", -0.4347), - ("YY", 0.091), - ("XX", 0.091), - ] - ) - - d = 2 - ansatz = EfficientSU2(observable.num_qubits, reps=d) - - # Define a set of initial parameters - parameters = list(ansatz.parameters) - param_dict = {param: np.pi / 4 for param in parameters} - var_principle = RealMcLachlanPrinciple() - - bound_metric_tensor = var_principle.metric_tensor(ansatz, list(param_dict.values())) - - np.testing.assert_almost_equal( - bound_metric_tensor, expected_bound_metric_tensor_2, decimal=5 - ) - - def test_calc_evolution_gradient(self): - """Test calculating evolution gradient.""" - observable = SparsePauliOp.from_list( - [ - ("II", 0.2252), - ("ZZ", 0.5716), - ("IZ", 0.3435), - ("ZI", -0.4347), - ("YY", 0.091), - ("XX", 0.091), - ] - ) - - d = 2 - ansatz = EfficientSU2(observable.num_qubits, reps=d) - - # Define a set of initial parameters - parameters = list(ansatz.parameters) - param_dict = {param: np.pi / 4 for param in parameters} - var_principle = RealMcLachlanPrinciple() - - bound_evolution_gradient = var_principle.evolution_gradient( - observable, ansatz, list(param_dict.values()), parameters - ) - - expected_evolution_gradient = [ - (-0.04514911474522546 + 4e-18j), - (0.0963123928027075 - 1.5e-18j), - (0.1365347823673539 - 7e-18j), - (0.004969316401057883 - 4.9999999999999996e-18j), - (-0.003843833929692342 - 4.999999999999998e-19j), - (0.07036988622493834 - 7e-18j), - (0.16560609099860682 - 3.5e-18j), - (0.16674183768051887 + 1e-18j), - (-0.03843296670360974 - 6e-18j), - (0.08891074158680243 - 6e-18j), - (0.06425681697616654 + 7e-18j), - (-0.03172376682078948 - 7e-18j), - ] - - np.testing.assert_almost_equal( - bound_evolution_gradient, expected_evolution_gradient, decimal=5 - ) - - def test_gradient_setting(self): - """Test reactions to wrong gradient settings..""" - estimator = Estimator() - gradient = LinCombEstimatorGradient(estimator, derivative_type=DerivativeType.REAL) - - with self.assertWarns(Warning): - var_principle = RealMcLachlanPrinciple(gradient=gradient) - - np.testing.assert_equal(var_principle.gradient._derivative_type, DerivativeType.IMAG) - - -if __name__ == "__main__": - unittest.main() diff --git a/test/python/algorithms/utils/__init__.py b/test/python/algorithms/utils/__init__.py deleted file mode 100644 index fdb172d367f0..000000000000 --- a/test/python/algorithms/utils/__init__.py +++ /dev/null @@ -1,11 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2022. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. diff --git a/test/python/algorithms/utils/test_validate_bounds.py b/test/python/algorithms/utils/test_validate_bounds.py deleted file mode 100644 index e4cc42ad154f..000000000000 --- a/test/python/algorithms/utils/test_validate_bounds.py +++ /dev/null @@ -1,57 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2022. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. - -"""Test validate bounds.""" - -import warnings - -from test.python.algorithms import QiskitAlgorithmsTestCase - -from unittest.mock import Mock - -import numpy as np - -from qiskit.algorithms.utils import validate_bounds -from qiskit.utils import algorithm_globals - - -class TestValidateBounds(QiskitAlgorithmsTestCase): - """Test the ``validate_bounds`` utility function.""" - - def setUp(self): - super().setUp() - with warnings.catch_warnings(): - warnings.filterwarnings("ignore", category=DeprecationWarning) - algorithm_globals.random_seed = 0 - self.bounds = [(-np.pi / 2, np.pi / 2)] - self.ansatz = Mock() - - def test_with_no_ansatz_bounds(self): - """Test with no ansatz bounds.""" - self.ansatz.num_parameters = 1 - self.ansatz.parameter_bounds = None - bounds = validate_bounds(self.ansatz) - self.assertEqual(bounds, [(None, None)]) - - def test_with_ansatz_bounds(self): - """Test with ansatz bounds.""" - self.ansatz.num_parameters = 1 - self.ansatz.parameter_bounds = self.bounds - bounds = validate_bounds(self.ansatz) - self.assertEqual(bounds, self.bounds) - - def test_with_mismatched_num_params(self): - """Test with a mismatched number of parameters and bounds""" - self.ansatz.num_parameters = 2 - self.ansatz.parameter_bounds = self.bounds - with self.assertRaises(ValueError): - _ = validate_bounds(self.ansatz) diff --git a/test/python/algorithms/utils/test_validate_initial_point.py b/test/python/algorithms/utils/test_validate_initial_point.py deleted file mode 100644 index 28854b485fee..000000000000 --- a/test/python/algorithms/utils/test_validate_initial_point.py +++ /dev/null @@ -1,54 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2022. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. - -"""Test validate initial point.""" - -import warnings - -from test.python.algorithms import QiskitAlgorithmsTestCase - -from unittest.mock import Mock - -import numpy as np - -from qiskit.algorithms.utils import validate_initial_point -from qiskit.utils import algorithm_globals - - -class TestValidateInitialPoint(QiskitAlgorithmsTestCase): - """Test the ``validate_initial_point`` utility function.""" - - def setUp(self): - super().setUp() - with warnings.catch_warnings(): - warnings.filterwarnings("ignore", category=DeprecationWarning) - algorithm_globals.random_seed = 0 - self.ansatz = Mock() - self.ansatz.num_parameters = 1 - - def test_with_no_initial_point_or_bounds(self): - """Test with no user-defined initial point and no ansatz bounds.""" - self.ansatz.parameter_bounds = None - initial_point = validate_initial_point(None, self.ansatz) - np.testing.assert_array_almost_equal(initial_point, [1.721111]) - - def test_with_no_initial_point(self): - """Test with no user-defined initial point with ansatz bounds.""" - self.ansatz.parameter_bounds = [(-np.pi / 2, np.pi / 2)] - initial_point = validate_initial_point(None, self.ansatz) - np.testing.assert_array_almost_equal(initial_point, [0.430278]) - - def test_with_mismatched_params(self): - """Test with mistmatched parameters and bounds..""" - self.ansatz.parameter_bounds = None - with self.assertRaises(ValueError): - _ = validate_initial_point([1.0, 2.0], self.ansatz) diff --git a/test/python/opflow/test_gradients.py b/test/python/opflow/test_gradients.py index b0fee1233bf5..fc82475313a0 100644 --- a/test/python/opflow/test_gradients.py +++ b/test/python/opflow/test_gradients.py @@ -21,12 +21,10 @@ from ddt import ddt, data, idata, unpack from qiskit import QuantumCircuit, QuantumRegister, BasicAer -from qiskit.test import slow_test from qiskit.utils import QuantumInstance from qiskit.exceptions import MissingOptionalLibraryError from qiskit.utils import algorithm_globals -from qiskit.algorithms import VQE -from qiskit.algorithms.optimizers import CG + from qiskit.opflow import ( I, X, @@ -1166,51 +1164,6 @@ def test_gradient_wrapper2(self, backend_type, atol): result = grad(value) self.assertTrue(np.allclose(result, correct_values[i], atol=atol)) - @slow_test - def test_vqe(self): - """Test VQE with gradients""" - - method = "lin_comb" - backend = "qasm_simulator" - - with self.assertWarns(DeprecationWarning): - q_instance = QuantumInstance( - BasicAer.get_backend(backend), seed_simulator=79, seed_transpiler=2 - ) - - # Define the Hamiltonian - h2_hamiltonian = ( - -1.05 * (I ^ I) + 0.39 * (I ^ Z) - 0.39 * (Z ^ I) - 0.01 * (Z ^ Z) + 0.18 * (X ^ X) - ) - h2_energy = -1.85727503 - - # Define the Ansatz - wavefunction = QuantumCircuit(2) - params = ParameterVector("theta", length=8) - itr = iter(params) - wavefunction.ry(next(itr), 0) - wavefunction.ry(next(itr), 1) - wavefunction.rz(next(itr), 0) - wavefunction.rz(next(itr), 1) - wavefunction.cx(0, 1) - wavefunction.ry(next(itr), 0) - wavefunction.ry(next(itr), 1) - wavefunction.rz(next(itr), 0) - wavefunction.rz(next(itr), 1) - - # Conjugate Gradient algorithm - optimizer = CG(maxiter=10) - - grad = Gradient(grad_method=method) - - # Gradient callable - with self.assertWarns(DeprecationWarning): - vqe = VQE( - ansatz=wavefunction, optimizer=optimizer, gradient=grad, quantum_instance=q_instance - ) - result = vqe.compute_minimum_eigenvalue(operator=h2_hamiltonian) - np.testing.assert_almost_equal(result.optimal_value, h2_energy, decimal=0) - def test_qfi_overlap_works_with_bound_parameters(self): """Test all QFI methods work if the circuit contains a gate with bound parameters.""" diff --git a/test/python/transpiler/aqc/test_aqc.py b/test/python/transpiler/aqc/test_aqc.py index 885fe4c1fc13..c54de8c21dc7 100644 --- a/test/python/transpiler/aqc/test_aqc.py +++ b/test/python/transpiler/aqc/test_aqc.py @@ -21,7 +21,6 @@ import numpy as np from scipy.optimize import minimize -from qiskit.algorithms.optimizers import L_BFGS_B from qiskit.quantum_info import Operator from qiskit.test import QiskitTestCase from qiskit.transpiler.synthesis.aqc.aqc import AQC @@ -67,15 +66,6 @@ def test_aqc(self, uses_default): error = 0.5 * (np.linalg.norm(approx_matrix - ORIGINAL_CIRCUIT, "fro") ** 2) self.assertLess(error, 1e-3) - def test_aqc_deprecation(self): - """Tests that AQC raises deprecation warning.""" - - seed = 12345 - optimizer = L_BFGS_B(maxiter=200) - - with self.assertRaises(DeprecationWarning): - _ = AQC(optimizer=optimizer, seed=seed) - def test_aqc_fastgrad(self): """ Tests AQC on a MCX circuit/matrix with random initial guess using