From e3233b7b6966ebc66414712cfc4b5d84db9bfc28 Mon Sep 17 00:00:00 2001 From: Dariusz Lasecki Date: Tue, 30 Aug 2022 13:21:25 +0200 Subject: [PATCH 01/48] Implemented observables_evaluator.py with primitives. --- qiskit/algorithms/observables_evaluator.py | 177 ++++++++++++++++++ .../algorithms/test_observables_evaluator.py | 105 +++++++++++ 2 files changed, 282 insertions(+) create mode 100644 qiskit/algorithms/observables_evaluator.py create mode 100644 test/python/algorithms/test_observables_evaluator.py diff --git a/qiskit/algorithms/observables_evaluator.py b/qiskit/algorithms/observables_evaluator.py new file mode 100644 index 000000000000..3ed9c6757f91 --- /dev/null +++ b/qiskit/algorithms/observables_evaluator.py @@ -0,0 +1,177 @@ +# 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 typing import Tuple, Union, List, Iterable, Sequence, Optional + +import numpy as np + +from qiskit import QuantumCircuit +from qiskit.opflow import ( + CircuitSampler, + ListOp, + StateFn, + OperatorBase, + ExpectationBase, + PauliSumOp, +) +from qiskit.providers import Backend +from qiskit.quantum_info import Statevector +from qiskit.utils import QuantumInstance + +from .list_or_dict import ListOrDict +from ..primitives import Estimator, EstimatorResult +from ..quantum_info.operators.base_operator import BaseOperator + + +def eval_observables( + estimator: Estimator, + quantum_state: Sequence[QuantumCircuit], + observables: Sequence[Union[BaseOperator, PauliSumOp]], + threshold: float = 1e-12, +) -> ListOrDict[Tuple[complex, complex]]: + """ + 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. + + Args: + estimator: An estimator primitive used for calculations. + quantum_state: An unparametrized quantum circuit representing a quantum state that + expectation values are computed against. + observables: A sequence of operators whose expectation values are to be + calculated. + threshold: A threshold value that defines which mean values should be neglected (helpful for + ignoring numerical instabilities close to 0). + + Returns: + A list or a dictionary of tuples (mean, 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." + ) + + # if type(observables) != Sequence: + # observables = [observables] + # if type(quantum_state) != Sequence: + # quantum_state = [quantum_state] + estimator_job = estimator.run(quantum_state, observables) + expectation_values = estimator_job.result().values + + # compute standard deviations + std_devs = _compute_std_devs(estimator_job, len(expectation_values)) + + # Discard values below threshold + observables_means = expectation_values * (np.abs(expectation_values) > threshold) + # zip means and standard deviations into tuples + observables_results = list(zip(observables_means, 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: Union[ + 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 = [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( + estimator_result: EstimatorResult, + results_length: int, +) -> List[Optional[complex]]: + """ + Calculates a list of standard deviations from expectation values of observables provided. + + Args: + estimator_result: An estimator result. + results_length: Number of expectation values calculated. + + Returns: + A list of standard deviations. + """ + if not estimator_result.metadata: + return [0] * results_length + + std_devs = [] + for metadata in estimator_result.metadata: + if metadata and "variance" in metadata.keys() and "shots" in metadata.keys(): + variance = metadata["variance"] + shots = metadata["shots"] + std_devs.append(np.sqrt(variance / shots)) + else: + std_devs.append(0) + + return std_devs diff --git a/test/python/algorithms/test_observables_evaluator.py b/test/python/algorithms/test_observables_evaluator.py new file mode 100644 index 000000000000..2fa4e7e4c913 --- /dev/null +++ b/test/python/algorithms/test_observables_evaluator.py @@ -0,0 +1,105 @@ +# 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 evaluator of auxiliary operators for algorithms.""" + +import unittest +from typing import Tuple, Sequence, List + +import numpy as np +from ddt import ddt, data + +from qiskit.algorithms.observables_evaluator import eval_observables +from qiskit.primitives import Estimator +from test.python.algorithms import QiskitAlgorithmsTestCase +from qiskit.algorithms.list_or_dict import ListOrDict +from qiskit.quantum_info import Statevector +from qiskit import QuantumCircuit +from qiskit.circuit.library import EfficientSU2 +from qiskit.opflow import ( + PauliSumOp, + X, + Z, + I, + OperatorBase, +) +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 + algorithm_globals.random_seed = self.seed + + self.threshold = 1e-8 + + def get_exact_expectation(self, ansatz: QuantumCircuit, observables: Sequence[OperatorBase]): + """ + Calculates the exact expectation to be used as an expected result for unit tests. + """ + + # 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 + ] + + return exact + + def _run_test( + self, + expected_result: List[Tuple[complex, complex]], + quantum_state: Sequence[QuantumCircuit], + decimal: int, + observables: Sequence[OperatorBase], + estimator: Estimator, + ): + result = eval_observables(estimator, quantum_state, observables, self.threshold) + + 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)]), + ], + ) + def test_eval_observables(self, observables: Sequence[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.bind_parameters(parameters) + states = [bound_ansatz] * len(observables) + expected_result = self.get_exact_expectation(bound_ansatz, observables) + estimator = Estimator() + decimal = 6 + self._run_test( + expected_result, + states, + decimal, + observables, + estimator, + ) + + +if __name__ == "__main__": + unittest.main() From d71f50b52b3993eb116fd9af99325f8b7dc89292 Mon Sep 17 00:00:00 2001 From: Dariusz Lasecki Date: Tue, 30 Aug 2022 13:23:27 +0200 Subject: [PATCH 02/48] Added evolvers problems and interfaces to time_evolvers package. --- qiskit/algorithms/time_evolvers/__init__.py | 11 ++ .../time_evolvers/evolution_problem.py | 109 ++++++++++++++++ .../time_evolvers/evolution_result.py | 40 ++++++ .../time_evolvers/imaginary_evolver.py | 37 ++++++ .../algorithms/time_evolvers/real_evolver.py | 37 ++++++ .../algorithms/test_observables_evaluator.py | 4 - .../algorithms/time_evolvers/__init__.py | 11 ++ .../time_evolvers/test_evolution_problem.py | 116 ++++++++++++++++++ .../time_evolvers/test_evolution_result.py | 48 ++++++++ 9 files changed, 409 insertions(+), 4 deletions(-) create mode 100644 qiskit/algorithms/time_evolvers/__init__.py create mode 100644 qiskit/algorithms/time_evolvers/evolution_problem.py create mode 100644 qiskit/algorithms/time_evolvers/evolution_result.py create mode 100644 qiskit/algorithms/time_evolvers/imaginary_evolver.py create mode 100644 qiskit/algorithms/time_evolvers/real_evolver.py create mode 100644 test/python/algorithms/time_evolvers/__init__.py create mode 100644 test/python/algorithms/time_evolvers/test_evolution_problem.py create mode 100644 test/python/algorithms/time_evolvers/test_evolution_result.py diff --git a/qiskit/algorithms/time_evolvers/__init__.py b/qiskit/algorithms/time_evolvers/__init__.py new file mode 100644 index 000000000000..fdb172d367f0 --- /dev/null +++ b/qiskit/algorithms/time_evolvers/__init__.py @@ -0,0 +1,11 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2022. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. diff --git a/qiskit/algorithms/time_evolvers/evolution_problem.py b/qiskit/algorithms/time_evolvers/evolution_problem.py new file mode 100644 index 000000000000..175beecd6bf6 --- /dev/null +++ b/qiskit/algorithms/time_evolvers/evolution_problem.py @@ -0,0 +1,109 @@ +# 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 typing import Union, Optional, Dict + +from qiskit import QuantumCircuit +from qiskit.circuit import Parameter +from qiskit.opflow import OperatorBase, StateFn +from ..list_or_dict import ListOrDict + + +class EvolutionProblem: + """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. + """ + + def __init__( + self, + hamiltonian: OperatorBase, + time: float, + initial_state: Optional[Union[StateFn, QuantumCircuit]] = None, + aux_operators: Optional[ListOrDict[OperatorBase]] = None, + truncation_threshold: float = 1e-12, + t_param: Optional[Parameter] = None, + param_value_dict: Optional[Dict[Parameter, complex]] = 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() + 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/time_evolvers/evolution_result.py b/qiskit/algorithms/time_evolvers/evolution_result.py new file mode 100644 index 000000000000..1dd91d705d28 --- /dev/null +++ b/qiskit/algorithms/time_evolvers/evolution_result.py @@ -0,0 +1,40 @@ +# 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 typing import Optional, Union, Tuple + +from qiskit import QuantumCircuit +from qiskit.algorithms.list_or_dict import ListOrDict +from qiskit.opflow import StateFn, OperatorBase +from ..algorithm_result import AlgorithmResult + + +class EvolutionResult(AlgorithmResult): + """Class for holding evolution result.""" + + def __init__( + self, + evolved_state: Union[StateFn, QuantumCircuit, OperatorBase], + aux_ops_evaluated: Optional[ListOrDict[Tuple[complex, complex]]] = 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/time_evolvers/imaginary_evolver.py b/qiskit/algorithms/time_evolvers/imaginary_evolver.py new file mode 100644 index 000000000000..309bb73b08af --- /dev/null +++ b/qiskit/algorithms/time_evolvers/imaginary_evolver.py @@ -0,0 +1,37 @@ +# 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 .evolution_problem import EvolutionProblem +from .evolution_result import EvolutionResult + + +class ImaginaryEvolver(ABC): + """Interface for Quantum Imaginary Time Evolution.""" + + @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/time_evolvers/real_evolver.py b/qiskit/algorithms/time_evolvers/real_evolver.py new file mode 100644 index 000000000000..6107facfe542 --- /dev/null +++ b/qiskit/algorithms/time_evolvers/real_evolver.py @@ -0,0 +1,37 @@ +# 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 .evolution_problem import EvolutionProblem +from .evolution_result import EvolutionResult + + +class RealEvolver(ABC): + """Interface for Quantum Real Time Evolution.""" + + @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/test/python/algorithms/test_observables_evaluator.py b/test/python/algorithms/test_observables_evaluator.py index 2fa4e7e4c913..ea9b3560746f 100644 --- a/test/python/algorithms/test_observables_evaluator.py +++ b/test/python/algorithms/test_observables_evaluator.py @@ -20,15 +20,11 @@ from qiskit.algorithms.observables_evaluator import eval_observables from qiskit.primitives import Estimator from test.python.algorithms import QiskitAlgorithmsTestCase -from qiskit.algorithms.list_or_dict import ListOrDict from qiskit.quantum_info import Statevector from qiskit import QuantumCircuit from qiskit.circuit.library import EfficientSU2 from qiskit.opflow import ( PauliSumOp, - X, - Z, - I, OperatorBase, ) from qiskit.utils import algorithm_globals diff --git a/test/python/algorithms/time_evolvers/__init__.py b/test/python/algorithms/time_evolvers/__init__.py new file mode 100644 index 000000000000..fdb172d367f0 --- /dev/null +++ b/test/python/algorithms/time_evolvers/__init__.py @@ -0,0 +1,11 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2022. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. diff --git a/test/python/algorithms/time_evolvers/test_evolution_problem.py b/test/python/algorithms/time_evolvers/test_evolution_problem.py new file mode 100644 index 000000000000..d3dd2f7bc9b2 --- /dev/null +++ b/test/python/algorithms/time_evolvers/test_evolution_problem.py @@ -0,0 +1,116 @@ +# 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 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 + + 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") + hamiltonian = t_parameter * Z + Y + time = 2 + initial_state = One + aux_operators = [X, Y] + param_value_dict = {t_parameter: 3.2} + + 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} + + 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 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."): + hamiltonian = param_x * X + param_y * Y + param_dict = {param_y: 2} + 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."): + hamiltonian = param_x * X + param_y * Y + param_dict = {} + 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."): + hamiltonian = param_x * X + param_y * Y + param_dict = {param_y: 2, param_x: 1, Parameter("z"): 1} + 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/time_evolvers/test_evolution_result.py b/test/python/algorithms/time_evolvers/test_evolution_result.py new file mode 100644 index 000000000000..5500b283a1cb --- /dev/null +++ b/test/python/algorithms/time_evolvers/test_evolution_result.py @@ -0,0 +1,48 @@ +# 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 + 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)] + 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() From 7a7c82be8f91653f7c857651d09491968c5b3213 Mon Sep 17 00:00:00 2001 From: Dariusz Lasecki Date: Tue, 30 Aug 2022 15:05:55 +0200 Subject: [PATCH 03/48] Mostly updated trotter_qrte.py to use primitives. --- .../time_evolvers/trotterization/__init__.py | 21 ++ .../trotterization/trotter_qrte.py | 183 +++++++++++++++ .../time_evolvers/trotterization/__init__.py | 11 + .../trotterization/test_trotter_qrte.py | 213 ++++++++++++++++++ 4 files changed, 428 insertions(+) create mode 100644 qiskit/algorithms/time_evolvers/trotterization/__init__.py create mode 100644 qiskit/algorithms/time_evolvers/trotterization/trotter_qrte.py create mode 100644 test/python/algorithms/time_evolvers/trotterization/__init__.py create mode 100644 test/python/algorithms/time_evolvers/trotterization/test_trotter_qrte.py diff --git a/qiskit/algorithms/time_evolvers/trotterization/__init__.py b/qiskit/algorithms/time_evolvers/trotterization/__init__.py new file mode 100644 index 000000000000..fe1b8d8aedf2 --- /dev/null +++ b/qiskit/algorithms/time_evolvers/trotterization/__init__.py @@ -0,0 +1,21 @@ +# 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/time_evolvers/trotterization/trotter_qrte.py b/qiskit/algorithms/time_evolvers/trotterization/trotter_qrte.py new file mode 100644 index 000000000000..010dd0b618c8 --- /dev/null +++ b/qiskit/algorithms/time_evolvers/trotterization/trotter_qrte.py @@ -0,0 +1,183 @@ +# 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 typing import Union, Optional + +from qiskit import QuantumCircuit +from qiskit.algorithms.evolvers import EvolutionProblem, EvolutionResult +from qiskit.algorithms.evolvers.real_evolver import RealEvolver +from qiskit.algorithms.observables_evaluator import eval_observables +from qiskit.opflow import ( + SummedOp, + PauliOp, + CircuitOp, + ExpectationBase, + CircuitSampler, + PauliSumOp, + StateFn, + OperatorBase, +) +from qiskit.circuit.library import PauliEvolutionGate +from qiskit.primitives import Estimator +from qiskit.providers import Backend +from qiskit.synthesis import ProductFormula, LieTrotter +from qiskit.utils import QuantumInstance + + +class TrotterQRTE(RealEvolver): + """Quantum Real Time Evolution using Trotterization. + Type of Trotterization is defined by a ProductFormula provided. + + Examples: + + .. jupyter-execute:: + + from qiskit.opflow import X, Z, Zero + from qiskit.algorithms import EvolutionProblem, TrotterQRTE + + operator = X + Z + initial_state = Zero + time = 1 + evolution_problem = EvolutionProblem(operator, 1, initial_state) + # LieTrotter with 1 rep + trotter_qrte = TrotterQRTE() + evolved_state = trotter_qrte.evolve(evolution_problem).evolved_state + """ + + def __init__( + self, + product_formula: Optional[ProductFormula] = None, + estimator: Estimator = 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. + estimator: An estimator primitive used for calculating expectation values of + EvolutionProblem.aux_operators. + """ + if product_formula is None: + product_formula = LieTrotter() + self.product_formula = product_formula + self.estimator = estimator + + @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 an estimator primitive 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 an estimator primitive. + + 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.estimator is None + ): + raise ValueError( + "aux_operators were provided for evaluations but no ``estimator`` 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.bind_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.estimator, + evolved_state.primitive, + evolution_problem.aux_operators, + evolution_problem.truncation_threshold, + ) + + return EvolutionResult(evolved_state, evaluated_aux_ops) + + @staticmethod + def _summed_op_to_pauli_sum_op( + hamiltonian: SummedOp, + ) -> Union[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/test/python/algorithms/time_evolvers/trotterization/__init__.py b/test/python/algorithms/time_evolvers/trotterization/__init__.py new file mode 100644 index 000000000000..96c0cf22bec9 --- /dev/null +++ b/test/python/algorithms/time_evolvers/trotterization/__init__.py @@ -0,0 +1,11 @@ +# 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/time_evolvers/trotterization/test_trotter_qrte.py b/test/python/algorithms/time_evolvers/trotterization/test_trotter_qrte.py new file mode 100644 index 000000000000..75d30142c049 --- /dev/null +++ b/test/python/algorithms/time_evolvers/trotterization/test_trotter_qrte.py @@ -0,0 +1,213 @@ +# 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. + +""" Test TrotterQRTE. """ + +import unittest + +from qiskit.algorithms.time_evolvers.evolution_problem import EvolutionProblem +from qiskit.algorithms.time_evolvers.trotterization.trotter_qrte import TrotterQRTE +from qiskit.primitives import Estimator +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.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 + algorithm_globals.random_seed = self.seed + + @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 + 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 + evolution_problem = EvolutionProblem(operator, time, initial_state, aux_ops) + estimator = Estimator() + + 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), + ] + + algorithm_globals.random_seed = 0 + trotter_qrte = TrotterQRTE(estimator=estimator) + evolution_result = trotter_qrte.evolve(evolution_problem) + + np.testing.assert_equal( + evolution_result.evolved_state.eval(), expected_evolved_state + ) + + 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]) + 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 + evolution_problem = EvolutionProblem( + operator, time, initial_state, param_value_dict=params_dict + ) + expected_state = VectorStateFn( + Statevector([-0.9899925 - 0.14112001j, 0.0 + 0.0j, 0.0 + 0.0j, 0.0 + 0.0j], dims=(2, 2)) + ) + 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 + evolution_problem = EvolutionProblem(operator, time, initial_state) + + algorithm_globals.random_seed = 0 + trotter_qrte = TrotterQRTE(product_formula=QDrift()) + 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 + algorithm_globals.random_seed = 0 + trotter_qrte = TrotterQRTE() + with assert_raises(ValueError): + evolution_problem = EvolutionProblem( + operator, + time, + initial_state, + t_param=t_param, + param_value_dict=param_value_dict, + ) + _ = trotter_qrte.evolve(evolution_problem) + + +if __name__ == "__main__": + unittest.main() From 54afe2519fc1b435a33a0ffe32ecf7c00a715c7b Mon Sep 17 00:00:00 2001 From: Dariusz Lasecki Date: Thu, 1 Sep 2022 14:14:15 +0200 Subject: [PATCH 04/48] Added observables_evaluator.py that uses primitives. --- qiskit/algorithms/observables_evaluator.py | 81 ++++--------------- .../algorithms/test_observables_evaluator.py | 3 +- 2 files changed, 18 insertions(+), 66 deletions(-) diff --git a/qiskit/algorithms/observables_evaluator.py b/qiskit/algorithms/observables_evaluator.py index 3ed9c6757f91..dba83b308940 100644 --- a/qiskit/algorithms/observables_evaluator.py +++ b/qiskit/algorithms/observables_evaluator.py @@ -11,36 +11,26 @@ # that they have been altered from the originals. """Evaluator of auxiliary operators for algorithms.""" -from typing import Tuple, Union, List, Iterable, Sequence, Optional +from typing import Tuple, Union, List, Sequence, Optional import numpy as np from qiskit import QuantumCircuit from qiskit.opflow import ( - CircuitSampler, - ListOp, - StateFn, - OperatorBase, - ExpectationBase, PauliSumOp, ) -from qiskit.providers import Backend -from qiskit.quantum_info import Statevector -from qiskit.utils import QuantumInstance - -from .list_or_dict import ListOrDict from ..primitives import Estimator, EstimatorResult from ..quantum_info.operators.base_operator import BaseOperator def eval_observables( estimator: Estimator, - quantum_state: Sequence[QuantumCircuit], + quantum_state: QuantumCircuit, observables: Sequence[Union[BaseOperator, PauliSumOp]], threshold: float = 1e-12, -) -> ListOrDict[Tuple[complex, complex]]: +) -> List[Tuple[complex, complex]]: """ - Accepts a list or a dictionary of operators and calculates their expectation values - means + Accepts a sequence of operators and calculates their expectation values - means and standard deviations. They are calculated with respect to a quantum state provided. A user can optionally provide a threshold value which filters mean values falling below the threshold. @@ -48,22 +38,19 @@ def eval_observables( estimator: An estimator primitive used for calculations. quantum_state: An unparametrized quantum circuit representing a quantum state that expectation values are computed against. - observables: A sequence of operators whose expectation values are to be - calculated. + observables: A sequence of operators whose expectation values are to be calculated. threshold: A threshold value that defines which mean values should be neglected (helpful for ignoring numerical instabilities close to 0). Returns: - A list or a dictionary of tuples (mean, standard deviation). + A list 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 + isinstance(quantum_state, QuantumCircuit) # Statevector cannot be parametrized and len(quantum_state.parameters) > 0 ): raise ValueError( @@ -71,10 +58,7 @@ def eval_observables( "allowed - it cannot have free parameters." ) - # if type(observables) != Sequence: - # observables = [observables] - # if type(quantum_state) != Sequence: - # quantum_state = [quantum_state] + quantum_state = [quantum_state] * len(observables) estimator_job = estimator.run(quantum_state, observables) expectation_values = estimator_job.result().values @@ -87,61 +71,30 @@ def eval_observables( 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: Union[ - 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]]: + observables: Sequence[BaseOperator], +) -> List[Tuple[complex, complex]]: """ - Prepares a list or a dictionary of eigenvalues from ``observables_results`` and + Prepares a list of eigenvalues and standard deviations 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 + observables: A list of operators whose expectation values are to be calculated. Returns: - A list or a dictionary of tuples (mean, standard deviation). + A list of tuples (mean, standard deviation). """ - if isinstance(observables, list): - observables_eigenvalues = [None] * len(observables) - key_value_iterator = enumerate(observables_results) - else: - observables_eigenvalues = {} - key_value_iterator = zip(observables.keys(), observables_results) + + observables_eigenvalues = [None] * len(observables) + key_value_iterator = enumerate(observables_results) + for key, value in key_value_iterator: if observables[key] is not None: observables_eigenvalues[key] = value diff --git a/test/python/algorithms/test_observables_evaluator.py b/test/python/algorithms/test_observables_evaluator.py index ea9b3560746f..88e256084fea 100644 --- a/test/python/algorithms/test_observables_evaluator.py +++ b/test/python/algorithms/test_observables_evaluator.py @@ -13,13 +13,12 @@ import unittest from typing import Tuple, Sequence, List - +from test.python.algorithms import QiskitAlgorithmsTestCase import numpy as np from ddt import ddt, data from qiskit.algorithms.observables_evaluator import eval_observables from qiskit.primitives import Estimator -from test.python.algorithms import QiskitAlgorithmsTestCase from qiskit.quantum_info import Statevector from qiskit import QuantumCircuit from qiskit.circuit.library import EfficientSU2 From d2b67d9cda8dfbfd857ec1f711f3bf8a77750de8 Mon Sep 17 00:00:00 2001 From: Dariusz Lasecki Date: Thu, 1 Sep 2022 14:15:25 +0200 Subject: [PATCH 05/48] Added observables_evaluator.py that uses primitives. --- test/python/algorithms/test_observables_evaluator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/python/algorithms/test_observables_evaluator.py b/test/python/algorithms/test_observables_evaluator.py index 88e256084fea..f9716fb0ee64 100644 --- a/test/python/algorithms/test_observables_evaluator.py +++ b/test/python/algorithms/test_observables_evaluator.py @@ -83,7 +83,7 @@ def test_eval_observables(self, observables: Sequence[OperatorBase]): ) bound_ansatz = ansatz.bind_parameters(parameters) - states = [bound_ansatz] * len(observables) + states = bound_ansatz expected_result = self.get_exact_expectation(bound_ansatz, observables) estimator = Estimator() decimal = 6 From c1276d2c5a4573fc8428df3c9aafe1cbd3a9c8bb Mon Sep 17 00:00:00 2001 From: Dariusz Lasecki Date: Thu, 1 Sep 2022 14:15:49 +0200 Subject: [PATCH 06/48] Updated trotter_qrte.py to use primitives. --- .../trotterization/trotter_qrte.py | 8 +------ .../trotterization/test_trotter_qrte.py | 24 +++++++------------ 2 files changed, 9 insertions(+), 23 deletions(-) diff --git a/qiskit/algorithms/time_evolvers/trotterization/trotter_qrte.py b/qiskit/algorithms/time_evolvers/trotterization/trotter_qrte.py index 010dd0b618c8..9c41ac748b2a 100644 --- a/qiskit/algorithms/time_evolvers/trotterization/trotter_qrte.py +++ b/qiskit/algorithms/time_evolvers/trotterization/trotter_qrte.py @@ -22,17 +22,13 @@ SummedOp, PauliOp, CircuitOp, - ExpectationBase, - CircuitSampler, PauliSumOp, StateFn, OperatorBase, ) from qiskit.circuit.library import PauliEvolutionGate from qiskit.primitives import Estimator -from qiskit.providers import Backend from qiskit.synthesis import ProductFormula, LieTrotter -from qiskit.utils import QuantumInstance class TrotterQRTE(RealEvolver): @@ -113,9 +109,7 @@ def evolve(self, evolution_problem: EvolutionProblem) -> EvolutionResult: "``t_param`` from the EvolutionProblem should be set to None." ) - if evolution_problem.aux_operators is not None and ( - self.estimator is 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/test/python/algorithms/time_evolvers/trotterization/test_trotter_qrte.py b/test/python/algorithms/time_evolvers/trotterization/test_trotter_qrte.py index 75d30142c049..91240c6423e1 100644 --- a/test/python/algorithms/time_evolvers/trotterization/test_trotter_qrte.py +++ b/test/python/algorithms/time_evolvers/trotterization/test_trotter_qrte.py @@ -13,19 +13,18 @@ """ Test TrotterQRTE. """ import unittest - -from qiskit.algorithms.time_evolvers.evolution_problem import EvolutionProblem -from qiskit.algorithms.time_evolvers.trotterization.trotter_qrte import TrotterQRTE -from qiskit.primitives import Estimator 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.time_evolvers.evolution_problem import EvolutionProblem +from qiskit.algorithms.time_evolvers.trotterization.trotter_qrte import TrotterQRTE +from qiskit.primitives import Estimator +from qiskit import QuantumCircuit from qiskit.circuit.library import ZGate -from qiskit.quantum_info import Statevector -from qiskit.utils import algorithm_globals, QuantumInstance +from qiskit.quantum_info import Statevector, Pauli +from qiskit.utils import algorithm_globals from qiskit.circuit import Parameter from qiskit.opflow import ( X, @@ -36,7 +35,6 @@ I, Y, SummedOp, - ExpectationFactory, ) from qiskit.synthesis import SuzukiTrotter, QDrift @@ -79,7 +77,7 @@ 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] + aux_ops = [Pauli("X"), Pauli("Y")] initial_state = Zero time = 3 @@ -90,18 +88,12 @@ def test_trotter_qrte_trotter_single_qubit_aux_ops(self): 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), - ] algorithm_globals.random_seed = 0 trotter_qrte = TrotterQRTE(estimator=estimator) evolution_result = trotter_qrte.evolve(evolution_problem) - np.testing.assert_equal( - evolution_result.evolved_state.eval(), expected_evolved_state - ) + np.testing.assert_equal(evolution_result.evolved_state.eval(), expected_evolved_state) np.testing.assert_array_almost_equal( evolution_result.aux_ops_evaluated, expected_aux_ops_evaluated From e06c6c28c0f75a5407127634263d79142ff4ff73 Mon Sep 17 00:00:00 2001 From: Dariusz Lasecki Date: Thu, 1 Sep 2022 14:16:49 +0200 Subject: [PATCH 07/48] Updated imports --- .../algorithms/time_evolvers/trotterization/trotter_qrte.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/qiskit/algorithms/time_evolvers/trotterization/trotter_qrte.py b/qiskit/algorithms/time_evolvers/trotterization/trotter_qrte.py index 9c41ac748b2a..a29a1739a65a 100644 --- a/qiskit/algorithms/time_evolvers/trotterization/trotter_qrte.py +++ b/qiskit/algorithms/time_evolvers/trotterization/trotter_qrte.py @@ -15,9 +15,10 @@ from typing import Union, Optional from qiskit import QuantumCircuit -from qiskit.algorithms.evolvers import EvolutionProblem, EvolutionResult -from qiskit.algorithms.evolvers.real_evolver import RealEvolver from qiskit.algorithms.observables_evaluator import eval_observables +from qiskit.algorithms.time_evolvers.evolution_problem import EvolutionProblem +from qiskit.algorithms.time_evolvers.evolution_result import EvolutionResult +from qiskit.algorithms.time_evolvers.real_evolver import RealEvolver from qiskit.opflow import ( SummedOp, PauliOp, From 02def3821300e47e7430f64f2bf5e131cbfc22d3 Mon Sep 17 00:00:00 2001 From: Dariusz Lasecki Date: Thu, 1 Sep 2022 16:30:35 +0200 Subject: [PATCH 08/48] Added estimator to pvqd.py (draft) --- qiskit/algorithms/time_evolvers/__init__.py | 11 + .../time_evolvers/evolution_problem.py | 109 +++++ .../time_evolvers/evolution_result.py | 40 ++ .../time_evolvers/imaginary_evolver.py | 37 ++ .../algorithms/time_evolvers/pvqd/__init__.py | 18 + qiskit/algorithms/time_evolvers/pvqd/pvqd.py | 396 ++++++++++++++++++ .../time_evolvers/pvqd/pvqd_result.py | 55 +++ qiskit/algorithms/time_evolvers/pvqd/utils.py | 101 +++++ .../algorithms/time_evolvers/real_evolver.py | 37 ++ 9 files changed, 804 insertions(+) create mode 100644 qiskit/algorithms/time_evolvers/__init__.py create mode 100644 qiskit/algorithms/time_evolvers/evolution_problem.py create mode 100644 qiskit/algorithms/time_evolvers/evolution_result.py create mode 100644 qiskit/algorithms/time_evolvers/imaginary_evolver.py create mode 100644 qiskit/algorithms/time_evolvers/pvqd/__init__.py create mode 100644 qiskit/algorithms/time_evolvers/pvqd/pvqd.py create mode 100644 qiskit/algorithms/time_evolvers/pvqd/pvqd_result.py create mode 100644 qiskit/algorithms/time_evolvers/pvqd/utils.py create mode 100644 qiskit/algorithms/time_evolvers/real_evolver.py diff --git a/qiskit/algorithms/time_evolvers/__init__.py b/qiskit/algorithms/time_evolvers/__init__.py new file mode 100644 index 000000000000..fdb172d367f0 --- /dev/null +++ b/qiskit/algorithms/time_evolvers/__init__.py @@ -0,0 +1,11 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2022. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. diff --git a/qiskit/algorithms/time_evolvers/evolution_problem.py b/qiskit/algorithms/time_evolvers/evolution_problem.py new file mode 100644 index 000000000000..175beecd6bf6 --- /dev/null +++ b/qiskit/algorithms/time_evolvers/evolution_problem.py @@ -0,0 +1,109 @@ +# 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 typing import Union, Optional, Dict + +from qiskit import QuantumCircuit +from qiskit.circuit import Parameter +from qiskit.opflow import OperatorBase, StateFn +from ..list_or_dict import ListOrDict + + +class EvolutionProblem: + """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. + """ + + def __init__( + self, + hamiltonian: OperatorBase, + time: float, + initial_state: Optional[Union[StateFn, QuantumCircuit]] = None, + aux_operators: Optional[ListOrDict[OperatorBase]] = None, + truncation_threshold: float = 1e-12, + t_param: Optional[Parameter] = None, + param_value_dict: Optional[Dict[Parameter, complex]] = 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() + 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/time_evolvers/evolution_result.py b/qiskit/algorithms/time_evolvers/evolution_result.py new file mode 100644 index 000000000000..1dd91d705d28 --- /dev/null +++ b/qiskit/algorithms/time_evolvers/evolution_result.py @@ -0,0 +1,40 @@ +# 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 typing import Optional, Union, Tuple + +from qiskit import QuantumCircuit +from qiskit.algorithms.list_or_dict import ListOrDict +from qiskit.opflow import StateFn, OperatorBase +from ..algorithm_result import AlgorithmResult + + +class EvolutionResult(AlgorithmResult): + """Class for holding evolution result.""" + + def __init__( + self, + evolved_state: Union[StateFn, QuantumCircuit, OperatorBase], + aux_ops_evaluated: Optional[ListOrDict[Tuple[complex, complex]]] = 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/time_evolvers/imaginary_evolver.py b/qiskit/algorithms/time_evolvers/imaginary_evolver.py new file mode 100644 index 000000000000..309bb73b08af --- /dev/null +++ b/qiskit/algorithms/time_evolvers/imaginary_evolver.py @@ -0,0 +1,37 @@ +# 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 .evolution_problem import EvolutionProblem +from .evolution_result import EvolutionResult + + +class ImaginaryEvolver(ABC): + """Interface for Quantum Imaginary Time Evolution.""" + + @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/time_evolvers/pvqd/__init__.py b/qiskit/algorithms/time_evolvers/pvqd/__init__.py new file mode 100644 index 000000000000..9377ce631b4e --- /dev/null +++ b/qiskit/algorithms/time_evolvers/pvqd/__init__.py @@ -0,0 +1,18 @@ +# 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 new file mode 100644 index 000000000000..adaa676b1b8c --- /dev/null +++ b/qiskit/algorithms/time_evolvers/pvqd/pvqd.py @@ -0,0 +1,396 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2019, 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 Dynamics Algorithm.""" + +from typing import Optional, Union, List, Tuple, Callable + +import logging +import numpy as np + +from qiskit import QiskitError +from qiskit.algorithms.optimizers import Optimizer, Minimizer +from qiskit.circuit import QuantumCircuit, ParameterVector +from qiskit.circuit.library import PauliEvolutionGate +from qiskit.extensions import HamiltonianGate +from qiskit.opflow import OperatorBase, CircuitSampler, ExpectationBase, StateFn, MatrixOp +from qiskit.primitives import Estimator +from qiskit.synthesis import EvolutionSynthesis, LieTrotter + +from .pvqd_result import PVQDResult +from .utils import _get_observable_evaluator, _is_gradient_supported + +from ..evolution_problem import EvolutionProblem +from ..evolution_result import EvolutionResult +from ..real_evolver import RealEvolver + +logger = logging.getLogger(__name__) + + +class PVQD(RealEvolver): + """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. + expectation (ExpectationBase): The method to compute expectation values. + 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 import BasicAer + from qiskit.circuit.library import EfficientSU2 + from qiskit.opflow import X, Z, I, MatrixExpectation + + backend = BasicAer.get_backend("statevector_simulator") + expectation = MatrixExpectation() + hamiltonian = 0.1 * (Z ^ Z) + (I ^ X) + (X ^ I) + observable = Z ^ Z + ansatz = EfficientSU2(2, reps=1) + initial_parameters = np.zeros(ansatz.num_parameters) + + time = 1 + optimizer = L_BFGS_B() + + # setup the algorithm + pvqd = PVQD( + ansatz, + initial_parameters, + num_timesteps=100, + optimizer=optimizer, + quantum_instance=backend, + expectation=expectation + ) + + # specify the evolution problem + problem = EvolutionProblem( + 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, + ansatz: QuantumCircuit, + initial_parameters: np.ndarray, + expectation: ExpectationBase, + estimator: Estimator, + optimizer: Optional[Union[Optimizer, Minimizer]] = None, + num_timesteps: Optional[int] = None, + evolution: Optional[EvolutionSynthesis] = None, + use_parameter_shift: bool = True, + initial_guess: Optional[np.ndarray] = None, + ) -> None: + """ + Args: + 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. + expectation: The expectation converter to evaluate expectation values. + 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_timestep: 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]`. + """ + 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.expectation = expectation + self.estimator = estimator + self.evolution = evolution + self.use_parameter_shift = use_parameter_shift + + def step( + self, + hamiltonian: OperatorBase, + 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: + initial_guess = np.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: OperatorBase, + ansatz: QuantumCircuit, + dt: float, + current_parameters: np.ndarray, + ) -> Tuple[Callable[[np.ndarray], float], Optional[Callable[[np.ndarray], np.ndarray]]]: + + """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.bind_parameters(current_parameters) + + if isinstance(hamiltonian, MatrixOp): + evolution_gate = HamiltonianGate(hamiltonian.primitive, time=dt) + else: + 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) + overlap = StateFn(trotterized).adjoint() @ StateFn(shifted) + + converted = self.expectation.convert(overlap) + + def evaluate_loss( + displacement: Union[np.ndarray, List[np.ndarray]] + ) -> Union[float, List[float]]: + """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. + """ + 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)) + + # TODO fidelity primitive + sampled = self._sampler.convert(converted, params=value_dict) + + # 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 - np.abs(sampled.eval()) ** 2 + + 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 evolve(self, evolution_problem: EvolutionProblem) -> EvolutionResult: + """ + 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 the evolution time is not positive or the timestep is too small. + 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: + evaluate_observables = _get_observable_evaluator( + self.ansatz, observables, self.estimator + ) + observable_values = [evaluate_observables(self.initial_parameters)] + + fidelities = [1] + 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.bind_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 = {"quantum_instance", "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 new file mode 100644 index 000000000000..2c562f3262a8 --- /dev/null +++ b/qiskit/algorithms/time_evolvers/pvqd/pvqd_result.py @@ -0,0 +1,55 @@ +# 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 typing import Union, Optional, List, Tuple +import numpy as np + +from qiskit.circuit import QuantumCircuit +from qiskit.opflow import StateFn, OperatorBase + +from ..evolution_result import EvolutionResult + + +class PVQDResult(EvolutionResult): + """The result object for the p-VQD algorithm.""" + + def __init__( + self, + evolved_state: Union[StateFn, QuantumCircuit], + aux_ops_evaluated: Optional[List[Tuple[complex, complex]]] = None, + times: Optional[List[float]] = None, + parameters: Optional[List[np.ndarray]] = None, + fidelities: Optional[List[float]] = None, + estimated_error: Optional[float] = None, + observables: Optional[List[List[float]]] = 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 new file mode 100644 index 000000000000..31db6b500af5 --- /dev/null +++ b/qiskit/algorithms/time_evolvers/pvqd/utils.py @@ -0,0 +1,101 @@ +# 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 typing import Union, List, Callable +import logging + +import numpy as np + +from qiskit.circuit import QuantumCircuit, Parameter, ParameterExpression +from qiskit.compiler import transpile +from qiskit.exceptions import QiskitError +from qiskit.opflow import ListOp, CircuitSampler, ExpectationBase, StateFn, OperatorBase +from qiskit.opflow.gradients.circuit_gradients import ParamShift +from qiskit.primitives import Estimator + +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: Union[OperatorBase, List[OperatorBase]], + estimator: Estimator, +) -> Callable[[np.ndarray], Union[float, List[float]]]: + """Get a callable to evaluate a (list of) observable(s) for given circuit parameters.""" + + if isinstance(observables, list): + observables = ListOp(observables) + + ansatz_parameters = ansatz.parameter + + def evaluate_observables(theta: np.ndarray) -> Union[float, List[float]]: + """Evaluate the observables for the ansatz parameters ``theta``. + + Args: + theta: The ansatz parameters. + + Returns: + The observables evaluated at the ansatz parameters. + """ + value_dict = dict(zip(ansatz_parameters, theta)) + states = [ansatz] * len(observables) + + estimator_job = estimator.run( + states, observables, parameter_values=list(value_dict.values()) + ) + return estimator_job.result().values() + + return evaluate_observables diff --git a/qiskit/algorithms/time_evolvers/real_evolver.py b/qiskit/algorithms/time_evolvers/real_evolver.py new file mode 100644 index 000000000000..6107facfe542 --- /dev/null +++ b/qiskit/algorithms/time_evolvers/real_evolver.py @@ -0,0 +1,37 @@ +# 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 .evolution_problem import EvolutionProblem +from .evolution_result import EvolutionResult + + +class RealEvolver(ABC): + """Interface for Quantum Real Time Evolution.""" + + @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() From 5a9bebe9880329dd94089e369d72f3c0bdacee89 Mon Sep 17 00:00:00 2001 From: Dariusz Lasecki Date: Mon, 5 Sep 2022 10:37:51 +0200 Subject: [PATCH 09/48] Updated typehints and limited use of opflow. --- .../time_evolvers/evolution_problem.py | 35 +-- .../time_evolvers/evolution_result.py | 7 +- .../time_evolvers/trotterization/__init__.py | 21 -- .../trotterization/trotter_qrte.py | 178 --------------- .../time_evolvers/test_evolution_problem.py | 26 +-- .../time_evolvers/test_evolution_result.py | 2 +- .../time_evolvers/trotterization/__init__.py | 11 - .../trotterization/test_trotter_qrte.py | 205 ------------------ 8 files changed, 21 insertions(+), 464 deletions(-) delete mode 100644 qiskit/algorithms/time_evolvers/trotterization/__init__.py delete mode 100644 qiskit/algorithms/time_evolvers/trotterization/trotter_qrte.py delete mode 100644 test/python/algorithms/time_evolvers/trotterization/__init__.py delete mode 100644 test/python/algorithms/time_evolvers/trotterization/test_trotter_qrte.py diff --git a/qiskit/algorithms/time_evolvers/evolution_problem.py b/qiskit/algorithms/time_evolvers/evolution_problem.py index 175beecd6bf6..8ea90f77f690 100644 --- a/qiskit/algorithms/time_evolvers/evolution_problem.py +++ b/qiskit/algorithms/time_evolvers/evolution_problem.py @@ -12,12 +12,13 @@ """Evolution problem class.""" -from typing import Union, Optional, Dict +from typing import Dict from qiskit import QuantumCircuit from qiskit.circuit import Parameter -from qiskit.opflow import OperatorBase, StateFn +from qiskit.opflow import PauliSumOp from ..list_or_dict import ListOrDict +from ...quantum_info.operators.base_operator import BaseOperator class EvolutionProblem: @@ -29,13 +30,14 @@ class EvolutionProblem: def __init__( self, - hamiltonian: OperatorBase, + hamiltonian: BaseOperator | PauliSumOp, time: float, - initial_state: Optional[Union[StateFn, QuantumCircuit]] = None, - aux_operators: Optional[ListOrDict[OperatorBase]] = None, + initial_state: QuantumCircuit | None = None, + aux_operators: ListOrDict[BaseOperator | PauliSumOp] | None = None, truncation_threshold: float = 1e-12, - t_param: Optional[Parameter] = None, - param_value_dict: Optional[Dict[Parameter, complex]] = None, + t_param: Parameter | None = None, + param_value_dict: Dict[Parameter, complex] + | None = None, # parametrization will become supported in BaseOperator soon ): """ Args: @@ -90,20 +92,5 @@ def validate_params(self) -> None: 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() - 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}." - ) + if isinstance(self.hamiltonian, PauliSumOp) and self.hamiltonian.parameters: + raise ValueError("A global parametrized coefficient for PauliSumOp is not allowed.") diff --git a/qiskit/algorithms/time_evolvers/evolution_result.py b/qiskit/algorithms/time_evolvers/evolution_result.py index 1dd91d705d28..55a3fad31032 100644 --- a/qiskit/algorithms/time_evolvers/evolution_result.py +++ b/qiskit/algorithms/time_evolvers/evolution_result.py @@ -12,11 +12,10 @@ """Class for holding evolution result.""" -from typing import Optional, Union, Tuple +from typing import Tuple from qiskit import QuantumCircuit from qiskit.algorithms.list_or_dict import ListOrDict -from qiskit.opflow import StateFn, OperatorBase from ..algorithm_result import AlgorithmResult @@ -25,8 +24,8 @@ class EvolutionResult(AlgorithmResult): def __init__( self, - evolved_state: Union[StateFn, QuantumCircuit, OperatorBase], - aux_ops_evaluated: Optional[ListOrDict[Tuple[complex, complex]]] = None, + evolved_state: QuantumCircuit, + aux_ops_evaluated: ListOrDict[Tuple[complex, complex]] | None = None, ): """ Args: diff --git a/qiskit/algorithms/time_evolvers/trotterization/__init__.py b/qiskit/algorithms/time_evolvers/trotterization/__init__.py deleted file mode 100644 index fe1b8d8aedf2..000000000000 --- a/qiskit/algorithms/time_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/time_evolvers/trotterization/trotter_qrte.py b/qiskit/algorithms/time_evolvers/trotterization/trotter_qrte.py deleted file mode 100644 index a29a1739a65a..000000000000 --- a/qiskit/algorithms/time_evolvers/trotterization/trotter_qrte.py +++ /dev/null @@ -1,178 +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 typing import Union, Optional - -from qiskit import QuantumCircuit -from qiskit.algorithms.observables_evaluator import eval_observables -from qiskit.algorithms.time_evolvers.evolution_problem import EvolutionProblem -from qiskit.algorithms.time_evolvers.evolution_result import EvolutionResult -from qiskit.algorithms.time_evolvers.real_evolver import RealEvolver -from qiskit.opflow import ( - SummedOp, - PauliOp, - CircuitOp, - PauliSumOp, - StateFn, - OperatorBase, -) -from qiskit.circuit.library import PauliEvolutionGate -from qiskit.primitives import Estimator -from qiskit.synthesis import ProductFormula, LieTrotter - - -class TrotterQRTE(RealEvolver): - """Quantum Real Time Evolution using Trotterization. - Type of Trotterization is defined by a ProductFormula provided. - - Examples: - - .. jupyter-execute:: - - from qiskit.opflow import X, Z, Zero - from qiskit.algorithms import EvolutionProblem, TrotterQRTE - - operator = X + Z - initial_state = Zero - time = 1 - evolution_problem = EvolutionProblem(operator, 1, initial_state) - # LieTrotter with 1 rep - trotter_qrte = TrotterQRTE() - evolved_state = trotter_qrte.evolve(evolution_problem).evolved_state - """ - - def __init__( - self, - product_formula: Optional[ProductFormula] = None, - estimator: Estimator = 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. - estimator: An estimator primitive used for calculating expectation values of - EvolutionProblem.aux_operators. - """ - if product_formula is None: - product_formula = LieTrotter() - self.product_formula = product_formula - self.estimator = estimator - - @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 an estimator primitive 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 an estimator primitive. - - 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.estimator is None): - raise ValueError( - "aux_operators were provided for evaluations but no ``estimator`` 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.bind_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.estimator, - evolved_state.primitive, - evolution_problem.aux_operators, - evolution_problem.truncation_threshold, - ) - - return EvolutionResult(evolved_state, evaluated_aux_ops) - - @staticmethod - def _summed_op_to_pauli_sum_op( - hamiltonian: SummedOp, - ) -> Union[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/test/python/algorithms/time_evolvers/test_evolution_problem.py b/test/python/algorithms/time_evolvers/test_evolution_problem.py index d3dd2f7bc9b2..d7d55500abfb 100644 --- a/test/python/algorithms/time_evolvers/test_evolution_problem.py +++ b/test/python/algorithms/time_evolvers/test_evolution_problem.py @@ -12,13 +12,15 @@ """Test evolver problem class.""" import unittest + +from qiskit.algorithms.time_evolvers.evolution_problem import EvolutionProblem +from qiskit.quantum_info import Pauli, SparsePauliOp 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 +from qiskit.opflow import Y, Z, One, X, Zero, PauliSumOp @ddt @@ -89,25 +91,9 @@ def test_init_errors(self, 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."): - hamiltonian = param_x * X + param_y * Y - param_dict = {param_y: 2} - 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."): - hamiltonian = param_x * X + param_y * Y - param_dict = {} - 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."): - hamiltonian = param_x * X + param_y * Y - param_dict = {param_y: 2, param_x: 1, Parameter("z"): 1} - evolution_problem = EvolutionProblem(hamiltonian, 2, Zero, param_value_dict=param_dict) + hamiltonian = PauliSumOp(SparsePauliOp([Pauli("X"), Pauli("Y")]), param_x) + evolution_problem = EvolutionProblem(hamiltonian, 2, Zero) with assert_raises(ValueError): evolution_problem.validate_params() diff --git a/test/python/algorithms/time_evolvers/test_evolution_result.py b/test/python/algorithms/time_evolvers/test_evolution_result.py index 5500b283a1cb..3c599f2eb1b4 100644 --- a/test/python/algorithms/time_evolvers/test_evolution_result.py +++ b/test/python/algorithms/time_evolvers/test_evolution_result.py @@ -12,8 +12,8 @@ """Class for testing evolution result.""" import unittest +from qiskit.algorithms.time_evolvers.evolution_result import EvolutionResult from test.python.algorithms import QiskitAlgorithmsTestCase -from qiskit.algorithms.evolvers.evolution_result import EvolutionResult from qiskit.opflow import Zero diff --git a/test/python/algorithms/time_evolvers/trotterization/__init__.py b/test/python/algorithms/time_evolvers/trotterization/__init__.py deleted file mode 100644 index 96c0cf22bec9..000000000000 --- a/test/python/algorithms/time_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/time_evolvers/trotterization/test_trotter_qrte.py b/test/python/algorithms/time_evolvers/trotterization/test_trotter_qrte.py deleted file mode 100644 index 91240c6423e1..000000000000 --- a/test/python/algorithms/time_evolvers/trotterization/test_trotter_qrte.py +++ /dev/null @@ -1,205 +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. - -""" Test TrotterQRTE. """ - -import unittest -from test.python.opflow import QiskitOpflowTestCase -from ddt import ddt, data, unpack -import numpy as np -from numpy.testing import assert_raises - -from qiskit.algorithms.time_evolvers.evolution_problem import EvolutionProblem -from qiskit.algorithms.time_evolvers.trotterization.trotter_qrte import TrotterQRTE -from qiskit.primitives import Estimator -from qiskit import QuantumCircuit -from qiskit.circuit.library import ZGate -from qiskit.quantum_info import Statevector, Pauli -from qiskit.utils import algorithm_globals -from qiskit.circuit import Parameter -from qiskit.opflow import ( - X, - Z, - Zero, - VectorStateFn, - StateFn, - I, - Y, - SummedOp, -) -from qiskit.synthesis import SuzukiTrotter, QDrift - - -@ddt -class TestTrotterQRTE(QiskitOpflowTestCase): - """TrotterQRTE tests.""" - - def setUp(self): - super().setUp() - self.seed = 50 - algorithm_globals.random_seed = self.seed - - @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 - 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 = [Pauli("X"), Pauli("Y")] - - initial_state = Zero - time = 3 - evolution_problem = EvolutionProblem(operator, time, initial_state, aux_ops) - estimator = Estimator() - - 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)] - - algorithm_globals.random_seed = 0 - trotter_qrte = TrotterQRTE(estimator=estimator) - evolution_result = trotter_qrte.evolve(evolution_problem) - - np.testing.assert_equal(evolution_result.evolved_state.eval(), expected_evolved_state) - - 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]) - 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 - evolution_problem = EvolutionProblem( - operator, time, initial_state, param_value_dict=params_dict - ) - expected_state = VectorStateFn( - Statevector([-0.9899925 - 0.14112001j, 0.0 + 0.0j, 0.0 + 0.0j, 0.0 + 0.0j], dims=(2, 2)) - ) - 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 - evolution_problem = EvolutionProblem(operator, time, initial_state) - - algorithm_globals.random_seed = 0 - trotter_qrte = TrotterQRTE(product_formula=QDrift()) - 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 - algorithm_globals.random_seed = 0 - trotter_qrte = TrotterQRTE() - with assert_raises(ValueError): - evolution_problem = EvolutionProblem( - operator, - time, - initial_state, - t_param=t_param, - param_value_dict=param_value_dict, - ) - _ = trotter_qrte.evolve(evolution_problem) - - -if __name__ == "__main__": - unittest.main() From 356deae49a90d21fd5b8777be0b9060c29c89aef Mon Sep 17 00:00:00 2001 From: Dariusz Lasecki Date: Mon, 5 Sep 2022 10:43:04 +0200 Subject: [PATCH 10/48] Updated typehints and limited use of opflow. --- qiskit/algorithms/observables_evaluator.py | 12 ++++++------ test/python/algorithms/test_observables_evaluator.py | 11 +++++++---- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/qiskit/algorithms/observables_evaluator.py b/qiskit/algorithms/observables_evaluator.py index dba83b308940..e0096e0a7cf1 100644 --- a/qiskit/algorithms/observables_evaluator.py +++ b/qiskit/algorithms/observables_evaluator.py @@ -11,7 +11,7 @@ # that they have been altered from the originals. """Evaluator of auxiliary operators for algorithms.""" -from typing import Tuple, Union, List, Sequence, Optional +from typing import Tuple, List, Sequence import numpy as np @@ -19,14 +19,14 @@ from qiskit.opflow import ( PauliSumOp, ) -from ..primitives import Estimator, EstimatorResult +from ..primitives import EstimatorResult, BaseEstimator from ..quantum_info.operators.base_operator import BaseOperator def eval_observables( - estimator: Estimator, + estimator: BaseEstimator, quantum_state: QuantumCircuit, - observables: Sequence[Union[BaseOperator, PauliSumOp]], + observables: Sequence[BaseOperator | PauliSumOp], threshold: float = 1e-12, ) -> List[Tuple[complex, complex]]: """ @@ -77,7 +77,7 @@ def eval_observables( def _prepare_result( observables_results: List[Tuple[complex, complex]], - observables: Sequence[BaseOperator], + observables: Sequence[BaseOperator | PauliSumOp], ) -> List[Tuple[complex, complex]]: """ Prepares a list of eigenvalues and standard deviations from ``observables_results`` and @@ -104,7 +104,7 @@ def _prepare_result( def _compute_std_devs( estimator_result: EstimatorResult, results_length: int, -) -> List[Optional[complex]]: +) -> List[complex | None]: """ Calculates a list of standard deviations from expectation values of observables provided. diff --git a/test/python/algorithms/test_observables_evaluator.py b/test/python/algorithms/test_observables_evaluator.py index f9716fb0ee64..0951c7690f5a 100644 --- a/test/python/algorithms/test_observables_evaluator.py +++ b/test/python/algorithms/test_observables_evaluator.py @@ -13,6 +13,8 @@ import unittest from typing import Tuple, Sequence, List + +from qiskit.quantum_info.operators.base_operator import BaseOperator from test.python.algorithms import QiskitAlgorithmsTestCase import numpy as np from ddt import ddt, data @@ -24,7 +26,6 @@ from qiskit.circuit.library import EfficientSU2 from qiskit.opflow import ( PauliSumOp, - OperatorBase, ) from qiskit.utils import algorithm_globals @@ -40,7 +41,9 @@ def setUp(self): self.threshold = 1e-8 - def get_exact_expectation(self, ansatz: QuantumCircuit, observables: Sequence[OperatorBase]): + def get_exact_expectation( + self, ansatz: QuantumCircuit, observables: Sequence[BaseOperator | PauliSumOp] + ): """ Calculates the exact expectation to be used as an expected result for unit tests. """ @@ -57,7 +60,7 @@ def _run_test( expected_result: List[Tuple[complex, complex]], quantum_state: Sequence[QuantumCircuit], decimal: int, - observables: Sequence[OperatorBase], + observables: Sequence[BaseOperator | PauliSumOp], estimator: Estimator, ): result = eval_observables(estimator, quantum_state, observables, self.threshold) @@ -73,7 +76,7 @@ def _run_test( PauliSumOp.from_list([("ZZ", 2.0)]), ], ) - def test_eval_observables(self, observables: Sequence[OperatorBase]): + def test_eval_observables(self, observables: Sequence[BaseOperator | PauliSumOp]): """Tests evaluator of auxiliary operators for algorithms.""" ansatz = EfficientSU2(2) From 14ba35da98ee928632745345e0ebe14f427f36b9 Mon Sep 17 00:00:00 2001 From: Dariusz Lasecki Date: Mon, 5 Sep 2022 11:10:37 +0200 Subject: [PATCH 11/48] Removed files out of scope for this PR. --- qiskit/algorithms/observables_evaluator.py | 130 ------------------ .../algorithms/test_observables_evaluator.py | 103 -------------- .../time_evolvers/test_evolution_problem.py | 5 +- .../time_evolvers/test_evolution_result.py | 3 +- 4 files changed, 3 insertions(+), 238 deletions(-) delete mode 100644 qiskit/algorithms/observables_evaluator.py delete mode 100644 test/python/algorithms/test_observables_evaluator.py diff --git a/qiskit/algorithms/observables_evaluator.py b/qiskit/algorithms/observables_evaluator.py deleted file mode 100644 index e0096e0a7cf1..000000000000 --- a/qiskit/algorithms/observables_evaluator.py +++ /dev/null @@ -1,130 +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 typing import Tuple, List, Sequence - -import numpy as np - -from qiskit import QuantumCircuit -from qiskit.opflow import ( - PauliSumOp, -) -from ..primitives import EstimatorResult, BaseEstimator -from ..quantum_info.operators.base_operator import BaseOperator - - -def eval_observables( - estimator: BaseEstimator, - quantum_state: QuantumCircuit, - observables: Sequence[BaseOperator | PauliSumOp], - threshold: float = 1e-12, -) -> List[Tuple[complex, complex]]: - """ - Accepts a sequence of operators and calculates their expectation values - means - and standard deviations. They are calculated with respect to a quantum state provided. A user - can optionally provide a threshold value which filters mean values falling below the threshold. - - Args: - estimator: An estimator primitive used for calculations. - quantum_state: An unparametrized quantum circuit representing a quantum state that - expectation values are computed against. - observables: A sequence of operators whose expectation values are to be calculated. - threshold: A threshold value that defines which mean values should be neglected (helpful for - ignoring numerical instabilities close to 0). - - Returns: - A list of tuples (mean, standard deviation). - - Raises: - ValueError: If a ``quantum_state`` with free parameters is provided. - """ - - if ( - isinstance(quantum_state, QuantumCircuit) # 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." - ) - - quantum_state = [quantum_state] * len(observables) - estimator_job = estimator.run(quantum_state, observables) - expectation_values = estimator_job.result().values - - # compute standard deviations - std_devs = _compute_std_devs(estimator_job, len(expectation_values)) - - # Discard values below threshold - observables_means = expectation_values * (np.abs(expectation_values) > threshold) - # zip means and standard deviations into tuples - observables_results = list(zip(observables_means, std_devs)) - - # Return None eigenvalues for None operators if observables is a list. - - return _prepare_result(observables_results, observables) - - -def _prepare_result( - observables_results: List[Tuple[complex, complex]], - observables: Sequence[BaseOperator | PauliSumOp], -) -> List[Tuple[complex, complex]]: - """ - Prepares a list of eigenvalues and standard deviations from ``observables_results`` and - ``observables``. - - Args: - observables_results: A list of of tuples (mean, standard deviation). - observables: A list of operators whose expectation values are to be - calculated. - - Returns: - A list of tuples (mean, standard deviation). - """ - - observables_eigenvalues = [None] * len(observables) - key_value_iterator = enumerate(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( - estimator_result: EstimatorResult, - results_length: int, -) -> List[complex | None]: - """ - Calculates a list of standard deviations from expectation values of observables provided. - - Args: - estimator_result: An estimator result. - results_length: Number of expectation values calculated. - - Returns: - A list of standard deviations. - """ - if not estimator_result.metadata: - return [0] * results_length - - std_devs = [] - for metadata in estimator_result.metadata: - if metadata and "variance" in metadata.keys() and "shots" in metadata.keys(): - variance = metadata["variance"] - shots = metadata["shots"] - std_devs.append(np.sqrt(variance / shots)) - else: - std_devs.append(0) - - return std_devs diff --git a/test/python/algorithms/test_observables_evaluator.py b/test/python/algorithms/test_observables_evaluator.py deleted file mode 100644 index 0951c7690f5a..000000000000 --- a/test/python/algorithms/test_observables_evaluator.py +++ /dev/null @@ -1,103 +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 evaluator of auxiliary operators for algorithms.""" - -import unittest -from typing import Tuple, Sequence, List - -from qiskit.quantum_info.operators.base_operator import BaseOperator -from test.python.algorithms import QiskitAlgorithmsTestCase -import numpy as np -from ddt import ddt, data - -from qiskit.algorithms.observables_evaluator import eval_observables -from qiskit.primitives import Estimator -from qiskit.quantum_info import Statevector -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 - algorithm_globals.random_seed = self.seed - - self.threshold = 1e-8 - - def get_exact_expectation( - self, ansatz: QuantumCircuit, observables: Sequence[BaseOperator | PauliSumOp] - ): - """ - Calculates the exact expectation to be used as an expected result for unit tests. - """ - - # 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 - ] - - return exact - - def _run_test( - self, - expected_result: List[Tuple[complex, complex]], - quantum_state: Sequence[QuantumCircuit], - decimal: int, - observables: Sequence[BaseOperator | PauliSumOp], - estimator: Estimator, - ): - result = eval_observables(estimator, quantum_state, observables, self.threshold) - - 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)]), - ], - ) - def test_eval_observables(self, observables: Sequence[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.bind_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, - ) - - -if __name__ == "__main__": - unittest.main() diff --git a/test/python/algorithms/time_evolvers/test_evolution_problem.py b/test/python/algorithms/time_evolvers/test_evolution_problem.py index d7d55500abfb..76683e27cd13 100644 --- a/test/python/algorithms/time_evolvers/test_evolution_problem.py +++ b/test/python/algorithms/time_evolvers/test_evolution_problem.py @@ -13,12 +13,11 @@ """Test evolver problem class.""" import unittest -from qiskit.algorithms.time_evolvers.evolution_problem import EvolutionProblem -from qiskit.quantum_info import Pauli, SparsePauliOp from test.python.algorithms import QiskitAlgorithmsTestCase from ddt import data, ddt, unpack from numpy.testing import assert_raises - +from qiskit.algorithms.time_evolvers.evolution_problem import EvolutionProblem +from qiskit.quantum_info import Pauli, SparsePauliOp from qiskit.circuit import Parameter from qiskit.opflow import Y, Z, One, X, Zero, PauliSumOp diff --git a/test/python/algorithms/time_evolvers/test_evolution_result.py b/test/python/algorithms/time_evolvers/test_evolution_result.py index 3c599f2eb1b4..52b7f3faaf98 100644 --- a/test/python/algorithms/time_evolvers/test_evolution_result.py +++ b/test/python/algorithms/time_evolvers/test_evolution_result.py @@ -11,9 +11,8 @@ # that they have been altered from the originals. """Class for testing evolution result.""" import unittest - -from qiskit.algorithms.time_evolvers.evolution_result import EvolutionResult from test.python.algorithms import QiskitAlgorithmsTestCase +from qiskit.algorithms.time_evolvers.evolution_result import EvolutionResult from qiskit.opflow import Zero From 4cf6f61b5310e17229ceb594891510ed96cd40e3 Mon Sep 17 00:00:00 2001 From: Dariusz Lasecki Date: Mon, 5 Sep 2022 13:22:22 +0200 Subject: [PATCH 12/48] Added annotations import. --- qiskit/algorithms/time_evolvers/evolution_problem.py | 2 +- qiskit/algorithms/time_evolvers/evolution_result.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/qiskit/algorithms/time_evolvers/evolution_problem.py b/qiskit/algorithms/time_evolvers/evolution_problem.py index 8ea90f77f690..9db122f96268 100644 --- a/qiskit/algorithms/time_evolvers/evolution_problem.py +++ b/qiskit/algorithms/time_evolvers/evolution_problem.py @@ -11,7 +11,7 @@ # that they have been altered from the originals. """Evolution problem class.""" - +from __future__ import annotations from typing import Dict from qiskit import QuantumCircuit diff --git a/qiskit/algorithms/time_evolvers/evolution_result.py b/qiskit/algorithms/time_evolvers/evolution_result.py index 55a3fad31032..e89c3dba52e8 100644 --- a/qiskit/algorithms/time_evolvers/evolution_result.py +++ b/qiskit/algorithms/time_evolvers/evolution_result.py @@ -11,7 +11,7 @@ # that they have been altered from the originals. """Class for holding evolution result.""" - +from __future__ import annotations from typing import Tuple from qiskit import QuantumCircuit From 39c97356435f69c5f531e9d4d0b999154cacf042 Mon Sep 17 00:00:00 2001 From: Dariusz Lasecki Date: Mon, 5 Sep 2022 14:33:01 +0200 Subject: [PATCH 13/48] Added observables_evaluator.py with primitives. --- qiskit/algorithms/observables_evaluator.py | 130 ++++++++++++++++++ .../algorithms/test_observables_evaluator.py | 100 ++++++++++++++ 2 files changed, 230 insertions(+) create mode 100644 qiskit/algorithms/observables_evaluator.py create mode 100644 test/python/algorithms/test_observables_evaluator.py diff --git a/qiskit/algorithms/observables_evaluator.py b/qiskit/algorithms/observables_evaluator.py new file mode 100644 index 000000000000..0a351d9c73aa --- /dev/null +++ b/qiskit/algorithms/observables_evaluator.py @@ -0,0 +1,130 @@ +# 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 +from typing import Tuple, List, Sequence + +import numpy as np + +from qiskit import QuantumCircuit +from qiskit.opflow import ( + PauliSumOp, +) +from ..primitives import EstimatorResult, BaseEstimator +from ..quantum_info.operators.base_operator import BaseOperator + + +def eval_observables( + estimator: BaseEstimator, + quantum_state: QuantumCircuit, + observables: Sequence[BaseOperator | PauliSumOp], + threshold: float = 1e-12, +) -> List[Tuple[complex, complex]]: + """ + Accepts a sequence of operators and calculates their expectation values - means + and standard deviations. They are calculated with respect to a quantum state provided. A user + can optionally provide a threshold value which filters mean values falling below the threshold. + + Args: + estimator: An estimator primitive used for calculations. + quantum_state: An unparametrized quantum circuit representing a quantum state that + expectation values are computed against. + observables: A sequence of operators whose expectation values are to be calculated. + threshold: A threshold value that defines which mean values should be neglected (helpful for + ignoring numerical instabilities close to 0). + + Returns: + A list of tuples (mean, standard deviation). + + Raises: + ValueError: If a ``quantum_state`` with free parameters is provided. + """ + + if ( + isinstance(quantum_state, QuantumCircuit) # 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." + ) + + quantum_state = [quantum_state] * len(observables) + estimator_job = estimator.run(quantum_state, observables) + expectation_values = estimator_job.result().values + + # compute standard deviations + std_devs = _compute_std_devs(estimator_job, len(expectation_values)) + + # Discard values below threshold + observables_means = expectation_values * (np.abs(expectation_values) > threshold) + # zip means and standard deviations into tuples + observables_results = list(zip(observables_means, std_devs)) + + # Return None eigenvalues for None operators if observables is a list. + + return _prepare_result(observables_results, observables) + + +def _prepare_result( + observables_results: List[Tuple[complex, complex]], + observables: Sequence[BaseOperator], +) -> List[Tuple[complex, complex]]: + """ + Prepares a list of eigenvalues and standard deviations from ``observables_results`` and + ``observables``. + + Args: + observables_results: A list of of tuples (mean, standard deviation). + observables: A list of operators whose expectation values are to be + calculated. + + Returns: + A list of tuples (mean, standard deviation). + """ + + observables_eigenvalues = [None] * len(observables) + key_value_iterator = enumerate(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( + estimator_result: EstimatorResult, + results_length: int, +) -> List[complex | None]: + """ + Calculates a list of standard deviations from expectation values of observables provided. + + Args: + estimator_result: An estimator result. + results_length: Number of expectation values calculated. + + Returns: + A list of standard deviations. + """ + if not estimator_result.metadata: + return [0] * results_length + + std_devs = [] + for metadata in estimator_result.metadata: + if metadata and "variance" in metadata.keys() and "shots" in metadata.keys(): + variance = metadata["variance"] + shots = metadata["shots"] + std_devs.append(np.sqrt(variance / shots)) + else: + std_devs.append(0) + + return std_devs diff --git a/test/python/algorithms/test_observables_evaluator.py b/test/python/algorithms/test_observables_evaluator.py new file mode 100644 index 000000000000..f9716fb0ee64 --- /dev/null +++ b/test/python/algorithms/test_observables_evaluator.py @@ -0,0 +1,100 @@ +# 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 evaluator of auxiliary operators for algorithms.""" + +import unittest +from typing import Tuple, Sequence, List +from test.python.algorithms import QiskitAlgorithmsTestCase +import numpy as np +from ddt import ddt, data + +from qiskit.algorithms.observables_evaluator import eval_observables +from qiskit.primitives import Estimator +from qiskit.quantum_info import Statevector +from qiskit import QuantumCircuit +from qiskit.circuit.library import EfficientSU2 +from qiskit.opflow import ( + PauliSumOp, + OperatorBase, +) +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 + algorithm_globals.random_seed = self.seed + + self.threshold = 1e-8 + + def get_exact_expectation(self, ansatz: QuantumCircuit, observables: Sequence[OperatorBase]): + """ + Calculates the exact expectation to be used as an expected result for unit tests. + """ + + # 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 + ] + + return exact + + def _run_test( + self, + expected_result: List[Tuple[complex, complex]], + quantum_state: Sequence[QuantumCircuit], + decimal: int, + observables: Sequence[OperatorBase], + estimator: Estimator, + ): + result = eval_observables(estimator, quantum_state, observables, self.threshold) + + 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)]), + ], + ) + def test_eval_observables(self, observables: Sequence[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.bind_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, + ) + + +if __name__ == "__main__": + unittest.main() From 99adde79a0747bd174c7b877fae53beb66622a9f Mon Sep 17 00:00:00 2001 From: Dariusz Lasecki Date: Mon, 5 Sep 2022 15:32:02 +0200 Subject: [PATCH 14/48] Added ListOrDict support to observables_evaluator.py. --- qiskit/algorithms/observables_evaluator.py | 45 +++++++++++------- .../algorithms/test_observables_evaluator.py | 46 ++++++++++++++----- 2 files changed, 64 insertions(+), 27 deletions(-) diff --git a/qiskit/algorithms/observables_evaluator.py b/qiskit/algorithms/observables_evaluator.py index 0a351d9c73aa..18fa2830ff8f 100644 --- a/qiskit/algorithms/observables_evaluator.py +++ b/qiskit/algorithms/observables_evaluator.py @@ -11,7 +11,8 @@ # that they have been altered from the originals. """Evaluator of auxiliary operators for algorithms.""" from __future__ import annotations -from typing import Tuple, List, Sequence + +from typing import Tuple, List import numpy as np @@ -19,6 +20,8 @@ from qiskit.opflow import ( PauliSumOp, ) +from . import AlgorithmError +from .list_or_dict import ListOrDict from ..primitives import EstimatorResult, BaseEstimator from ..quantum_info.operators.base_operator import BaseOperator @@ -26,9 +29,9 @@ def eval_observables( estimator: BaseEstimator, quantum_state: QuantumCircuit, - observables: Sequence[BaseOperator | PauliSumOp], + observables: ListOrDict[BaseOperator | PauliSumOp], threshold: float = 1e-12, -) -> List[Tuple[complex, complex]]: +) -> ListOrDict[Tuple[complex, complex]]: """ Accepts a sequence of operators and calculates their expectation values - means and standard deviations. They are calculated with respect to a quantum state provided. A user @@ -38,15 +41,17 @@ def eval_observables( estimator: An estimator primitive used for calculations. quantum_state: An unparametrized quantum circuit representing a quantum state that expectation values are computed against. - observables: A sequence of operators whose expectation values are to be calculated. + observables: A list or a dictionary of operators whose expectation values are to be + calculated. threshold: A threshold value that defines which mean values should be neglected (helpful for ignoring numerical instabilities close to 0). Returns: - A list of tuples (mean, standard deviation). + A list or a dictionary of tuples (mean, standard deviation). Raises: ValueError: If a ``quantum_state`` with free parameters is provided. + AlgorithmError: If a primitive job is not successful. """ if ( @@ -57,12 +62,17 @@ def eval_observables( "A parametrized representation of a quantum_state was provided. It is not " "allowed - it cannot have free parameters." ) - + if isinstance(observables, dict): + observables_list = list(observables.values()) + else: + observables_list = observables quantum_state = [quantum_state] * len(observables) - estimator_job = estimator.run(quantum_state, observables) - expectation_values = estimator_job.result().values + try: + estimator_job = estimator.run(quantum_state, observables_list) + expectation_values = estimator_job.result().values + except Exception as exc: + raise AlgorithmError("The primitive job failed!") from exc - # compute standard deviations std_devs = _compute_std_devs(estimator_job, len(expectation_values)) # Discard values below threshold @@ -71,29 +81,32 @@ def eval_observables( observables_results = list(zip(observables_means, std_devs)) # Return None eigenvalues for None operators if observables is a list. - return _prepare_result(observables_results, observables) def _prepare_result( observables_results: List[Tuple[complex, complex]], - observables: Sequence[BaseOperator], -) -> List[Tuple[complex, complex]]: + observables: ListOrDict[BaseOperator | PauliSumOp], +) -> ListOrDict[Tuple[complex, complex]]: """ Prepares a list of eigenvalues and standard deviations from ``observables_results`` and ``observables``. Args: observables_results: A list of of tuples (mean, standard deviation). - observables: A list of operators whose expectation values are to be + observables: A list or a dictionary of operators whose expectation values are to be calculated. Returns: - A list of tuples (mean, standard deviation). + A list or a dictionary of tuples (mean, standard deviation). """ - observables_eigenvalues = [None] * len(observables) - key_value_iterator = enumerate(observables_results) + if isinstance(observables, list): + observables_eigenvalues = [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: diff --git a/test/python/algorithms/test_observables_evaluator.py b/test/python/algorithms/test_observables_evaluator.py index f9716fb0ee64..74ccee63bc9a 100644 --- a/test/python/algorithms/test_observables_evaluator.py +++ b/test/python/algorithms/test_observables_evaluator.py @@ -10,13 +10,16 @@ # copyright notice, and modified files need to carry a notice indicating # that they have been altered from the originals. """Tests evaluator of auxiliary operators for algorithms.""" - +from __future__ import annotations import unittest -from typing import Tuple, Sequence, List +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.observables_evaluator import eval_observables from qiskit.primitives import Estimator from qiskit.quantum_info import Statevector @@ -24,7 +27,6 @@ from qiskit.circuit.library import EfficientSU2 from qiskit.opflow import ( PauliSumOp, - OperatorBase, ) from qiskit.utils import algorithm_globals @@ -40,29 +42,44 @@ def setUp(self): self.threshold = 1e-8 - def get_exact_expectation(self, ansatz: QuantumCircuit, observables: Sequence[OperatorBase]): + 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) where we expect 0 variance exact = [ - (Statevector(ansatz).expectation_value(observable), 0) for observable in observables + (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: List[Tuple[complex, complex]], - quantum_state: Sequence[QuantumCircuit], + expected_result: ListOrDict[Tuple[complex, complex]], + quantum_state: QuantumCircuit, decimal: int, - observables: Sequence[OperatorBase], + observables: ListOrDict[BaseOperator | PauliSumOp], estimator: Estimator, ): result = eval_observables(estimator, quantum_state, observables, self.threshold) - np.testing.assert_array_almost_equal(result, expected_result, decimal=decimal) + 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( [ @@ -72,8 +89,15 @@ def _run_test( [ 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: Sequence[OperatorBase]): + def test_eval_observables(self, observables: ListOrDict[BaseOperator | PauliSumOp]): """Tests evaluator of auxiliary operators for algorithms.""" ansatz = EfficientSU2(2) From da2cdfa231a5ada04f7d087e57b2a1580658234f Mon Sep 17 00:00:00 2001 From: Dariusz Lasecki Date: Mon, 5 Sep 2022 21:27:04 +0200 Subject: [PATCH 15/48] Drafted pvqd.py with the fidelity primitive; type hints updated. --- qiskit/algorithms/time_evolvers/pvqd/pvqd.py | 52 ++++++++++--------- .../time_evolvers/pvqd/pvqd_result.py | 19 ++++--- qiskit/algorithms/time_evolvers/pvqd/utils.py | 19 +++---- 3 files changed, 45 insertions(+), 45 deletions(-) diff --git a/qiskit/algorithms/time_evolvers/pvqd/pvqd.py b/qiskit/algorithms/time_evolvers/pvqd/pvqd.py index adaa676b1b8c..a5188c9c1594 100644 --- a/qiskit/algorithms/time_evolvers/pvqd/pvqd.py +++ b/qiskit/algorithms/time_evolvers/pvqd/pvqd.py @@ -12,9 +12,9 @@ """The projected Variational Quantum Dynamics Algorithm.""" -from typing import Optional, Union, List, Tuple, Callable - +from typing import List, Tuple, Callable import logging + import numpy as np from qiskit import QiskitError @@ -22,16 +22,16 @@ from qiskit.circuit import QuantumCircuit, ParameterVector from qiskit.circuit.library import PauliEvolutionGate from qiskit.extensions import HamiltonianGate -from qiskit.opflow import OperatorBase, CircuitSampler, ExpectationBase, StateFn, MatrixOp -from qiskit.primitives import Estimator +from qiskit.opflow import ExpectationBase, MatrixOp, PauliSumOp +from qiskit.primitives import BaseEstimator, BaseSampler +from qiskit.quantum_info.operators.base_operator import BaseOperator from qiskit.synthesis import EvolutionSynthesis, LieTrotter - from .pvqd_result import PVQDResult from .utils import _get_observable_evaluator, _is_gradient_supported - from ..evolution_problem import EvolutionProblem from ..evolution_result import EvolutionResult from ..real_evolver import RealEvolver +from ...state_fidelities import ComputeUncompute logger = logging.getLogger(__name__) @@ -116,12 +116,13 @@ def __init__( ansatz: QuantumCircuit, initial_parameters: np.ndarray, expectation: ExpectationBase, - estimator: Estimator, - optimizer: Optional[Union[Optimizer, Minimizer]] = None, - num_timesteps: Optional[int] = None, - evolution: Optional[EvolutionSynthesis] = None, + sampler: BaseSampler, + 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: Optional[np.ndarray] = None, + initial_guess: np.ndarray | None = None, ) -> None: """ Args: @@ -135,7 +136,8 @@ def __init__( 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_timestep: The number of time steps. If ``None`` it will be set such that the timestep + num_timestep: 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 @@ -157,12 +159,13 @@ def __init__( self.initial_guess = initial_guess self.expectation = expectation self.estimator = estimator + self.sampler = sampler self.evolution = evolution self.use_parameter_shift = use_parameter_shift def step( self, - hamiltonian: OperatorBase, + hamiltonian: BaseOperator | PauliSumOp, ansatz: QuantumCircuit, theta: np.ndarray, dt: float, @@ -203,11 +206,11 @@ def step( def get_loss( self, - hamiltonian: OperatorBase, + hamiltonian: BaseOperator | PauliSumOp, ansatz: QuantumCircuit, dt: float, current_parameters: np.ndarray, - ) -> Tuple[Callable[[np.ndarray], float], Optional[Callable[[np.ndarray], 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. @@ -237,13 +240,8 @@ def get_loss( # define the overlap of the Trotterized state and the ansatz x = ParameterVector("w", ansatz.num_parameters) shifted = ansatz.assign_parameters(current_parameters + x) - overlap = StateFn(trotterized).adjoint() @ StateFn(shifted) - - converted = self.expectation.convert(overlap) - def evaluate_loss( - displacement: Union[np.ndarray, List[np.ndarray]] - ) -> Union[float, List[float]]: + def evaluate_loss(displacement: np.ndarray | List[np.ndarray]) -> float | List[float]: """Evaluate the overlap of the ansatz with the Trotterized evolution. Args: @@ -258,12 +256,13 @@ def evaluate_loss( else: value_dict = dict(zip(x, displacement)) - # TODO fidelity primitive - sampled = self._sampler.convert(converted, params=value_dict) + fidelity = ComputeUncompute(self.sampler) + job = fidelity.run([trotterized], [shifted], None, list(value_dict.values())) + fidelity = job.result().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 - np.abs(sampled.eval()) ** 2 + return 1 - np.abs(fidelity.eval()) ** 2 if _is_gradient_supported(ansatz) and self.use_parameter_shift: @@ -307,6 +306,7 @@ def evolve(self, evolution_problem: EvolutionProblem) -> EvolutionResult: Raises: ValueError: If the evolution time is not positive or the timestep is too small. NotImplementedError: If the evolution problem contains an initial state. + Warning: If observables provided but an estimator not provided. """ self._validate_setup() @@ -327,6 +327,10 @@ def evolve(self, evolution_problem: EvolutionProblem) -> EvolutionResult: # 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 Warning( + "Observables provided to be estimated but the estimator was not provided. " + ) evaluate_observables = _get_observable_evaluator( self.ansatz, observables, self.estimator ) diff --git a/qiskit/algorithms/time_evolvers/pvqd/pvqd_result.py b/qiskit/algorithms/time_evolvers/pvqd/pvqd_result.py index 2c562f3262a8..631e73907392 100644 --- a/qiskit/algorithms/time_evolvers/pvqd/pvqd_result.py +++ b/qiskit/algorithms/time_evolvers/pvqd/pvqd_result.py @@ -12,12 +12,11 @@ """Result object for p-VQD.""" -from typing import Union, Optional, List, Tuple +from typing import List, Tuple + import numpy as np from qiskit.circuit import QuantumCircuit -from qiskit.opflow import StateFn, OperatorBase - from ..evolution_result import EvolutionResult @@ -26,13 +25,13 @@ class PVQDResult(EvolutionResult): def __init__( self, - evolved_state: Union[StateFn, QuantumCircuit], - aux_ops_evaluated: Optional[List[Tuple[complex, complex]]] = None, - times: Optional[List[float]] = None, - parameters: Optional[List[np.ndarray]] = None, - fidelities: Optional[List[float]] = None, - estimated_error: Optional[float] = None, - observables: Optional[List[List[float]]] = None, + evolved_state: QuantumCircuit, + aux_ops_evaluated: List[Tuple[complex, complex]] | None = None, + times: List[float] | None = None, + parameters: List[np.ndarray] | None = None, + fidelities: List[float] | None = None, + estimated_error: float | None = None, + observables: List[List[float]] | None = None, ): """ Args: diff --git a/qiskit/algorithms/time_evolvers/pvqd/utils.py b/qiskit/algorithms/time_evolvers/pvqd/utils.py index 31db6b500af5..1d61c692ad09 100644 --- a/qiskit/algorithms/time_evolvers/pvqd/utils.py +++ b/qiskit/algorithms/time_evolvers/pvqd/utils.py @@ -12,8 +12,8 @@ """Utilities for p-VQD.""" - -from typing import Union, List, Callable +from __future__ import annotations +from typing import List, Callable import logging import numpy as np @@ -21,9 +21,9 @@ from qiskit.circuit import QuantumCircuit, Parameter, ParameterExpression from qiskit.compiler import transpile from qiskit.exceptions import QiskitError -from qiskit.opflow import ListOp, CircuitSampler, ExpectationBase, StateFn, OperatorBase from qiskit.opflow.gradients.circuit_gradients import ParamShift -from qiskit.primitives import Estimator +from qiskit.primitives import BaseEstimator +from qiskit.quantum_info.operators.base_operator import BaseOperator logger = logging.getLogger(__name__) @@ -71,17 +71,14 @@ def _is_gradient_supported(ansatz: QuantumCircuit) -> bool: def _get_observable_evaluator( ansatz: QuantumCircuit, - observables: Union[OperatorBase, List[OperatorBase]], - estimator: Estimator, -) -> Callable[[np.ndarray], Union[float, List[float]]]: + 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.""" - if isinstance(observables, list): - observables = ListOp(observables) - ansatz_parameters = ansatz.parameter - def evaluate_observables(theta: np.ndarray) -> Union[float, List[float]]: + def evaluate_observables(theta: np.ndarray) -> float | List[float]: """Evaluate the observables for the ansatz parameters ``theta``. Args: From e7def3dc1779cc3f6d8d0279c927f5bcc957745d Mon Sep 17 00:00:00 2001 From: Dariusz Lasecki Date: Tue, 6 Sep 2022 11:46:30 +0200 Subject: [PATCH 16/48] Accepting fidelity primitive. --- qiskit/algorithms/time_evolvers/pvqd/pvqd.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/qiskit/algorithms/time_evolvers/pvqd/pvqd.py b/qiskit/algorithms/time_evolvers/pvqd/pvqd.py index a5188c9c1594..17cee4105533 100644 --- a/qiskit/algorithms/time_evolvers/pvqd/pvqd.py +++ b/qiskit/algorithms/time_evolvers/pvqd/pvqd.py @@ -31,7 +31,7 @@ from ..evolution_problem import EvolutionProblem from ..evolution_result import EvolutionResult from ..real_evolver import RealEvolver -from ...state_fidelities import ComputeUncompute +from ...state_fidelities import ComputeUncompute, BaseStateFidelity logger = logging.getLogger(__name__) @@ -116,7 +116,7 @@ def __init__( ansatz: QuantumCircuit, initial_parameters: np.ndarray, expectation: ExpectationBase, - sampler: BaseSampler, + fidelity_primitive: BaseStateFidelity, estimator: BaseEstimator | None = None, optimizer: Optimizer | Minimizer | None = None, num_timesteps: int | None = None, @@ -159,7 +159,7 @@ def __init__( self.initial_guess = initial_guess self.expectation = expectation self.estimator = estimator - self.sampler = sampler + self.fidelity_primitive = fidelity_primitive self.evolution = evolution self.use_parameter_shift = use_parameter_shift @@ -256,8 +256,9 @@ def evaluate_loss(displacement: np.ndarray | List[np.ndarray]) -> float | List[f else: value_dict = dict(zip(x, displacement)) - fidelity = ComputeUncompute(self.sampler) - job = fidelity.run([trotterized], [shifted], None, list(value_dict.values())) + job = self.fidelity_primitive.run( + [trotterized], [shifted], None, list(value_dict.values()) + ) fidelity = job.result().fidelities[0] # in principle we could add different loss functions here, but we're currently From 6462bb03f583ac4c0a936ef6814411c51870f3e1 Mon Sep 17 00:00:00 2001 From: Dariusz Lasecki Date: Tue, 6 Sep 2022 11:51:45 +0200 Subject: [PATCH 17/48] Included CR suggestions. --- qiskit/algorithms/observables_evaluator.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/qiskit/algorithms/observables_evaluator.py b/qiskit/algorithms/observables_evaluator.py index 18fa2830ff8f..43d439088339 100644 --- a/qiskit/algorithms/observables_evaluator.py +++ b/qiskit/algorithms/observables_evaluator.py @@ -55,7 +55,7 @@ def eval_observables( """ if ( - isinstance(quantum_state, QuantumCircuit) # Statevector cannot be parametrized + isinstance(quantum_state, QuantumCircuit) # State cannot be parametrized and len(quantum_state.parameters) > 0 ): raise ValueError( @@ -119,7 +119,9 @@ def _compute_std_devs( results_length: int, ) -> List[complex | None]: """ - Calculates a list of standard deviations from expectation values of observables provided. + Calculates a list of standard deviations from expectation values of observables provided. If + the choice of an underlying hardware is not shot-based and hence does not provide variance data, + the standard deviation values will be set to ``None``. Args: estimator_result: An estimator result. @@ -138,6 +140,6 @@ def _compute_std_devs( shots = metadata["shots"] std_devs.append(np.sqrt(variance / shots)) else: - std_devs.append(0) + std_devs.append(None) return std_devs From 604944c94f3eb691056282192d8b149a3b6317ed Mon Sep 17 00:00:00 2001 From: Dariusz Lasecki Date: Tue, 6 Sep 2022 20:19:27 +0200 Subject: [PATCH 18/48] Applied some CR comments. --- qiskit/algorithms/time_evolvers/evolution_problem.py | 10 ++++++---- qiskit/algorithms/time_evolvers/evolution_result.py | 3 +-- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/qiskit/algorithms/time_evolvers/evolution_problem.py b/qiskit/algorithms/time_evolvers/evolution_problem.py index 9db122f96268..f33af102351a 100644 --- a/qiskit/algorithms/time_evolvers/evolution_problem.py +++ b/qiskit/algorithms/time_evolvers/evolution_problem.py @@ -12,12 +12,12 @@ """Evolution problem class.""" from __future__ import annotations -from typing import Dict from qiskit import QuantumCircuit -from qiskit.circuit import Parameter +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 @@ -36,7 +36,7 @@ def __init__( aux_operators: ListOrDict[BaseOperator | PauliSumOp] | None = None, truncation_threshold: float = 1e-12, t_param: Parameter | None = None, - param_value_dict: Dict[Parameter, complex] + param_value_dict: dict[Parameter, complex] | None = None, # parametrization will become supported in BaseOperator soon ): """ @@ -92,5 +92,7 @@ def validate_params(self) -> None: Raises: ValueError: If Hamiltonian parameters cannot be bound with data provided. """ - if isinstance(self.hamiltonian, PauliSumOp) and self.hamiltonian.parameters: + 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/evolution_result.py b/qiskit/algorithms/time_evolvers/evolution_result.py index e89c3dba52e8..6d6730057871 100644 --- a/qiskit/algorithms/time_evolvers/evolution_result.py +++ b/qiskit/algorithms/time_evolvers/evolution_result.py @@ -12,7 +12,6 @@ """Class for holding evolution result.""" from __future__ import annotations -from typing import Tuple from qiskit import QuantumCircuit from qiskit.algorithms.list_or_dict import ListOrDict @@ -25,7 +24,7 @@ class EvolutionResult(AlgorithmResult): def __init__( self, evolved_state: QuantumCircuit, - aux_ops_evaluated: ListOrDict[Tuple[complex, complex]] | None = None, + aux_ops_evaluated: ListOrDict[tuple[complex, complex]] | None = None, ): """ Args: From d287c2f5b06987fbce21b0feb9640e9448f768d8 Mon Sep 17 00:00:00 2001 From: Dariusz Lasecki Date: Tue, 6 Sep 2022 20:28:58 +0200 Subject: [PATCH 19/48] Applied some CR comments. --- qiskit/algorithms/observables_evaluator.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/qiskit/algorithms/observables_evaluator.py b/qiskit/algorithms/observables_evaluator.py index 43d439088339..855a54f3ad79 100644 --- a/qiskit/algorithms/observables_evaluator.py +++ b/qiskit/algorithms/observables_evaluator.py @@ -120,8 +120,8 @@ def _compute_std_devs( ) -> List[complex | None]: """ Calculates a list of standard deviations from expectation values of observables provided. If - the choice of an underlying hardware is not shot-based and hence does not provide variance data, - the standard deviation values will be set to ``None``. + there is no variance data available from a primitive, the standard deviation values will be set + to ``None``. Args: estimator_result: An estimator result. @@ -138,8 +138,11 @@ def _compute_std_devs( if metadata and "variance" in metadata.keys() and "shots" in metadata.keys(): variance = metadata["variance"] shots = metadata["shots"] - std_devs.append(np.sqrt(variance / shots)) + if variance is None or shots is None: + std_devs.append(None) + else: + std_devs.append(np.sqrt(variance / shots)) else: - std_devs.append(None) + std_devs.append(0) return std_devs From 23821e0c45843d4978bd1fa21da2e6679780fb18 Mon Sep 17 00:00:00 2001 From: Dariusz Lasecki Date: Wed, 7 Sep 2022 10:07:10 +0200 Subject: [PATCH 20/48] Added reno. --- ...lution-framework-primitives-c86779b5d0dffd25.yaml | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 releasenotes/notes/evolution-framework-primitives-c86779b5d0dffd25.yaml diff --git a/releasenotes/notes/evolution-framework-primitives-c86779b5d0dffd25.yaml b/releasenotes/notes/evolution-framework-primitives-c86779b5d0dffd25.yaml new file mode 100644 index 000000000000..1ea0b3485541 --- /dev/null +++ b/releasenotes/notes/evolution-framework-primitives-c86779b5d0dffd25.yaml @@ -0,0 +1,12 @@ +--- +features: + - | + Added `~qiskit.algorithms.time_evolvers` package with interfaces that will cover + primitive-enabled time evolution algorithms: + :class:`~qiskit.algorithms.time_evolvers.EvolutionProblem`, + :class:`~qiskit.algorithms.time_evolvers.EvolutionResult`, + :class:`~qiskit.algorithms.time_evolvers.ImaginaryEvolver`, + :class:`~qiskit.algorithms.time_evolvers.RealEvolver`. +deprecations: + - | + `~qiskit.algorithms.evolvers` package will now issue a ``PendingDeprecationWarning``. From 80a2e2bae2e4eb6d84ae44b13a4a86a71c58a81e Mon Sep 17 00:00:00 2001 From: Dariusz Lasecki Date: Wed, 7 Sep 2022 10:14:02 +0200 Subject: [PATCH 21/48] Added reno. --- .../observable-eval-primitives-e1fd989e15c7760c.yaml | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 releasenotes/notes/observable-eval-primitives-e1fd989e15c7760c.yaml diff --git a/releasenotes/notes/observable-eval-primitives-e1fd989e15c7760c.yaml b/releasenotes/notes/observable-eval-primitives-e1fd989e15c7760c.yaml new file mode 100644 index 000000000000..a7e30f4fc8b1 --- /dev/null +++ b/releasenotes/notes/observable-eval-primitives-e1fd989e15c7760c.yaml @@ -0,0 +1,9 @@ +--- +features: + - | + Added `~qiskit.algorithms.observables_evaluator` with + :class:`~qiskit.primitives.BaseEstimator` as ``init`` parameter. It will soon replace + `~qiskit.algorithms.aux_ops_evaluator`. +deprecations: + - | + Using `~qiskit.algorithms.aux_ops_evaluator` will now issue a ``PendingDeprecationWarning``. From a2960b2bb36a9346160baa92c5696da8266278c5 Mon Sep 17 00:00:00 2001 From: Dariusz Lasecki Date: Wed, 7 Sep 2022 10:26:34 +0200 Subject: [PATCH 22/48] Accepting Statevector. --- qiskit/algorithms/time_evolvers/evolution_problem.py | 6 +++++- .../time_evolvers/test_evolution_problem.py | 12 ++++++------ 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/qiskit/algorithms/time_evolvers/evolution_problem.py b/qiskit/algorithms/time_evolvers/evolution_problem.py index f33af102351a..ee2cb4282cee 100644 --- a/qiskit/algorithms/time_evolvers/evolution_problem.py +++ b/qiskit/algorithms/time_evolvers/evolution_problem.py @@ -32,7 +32,7 @@ def __init__( self, hamiltonian: BaseOperator | PauliSumOp, time: float, - initial_state: QuantumCircuit | None = None, + initial_state: QuantumCircuit | Statevector | None = None, aux_operators: ListOrDict[BaseOperator | PauliSumOp] | None = None, truncation_threshold: float = 1e-12, t_param: Parameter | None = None, @@ -63,6 +63,10 @@ def __init__( self.param_value_dict = param_value_dict 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 = initial_state self.aux_operators = aux_operators self.truncation_threshold = truncation_threshold diff --git a/test/python/algorithms/time_evolvers/test_evolution_problem.py b/test/python/algorithms/time_evolvers/test_evolution_problem.py index 76683e27cd13..315245b20ebd 100644 --- a/test/python/algorithms/time_evolvers/test_evolution_problem.py +++ b/test/python/algorithms/time_evolvers/test_evolution_problem.py @@ -12,12 +12,12 @@ """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 import QuantumCircuit from qiskit.algorithms.time_evolvers.evolution_problem import EvolutionProblem -from qiskit.quantum_info import Pauli, SparsePauliOp +from qiskit.quantum_info import Pauli, SparsePauliOp, Statevector from qiskit.circuit import Parameter from qiskit.opflow import Y, Z, One, X, Zero, PauliSumOp @@ -48,12 +48,12 @@ def test_init_default(self): 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): + @data(QuantumCircuit(1), Statevector([1, 0])) + def test_init_all(self, initial_state): """Tests that all fields are initialized correctly.""" t_parameter = Parameter("t") hamiltonian = t_parameter * Z + Y time = 2 - initial_state = One aux_operators = [X, Y] param_value_dict = {t_parameter: 3.2} @@ -68,14 +68,14 @@ def test_init_all(self): expected_hamiltonian = Y + t_parameter * Z expected_time = 2 - expected_initial_state = One + expected_type = QuantumCircuit expected_aux_operators = [X, Y] expected_t_param = t_parameter expected_param_value_dict = {t_parameter: 3.2} 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(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_dict, expected_param_value_dict) From 9b5f50a4348f1c9090a982465dff8e3f493c2e57 Mon Sep 17 00:00:00 2001 From: Dariusz Lasecki Date: Wed, 7 Sep 2022 10:36:37 +0200 Subject: [PATCH 23/48] Added attributes docs. --- .../time_evolvers/evolution_problem.py | 20 ++++++++++++++++++- .../time_evolvers/evolution_result.py | 10 +++++++++- 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/qiskit/algorithms/time_evolvers/evolution_problem.py b/qiskit/algorithms/time_evolvers/evolution_problem.py index ee2cb4282cee..8b517c4e04ed 100644 --- a/qiskit/algorithms/time_evolvers/evolution_problem.py +++ b/qiskit/algorithms/time_evolvers/evolution_problem.py @@ -13,6 +13,8 @@ """Evolution problem class.""" from __future__ import annotations +from typing import Mapping + from qiskit import QuantumCircuit from qiskit.circuit import Parameter, ParameterExpression from qiskit.opflow import PauliSumOp @@ -26,6 +28,22 @@ class EvolutionProblem: 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_dict (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__( @@ -36,7 +54,7 @@ def __init__( aux_operators: ListOrDict[BaseOperator | PauliSumOp] | None = None, truncation_threshold: float = 1e-12, t_param: Parameter | None = None, - param_value_dict: dict[Parameter, complex] + param_value_dict: Mapping[Parameter, complex] | None = None, # parametrization will become supported in BaseOperator soon ): """ diff --git a/qiskit/algorithms/time_evolvers/evolution_result.py b/qiskit/algorithms/time_evolvers/evolution_result.py index 6d6730057871..d049e68da58d 100644 --- a/qiskit/algorithms/time_evolvers/evolution_result.py +++ b/qiskit/algorithms/time_evolvers/evolution_result.py @@ -19,7 +19,15 @@ class EvolutionResult(AlgorithmResult): - """Class for holding evolution result.""" + """ + Class for holding evolution result. + + Attributes: + evolved_state (QuantumCircuit): 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). + """ def __init__( self, From 4c40e0017575d8468053fdcc3eed039ead28b75e Mon Sep 17 00:00:00 2001 From: Dariusz Lasecki Date: Wed, 7 Sep 2022 21:45:36 +0200 Subject: [PATCH 24/48] Support for 0 operator. --- qiskit/algorithms/observables_evaluator.py | 14 ++++++++++++++ .../algorithms/test_observables_evaluator.py | 18 ++++++++++++++++++ 2 files changed, 32 insertions(+) diff --git a/qiskit/algorithms/observables_evaluator.py b/qiskit/algorithms/observables_evaluator.py index 855a54f3ad79..cc6368c34672 100644 --- a/qiskit/algorithms/observables_evaluator.py +++ b/qiskit/algorithms/observables_evaluator.py @@ -66,6 +66,8 @@ def eval_observables( observables_list = list(observables.values()) else: observables_list = observables + + observables_list = _handle_zero_ops(observables_list) quantum_state = [quantum_state] * len(observables) try: estimator_job = estimator.run(quantum_state, observables_list) @@ -84,6 +86,18 @@ def eval_observables( 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.""" + zero_op = PauliSumOp.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, complex]], observables: ListOrDict[BaseOperator | PauliSumOp], diff --git a/test/python/algorithms/test_observables_evaluator.py b/test/python/algorithms/test_observables_evaluator.py index 74ccee63bc9a..23e3cbda3d2a 100644 --- a/test/python/algorithms/test_observables_evaluator.py +++ b/test/python/algorithms/test_observables_evaluator.py @@ -27,6 +27,8 @@ from qiskit.circuit.library import EfficientSU2 from qiskit.opflow import ( PauliSumOp, + X, + Y, ) from qiskit.utils import algorithm_globals @@ -119,6 +121,22 @@ def test_eval_observables(self, observables: ListOrDict[BaseOperator | PauliSumO estimator, ) + def test_eval_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.bind_parameters(parameters) + state = bound_ansatz + estimator = Estimator() + observables = [(X ^ X) + (Y ^ Y), 0] + result = eval_observables(estimator, state, observables, self.threshold) + expected_result = [(0.015607318055509564, 0), (0.0, 0)] + np.testing.assert_array_almost_equal(result, expected_result) + if __name__ == "__main__": unittest.main() From cfe44477de7f644954693f6ad3cba1b33c48d5bd Mon Sep 17 00:00:00 2001 From: Dariusz Lasecki Date: Thu, 8 Sep 2022 12:09:43 +0200 Subject: [PATCH 25/48] Implemented pvqd unit tests with primitives; code refactoring. --- qiskit/algorithms/time_evolvers/pvqd/pvqd.py | 60 ++-- .../time_evolvers/pvqd/pvqd_result.py | 13 +- qiskit/algorithms/time_evolvers/pvqd/utils.py | 29 +- .../algorithms/time_evolvers/__init__.py | 11 + .../algorithms/time_evolvers/test_pvqd.py | 313 ++++++++++++++++++ 5 files changed, 383 insertions(+), 43 deletions(-) create mode 100644 test/python/algorithms/time_evolvers/__init__.py create mode 100644 test/python/algorithms/time_evolvers/test_pvqd.py diff --git a/qiskit/algorithms/time_evolvers/pvqd/pvqd.py b/qiskit/algorithms/time_evolvers/pvqd/pvqd.py index 17cee4105533..bb39ba11e7ab 100644 --- a/qiskit/algorithms/time_evolvers/pvqd/pvqd.py +++ b/qiskit/algorithms/time_evolvers/pvqd/pvqd.py @@ -11,19 +11,20 @@ # that they have been altered from the originals. """The projected Variational Quantum Dynamics Algorithm.""" +from __future__ import annotations -from typing import List, Tuple, Callable import logging +from typing import Callable import numpy as np from qiskit import QiskitError from qiskit.algorithms.optimizers import Optimizer, Minimizer -from qiskit.circuit import QuantumCircuit, ParameterVector +from qiskit.circuit import QuantumCircuit, ParameterVector, Parameter from qiskit.circuit.library import PauliEvolutionGate from qiskit.extensions import HamiltonianGate -from qiskit.opflow import ExpectationBase, MatrixOp, PauliSumOp -from qiskit.primitives import BaseEstimator, BaseSampler +from qiskit.opflow import MatrixOp, PauliSumOp +from qiskit.primitives import BaseEstimator from qiskit.quantum_info.operators.base_operator import BaseOperator from qiskit.synthesis import EvolutionSynthesis, LieTrotter from .pvqd_result import PVQDResult @@ -31,7 +32,7 @@ from ..evolution_problem import EvolutionProblem from ..evolution_result import EvolutionResult from ..real_evolver import RealEvolver -from ...state_fidelities import ComputeUncompute, BaseStateFidelity +from ...state_fidelities import BaseStateFidelity logger = logging.getLogger(__name__) @@ -51,7 +52,6 @@ class PVQD(RealEvolver): ansatz (QuantumCircuit): The parameterized circuit representing the time-evolved state. initial_parameters (np.ndarray): The parameters of the ansatz at time 0. - expectation (ExpectationBase): The method to compute expectation values. 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 @@ -74,10 +74,9 @@ class PVQD(RealEvolver): from qiskit import BasicAer from qiskit.circuit.library import EfficientSU2 - from qiskit.opflow import X, Z, I, MatrixExpectation + from qiskit.opflow import X, Z, I backend = BasicAer.get_backend("statevector_simulator") - expectation = MatrixExpectation() hamiltonian = 0.1 * (Z ^ Z) + (I ^ X) + (X ^ I) observable = Z ^ Z ansatz = EfficientSU2(2, reps=1) @@ -93,7 +92,6 @@ class PVQD(RealEvolver): num_timesteps=100, optimizer=optimizer, quantum_instance=backend, - expectation=expectation ) # specify the evolution problem @@ -115,7 +113,6 @@ def __init__( self, ansatz: QuantumCircuit, initial_parameters: np.ndarray, - expectation: ExpectationBase, fidelity_primitive: BaseStateFidelity, estimator: BaseEstimator | None = None, optimizer: Optimizer | Minimizer | None = None, @@ -130,7 +127,6 @@ def __init__( 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. - expectation: The expectation converter to evaluate expectation values. 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 @@ -157,7 +153,6 @@ def __init__( self.num_timesteps = num_timesteps self.optimizer = optimizer self.initial_guess = initial_guess - self.expectation = expectation self.estimator = estimator self.fidelity_primitive = fidelity_primitive self.evolution = evolution @@ -170,7 +165,7 @@ def step( theta: np.ndarray, dt: float, initial_guess: np.ndarray, - ) -> Tuple[np.ndarray, float]: + ) -> tuple[np.ndarray, float]: """Perform a single time step. Args: @@ -210,7 +205,7 @@ def get_loss( ansatz: QuantumCircuit, dt: float, current_parameters: np.ndarray, - ) -> Tuple[Callable[[np.ndarray], float], Callable[[np.ndarray], np.ndarray]] | None: + ) -> tuple[Callable[[np.ndarray], float], Callable[[np.ndarray], np.ndarray]] | None: """Get a function to evaluate the infidelity between Trotter step and ansatz. @@ -241,7 +236,7 @@ def get_loss( x = ParameterVector("w", ansatz.num_parameters) shifted = ansatz.assign_parameters(current_parameters + x) - def evaluate_loss(displacement: np.ndarray | List[np.ndarray]) -> float | List[float]: + def evaluate_loss(displacement: np.ndarray | list[np.ndarray]) -> float | list[float]: """Evaluate the overlap of the ansatz with the Trotterized evolution. Args: @@ -256,14 +251,20 @@ def evaluate_loss(displacement: np.ndarray | List[np.ndarray]) -> float | List[f else: value_dict = dict(zip(x, displacement)) - job = self.fidelity_primitive.run( - [trotterized], [shifted], None, list(value_dict.values()) - ) - fidelity = job.result().fidelities[0] - - # in principle we could add different loss functions here, but we're currently + 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 + job = self.fidelity_primitive.run(states1, states2, values_2=param_dicts2) + fidelities = job.result().fidelities + 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 - np.abs(fidelity.eval()) ** 2 + return 1 - np.abs(fidelities) ** 2 if _is_gradient_supported(ansatz) and self.use_parameter_shift: @@ -295,6 +296,19 @@ def evaluate_gradient(displacement: np.ndarray) -> np.ndarray: 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: EvolutionProblem) -> EvolutionResult: """ Args: @@ -378,7 +392,7 @@ def _validate_setup(self, skip=None): if skip is None: skip = {} - required_attributes = {"quantum_instance", "optimizer"}.difference(skip) + required_attributes = {"fidelity_primitive", "optimizer"}.difference(skip) for attr in required_attributes: if getattr(self, attr, None) is None: diff --git a/qiskit/algorithms/time_evolvers/pvqd/pvqd_result.py b/qiskit/algorithms/time_evolvers/pvqd/pvqd_result.py index 631e73907392..71b332c4e142 100644 --- a/qiskit/algorithms/time_evolvers/pvqd/pvqd_result.py +++ b/qiskit/algorithms/time_evolvers/pvqd/pvqd_result.py @@ -11,8 +11,7 @@ # that they have been altered from the originals. """Result object for p-VQD.""" - -from typing import List, Tuple +from __future__ import annotations import numpy as np @@ -26,12 +25,12 @@ class PVQDResult(EvolutionResult): 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: List[float] | None = None, + aux_ops_evaluated: list[tuple[complex, complex]] | None = None, + times: list[float] | None = None, + parameters: list[np.ndarray] | None = None, + fidelities: list[float] | None = None, estimated_error: float | None = None, - observables: List[List[float]] | None = None, + observables: list[list[float]] | None = None, ): """ Args: diff --git a/qiskit/algorithms/time_evolvers/pvqd/utils.py b/qiskit/algorithms/time_evolvers/pvqd/utils.py index 1d61c692ad09..27db241f3f37 100644 --- a/qiskit/algorithms/time_evolvers/pvqd/utils.py +++ b/qiskit/algorithms/time_evolvers/pvqd/utils.py @@ -13,8 +13,8 @@ """Utilities for p-VQD.""" from __future__ import annotations -from typing import List, Callable import logging +from typing import Callable import numpy as np @@ -71,14 +71,12 @@ def _is_gradient_supported(ansatz: QuantumCircuit) -> bool: def _get_observable_evaluator( ansatz: QuantumCircuit, - observables: BaseOperator | List[BaseOperator], + observables: BaseOperator | list[BaseOperator], estimator: BaseEstimator, -) -> Callable[[np.ndarray], float | List[float]]: +) -> Callable[[np.ndarray], float | list[float]]: """Get a callable to evaluate a (list of) observable(s) for given circuit parameters.""" - ansatz_parameters = ansatz.parameter - - def evaluate_observables(theta: np.ndarray) -> float | List[float]: + def evaluate_observables(theta: np.ndarray) -> float | list[float]: """Evaluate the observables for the ansatz parameters ``theta``. Args: @@ -87,12 +85,17 @@ def evaluate_observables(theta: np.ndarray) -> float | List[float]: Returns: The observables evaluated at the ansatz parameters. """ - value_dict = dict(zip(ansatz_parameters, theta)) - states = [ansatz] * len(observables) - - estimator_job = estimator.run( - states, observables, parameter_values=list(value_dict.values()) - ) - return estimator_job.result().values() + 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 + + estimator_job = estimator.run(states, obs, parameter_values=parameter_values) + + return estimator_job.result().values return evaluate_observables diff --git a/test/python/algorithms/time_evolvers/__init__.py b/test/python/algorithms/time_evolvers/__init__.py new file mode 100644 index 000000000000..fdb172d367f0 --- /dev/null +++ b/test/python/algorithms/time_evolvers/__init__.py @@ -0,0 +1,11 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2022. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. diff --git a/test/python/algorithms/time_evolvers/test_pvqd.py b/test/python/algorithms/time_evolvers/test_pvqd.py new file mode 100644 index 000000000000..53426fb282ef --- /dev/null +++ b/test/python/algorithms/time_evolvers/test_pvqd.py @@ -0,0 +1,313 @@ +# 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 +from functools import partial + +from ddt import ddt, data, unpack +import numpy as np + +from qiskit.algorithms.state_fidelities import ComputeUncompute +from qiskit.primitives import Sampler, Estimator +from qiskit.quantum_info import Pauli +from qiskit.test import QiskitTestCase +from qiskit import QiskitError +from qiskit.circuit import QuantumCircuit, Parameter, Gate +from qiskit.algorithms.evolvers import EvolutionProblem +from qiskit.algorithms.time_evolvers.pvqd import PVQD +from qiskit.algorithms.optimizers import L_BFGS_B, GradientDescent, SPSA, OptimizerResult +from qiskit.circuit.library import EfficientSU2 +from qiskit.opflow import X, Z, I + + +# 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(QiskitTestCase): + """Tests for the pVQD algorithm.""" + + def setUp(self): + super().setUp() + self.hamiltonian = 0.1 * (Z ^ Z) + (I ^ X) + (X ^ I) + self.observable = Pauli("ZZ") + self.ansatz = EfficientSU2(2, reps=1) + self.initial_parameters = np.zeros(self.ansatz.num_parameters) + + @data( + ("ising", True, 2), + ("pauli", False, None), + ) + @unpack + def test_pvqd(self, hamiltonian_type, gradient, num_timesteps): + """Test a simple evolution.""" + time = 0.02 + + if hamiltonian_type == "ising": + hamiltonian = 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( + self.ansatz, + self.initial_parameters, + fidelity_primitive, + estimator, + num_timesteps=num_timesteps, + optimizer=optimizer, + ) + problem = EvolutionProblem(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( + self.ansatz, + self.initial_parameters, + fidelity_primitive, + 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.to_matrix_op(), + 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( + self.ansatz, + self.initial_parameters, + fidelity_primitive, + 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( + self.ansatz, + self.initial_parameters, + fidelity_primitive, + estimator, + num_timesteps=0, + optimizer=L_BFGS_B(), + ) + problem = EvolutionProblem( + 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( + self.ansatz, + self.initial_parameters, + fidelity_primitive, + estimator, + num_timesteps=10, + optimizer=SPSA(maxiter=0, learning_rate=0.1, perturbation=0.01), + initial_guess=initial_guess, + ) + problem = EvolutionProblem( + 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 = EvolutionProblem(self.hamiltonian, time=0.02) + sampler = Sampler() + fidelity_primitive = ComputeUncompute(sampler) + + pvqd = PVQD( + QuantumCircuit(2), + np.array([]), + fidelity_primitive, + 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 = EvolutionProblem( + self.hamiltonian, + time=0.02, + initial_state=initial_state, + ) + + sampler = Sampler() + fidelity_primitive = ComputeUncompute(sampler) + + pvqd = PVQD( + self.ansatz, + self.initial_parameters, + fidelity_primitive, + optimizer=SPSA(maxiter=0, learning_rate=0.1, perturbation=0.01), + ) + + with self.assertRaises(NotImplementedError): + _ = pvqd.evolve(problem) + + +class TestPVQDUtils(QiskitTestCase): + """Test some utility functions for PVQD.""" + + def setUp(self): + super().setUp() + self.hamiltonian = 0.1 * (Z ^ Z) + (I ^ X) + (X ^ I) + 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( + ansatz=None, + initial_parameters=np.array([]), + fidelity_primitive=fidelity_primitive, + estimator=estimator, + optimizer=optimizer, + ) + problem = EvolutionProblem(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() From cb036745ea4c13f958748b01026576d7763a2b0c Mon Sep 17 00:00:00 2001 From: Dariusz Lasecki Date: Thu, 8 Sep 2022 12:51:14 +0200 Subject: [PATCH 26/48] Added reno. --- ...dynamics-primitives-6003336d0866ca19.yaml} | 22 ++++++++++++------- 1 file changed, 14 insertions(+), 8 deletions(-) rename releasenotes/notes/{project-dynamics-2f848a5f89655429.yaml => project-dynamics-primitives-6003336d0866ca19.yaml} (61%) diff --git a/releasenotes/notes/project-dynamics-2f848a5f89655429.yaml b/releasenotes/notes/project-dynamics-primitives-6003336d0866ca19.yaml similarity index 61% rename from releasenotes/notes/project-dynamics-2f848a5f89655429.yaml rename to releasenotes/notes/project-dynamics-primitives-6003336d0866ca19.yaml index a33fdfefed28..9ed0c24f2304 100644 --- a/releasenotes/notes/project-dynamics-2f848a5f89655429.yaml +++ b/releasenotes/notes/project-dynamics-primitives-6003336d0866ca19.yaml @@ -1,7 +1,7 @@ features: - | - Added the :class:`PVQD` class to the time evolution framework. This class implements the - projected Variational Quantum Dynamics (p-VQD) algorithm as :class:`.PVQD` of + Added the primitives-enabled :class:`PVQD` class to the time evolution framework. This class + implements the projected Variational Quantum Dynamics (p-VQD) algorithm as :class:`.PVQD` of `Barison et al. `_. In each timestep this algorithm computes the next state with a Trotter formula and projects it @@ -12,14 +12,20 @@ features: import numpy as np + from qiskit.algorithms.state_fidelities import ComputeUncompute + from qiskit.algorithms.time_evolvers.pvqd import PVQD + from qiskit.primitives import Estimator from qiskit import BasicAer from qiskit.circuit.library import EfficientSU2 - from qiskit.opflow import X, Z, I, MatrixExpectation + from qiskit.opflow import X, Z, I + from qiskit.quantum_info import Pauli + from qiskit.algorithms.optimizers import L_BFGS_B - backend = BasicAer.get_backend("statevector_simulator") - expectation = MatrixExpectation() + sampler = Sampler() + fidelity = ComputeUncompute(sampler) + estimator = Estimator() hamiltonian = 0.1 * (Z ^ Z) + (I ^ X) + (X ^ I) - observable = Z ^ Z + observable = Pauli("ZZ") ansatz = EfficientSU2(2, reps=1) initial_parameters = np.zeros(ansatz.num_parameters) @@ -29,11 +35,11 @@ features: # setup the algorithm pvqd = PVQD( ansatz, + fidelity, + estimator, initial_parameters, num_timesteps=100, optimizer=optimizer, - quantum_instance=backend, - expectation=expectation ) # specify the evolution problem From 6b5fcfb3a376035bff948984d3b30555fa0b07b5 Mon Sep 17 00:00:00 2001 From: Dariusz Lasecki Date: Thu, 8 Sep 2022 13:26:53 +0200 Subject: [PATCH 27/48] Updated algorithms init. --- qiskit/algorithms/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qiskit/algorithms/__init__.py b/qiskit/algorithms/__init__.py index c13f00cb1e60..8e8a134d3f8c 100644 --- a/qiskit/algorithms/__init__.py +++ b/qiskit/algorithms/__init__.py @@ -297,7 +297,7 @@ from .evolvers.variational.var_qite import VarQITE from .evolvers.variational.var_qrte import VarQRTE -from .evolvers.pvqd import PVQD, PVQDResult +from .time_evolvers.pvqd import PVQD, PVQDResult __all__ = [ "AlgorithmJob", From fe90188e75b982ecd91c8d874d6097f67d449ea3 Mon Sep 17 00:00:00 2001 From: Dariusz Lasecki Date: Thu, 8 Sep 2022 13:36:00 +0200 Subject: [PATCH 28/48] Code refactoring. --- qiskit/algorithms/time_evolvers/pvqd/pvqd.py | 26 +++++++++++++------ ...-dynamics-primitives-6003336d0866ca19.yaml | 2 +- .../algorithms/time_evolvers/test_pvqd.py | 22 ++++++++-------- 3 files changed, 30 insertions(+), 20 deletions(-) diff --git a/qiskit/algorithms/time_evolvers/pvqd/pvqd.py b/qiskit/algorithms/time_evolvers/pvqd/pvqd.py index bb39ba11e7ab..f65431fb149f 100644 --- a/qiskit/algorithms/time_evolvers/pvqd/pvqd.py +++ b/qiskit/algorithms/time_evolvers/pvqd/pvqd.py @@ -70,15 +70,22 @@ class PVQD(RealEvolver): .. code-block:: python - import numpy as np + import numpy as np + from qiskit.algorithms.state_fidelities import ComputeUncompute + from qiskit.algorithms.time_evolvers.pvqd import PVQD + from qiskit.primitives import Estimator from qiskit import BasicAer from qiskit.circuit.library import EfficientSU2 from qiskit.opflow import X, Z, I + from qiskit.quantum_info import Pauli + from qiskit.algorithms.optimizers import L_BFGS_B - backend = BasicAer.get_backend("statevector_simulator") + sampler = Sampler() + fidelity = ComputeUncompute(sampler) + estimator = Estimator() hamiltonian = 0.1 * (Z ^ Z) + (I ^ X) + (X ^ I) - observable = Z ^ Z + observable = Pauli("ZZ") ansatz = EfficientSU2(2, reps=1) initial_parameters = np.zeros(ansatz.num_parameters) @@ -87,11 +94,12 @@ class PVQD(RealEvolver): # setup the algorithm pvqd = PVQD( + fidelity, ansatz, + estimator, initial_parameters, num_timesteps=100, optimizer=optimizer, - quantum_instance=backend, ) # specify the evolution problem @@ -111,9 +119,9 @@ class PVQD(RealEvolver): def __init__( self, + fidelity_primitive: BaseStateFidelity, ansatz: QuantumCircuit, initial_parameters: np.ndarray, - fidelity_primitive: BaseStateFidelity, estimator: BaseEstimator | None = None, optimizer: Optimizer | Minimizer | None = None, num_timesteps: int | None = None, @@ -123,18 +131,20 @@ def __init__( ) -> None: """ Args: + fidelity_primitive: 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). 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_timestep: The number of time steps. If ``None`` it will be set such that the - timestep - is close to 0.01. + 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. diff --git a/releasenotes/notes/project-dynamics-primitives-6003336d0866ca19.yaml b/releasenotes/notes/project-dynamics-primitives-6003336d0866ca19.yaml index 9ed0c24f2304..3238127e1f14 100644 --- a/releasenotes/notes/project-dynamics-primitives-6003336d0866ca19.yaml +++ b/releasenotes/notes/project-dynamics-primitives-6003336d0866ca19.yaml @@ -34,8 +34,8 @@ features: # setup the algorithm pvqd = PVQD( - ansatz, fidelity, + ansatz, estimator, initial_parameters, num_timesteps=100, diff --git a/test/python/algorithms/time_evolvers/test_pvqd.py b/test/python/algorithms/time_evolvers/test_pvqd.py index 53426fb282ef..04870ef578ec 100644 --- a/test/python/algorithms/time_evolvers/test_pvqd.py +++ b/test/python/algorithms/time_evolvers/test_pvqd.py @@ -88,12 +88,12 @@ def test_pvqd(self, hamiltonian_type, gradient, num_timesteps): # run pVQD keeping track of the energy and the magnetization pvqd = PVQD( + fidelity_primitive, self.ansatz, self.initial_parameters, - fidelity_primitive, estimator, - num_timesteps=num_timesteps, optimizer=optimizer, + num_timesteps=num_timesteps, ) problem = EvolutionProblem(hamiltonian, time, aux_operators=[hamiltonian, self.observable]) result = pvqd.evolve(problem) @@ -113,9 +113,9 @@ def test_step(self): estimator = Estimator() fidelity_primitive = ComputeUncompute(sampler) pvqd = PVQD( + fidelity_primitive, self.ansatz, self.initial_parameters, - fidelity_primitive, estimator, optimizer=L_BFGS_B(maxiter=100), ) @@ -141,9 +141,9 @@ def test_get_loss(self): fidelity_primitive = ComputeUncompute(sampler) pvqd = PVQD( + fidelity_primitive, self.ansatz, self.initial_parameters, - fidelity_primitive, estimator, use_parameter_shift=False, ) @@ -168,12 +168,12 @@ def test_invalid_num_timestep(self): estimator = Estimator() fidelity_primitive = ComputeUncompute(sampler) pvqd = PVQD( + fidelity_primitive, self.ansatz, self.initial_parameters, - fidelity_primitive, estimator, - num_timesteps=0, optimizer=L_BFGS_B(), + num_timesteps=0, ) problem = EvolutionProblem( self.hamiltonian, time=0.01, aux_operators=[self.hamiltonian, self.observable] @@ -190,12 +190,12 @@ def test_initial_guess_and_observables(self): fidelity_primitive = ComputeUncompute(sampler) pvqd = PVQD( + fidelity_primitive, self.ansatz, self.initial_parameters, - fidelity_primitive, estimator, - num_timesteps=10, optimizer=SPSA(maxiter=0, learning_rate=0.1, perturbation=0.01), + num_timesteps=10, initial_guess=initial_guess, ) problem = EvolutionProblem( @@ -215,9 +215,9 @@ def test_zero_parameters(self): fidelity_primitive = ComputeUncompute(sampler) pvqd = PVQD( + fidelity_primitive, QuantumCircuit(2), np.array([]), - fidelity_primitive, optimizer=SPSA(maxiter=10, learning_rate=0.1, perturbation=0.01), ) @@ -239,9 +239,9 @@ def test_initial_state_raises(self): fidelity_primitive = ComputeUncompute(sampler) pvqd = PVQD( + fidelity_primitive, self.ansatz, self.initial_parameters, - fidelity_primitive, optimizer=SPSA(maxiter=0, learning_rate=0.1, perturbation=0.01), ) @@ -294,9 +294,9 @@ def test_gradient_supported(self): fidelity_primitive = ComputeUncompute(sampler) pvqd = PVQD( + fidelity_primitive=fidelity_primitive, ansatz=None, initial_parameters=np.array([]), - fidelity_primitive=fidelity_primitive, estimator=estimator, optimizer=optimizer, ) From 12e79afa9371d255ec8bd13bc666bf708f923094 Mon Sep 17 00:00:00 2001 From: Dariusz Lasecki Date: Thu, 8 Sep 2022 13:36:49 +0200 Subject: [PATCH 29/48] Removed old pvqd algorithm and related files. --- qiskit/algorithms/evolvers/pvqd/__init__.py | 18 - qiskit/algorithms/evolvers/pvqd/pvqd.py | 414 ------------------ .../algorithms/evolvers/pvqd/pvqd_result.py | 55 --- qiskit/algorithms/evolvers/pvqd/utils.py | 100 ----- test/python/algorithms/evolvers/test_pvqd.py | 325 -------------- 5 files changed, 912 deletions(-) delete mode 100644 qiskit/algorithms/evolvers/pvqd/__init__.py delete mode 100644 qiskit/algorithms/evolvers/pvqd/pvqd.py delete mode 100644 qiskit/algorithms/evolvers/pvqd/pvqd_result.py delete mode 100644 qiskit/algorithms/evolvers/pvqd/utils.py delete mode 100644 test/python/algorithms/evolvers/test_pvqd.py diff --git a/qiskit/algorithms/evolvers/pvqd/__init__.py b/qiskit/algorithms/evolvers/pvqd/__init__.py deleted file mode 100644 index 9377ce631b4e..000000000000 --- a/qiskit/algorithms/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/evolvers/pvqd/pvqd.py b/qiskit/algorithms/evolvers/pvqd/pvqd.py deleted file mode 100644 index e4a1d5893bb5..000000000000 --- a/qiskit/algorithms/evolvers/pvqd/pvqd.py +++ /dev/null @@ -1,414 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2019, 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 Dynamics Algorithm.""" - -from typing import Optional, Union, List, Tuple, Callable - -import logging -import numpy as np - -from qiskit import QiskitError -from qiskit.algorithms.optimizers import Optimizer, Minimizer -from qiskit.circuit import QuantumCircuit, ParameterVector -from qiskit.circuit.library import PauliEvolutionGate -from qiskit.extensions import HamiltonianGate -from qiskit.providers import Backend -from qiskit.opflow import OperatorBase, CircuitSampler, ExpectationBase, StateFn, MatrixOp -from qiskit.synthesis import EvolutionSynthesis, LieTrotter -from qiskit.utils import QuantumInstance - -from .pvqd_result import PVQDResult -from .utils import _get_observable_evaluator, _is_gradient_supported - -from ..evolution_problem import EvolutionProblem -from ..evolution_result import EvolutionResult -from ..real_evolver import RealEvolver - -logger = logging.getLogger(__name__) - - -class PVQD(RealEvolver): - """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. - expectation (ExpectationBase): The method to compute expectation values. - 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 import BasicAer - from qiskit.circuit.library import EfficientSU2 - from qiskit.opflow import X, Z, I, MatrixExpectation - - backend = BasicAer.get_backend("statevector_simulator") - expectation = MatrixExpectation() - hamiltonian = 0.1 * (Z ^ Z) + (I ^ X) + (X ^ I) - observable = Z ^ Z - ansatz = EfficientSU2(2, reps=1) - initial_parameters = np.zeros(ansatz.num_parameters) - - time = 1 - optimizer = L_BFGS_B() - - # setup the algorithm - pvqd = PVQD( - ansatz, - initial_parameters, - num_timesteps=100, - optimizer=optimizer, - quantum_instance=backend, - expectation=expectation - ) - - # specify the evolution problem - problem = EvolutionProblem( - 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, - ansatz: QuantumCircuit, - initial_parameters: np.ndarray, - expectation: ExpectationBase, - optimizer: Optional[Union[Optimizer, Minimizer]] = None, - num_timesteps: Optional[int] = None, - evolution: Optional[EvolutionSynthesis] = None, - use_parameter_shift: bool = True, - initial_guess: Optional[np.ndarray] = None, - quantum_instance: Optional[Union[Backend, QuantumInstance]] = None, - ) -> None: - """ - Args: - 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. - expectation: The expectation converter to evaluate expectation values. - 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_timestep: 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]`. - quantum_instance: The backend or quantum instance used to evaluate the circuits. - """ - 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.expectation = expectation - self.evolution = evolution - self.use_parameter_shift = use_parameter_shift - - self._sampler = None - self.quantum_instance = quantum_instance - - @property - def quantum_instance(self) -> Optional[QuantumInstance]: - """Return the current quantum instance.""" - return self._quantum_instance - - @quantum_instance.setter - def quantum_instance(self, quantum_instance: Optional[Union[Backend, QuantumInstance]]) -> None: - """Set the quantum instance and circuit sampler.""" - if quantum_instance is not None: - if not isinstance(quantum_instance, QuantumInstance): - quantum_instance = QuantumInstance(quantum_instance) - self._sampler = CircuitSampler(quantum_instance) - - self._quantum_instance = quantum_instance - - def step( - self, - hamiltonian: OperatorBase, - 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: - initial_guess = np.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: OperatorBase, - ansatz: QuantumCircuit, - dt: float, - current_parameters: np.ndarray, - ) -> Tuple[Callable[[np.ndarray], float], Optional[Callable[[np.ndarray], np.ndarray]]]: - - """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.bind_parameters(current_parameters) - - if isinstance(hamiltonian, MatrixOp): - evolution_gate = HamiltonianGate(hamiltonian.primitive, time=dt) - else: - 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) - overlap = StateFn(trotterized).adjoint() @ StateFn(shifted) - - converted = self.expectation.convert(overlap) - - def evaluate_loss( - displacement: Union[np.ndarray, List[np.ndarray]] - ) -> Union[float, List[float]]: - """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. - """ - 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)) - - sampled = self._sampler.convert(converted, params=value_dict) - - # 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 - np.abs(sampled.eval()) ** 2 - - 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 evolve(self, evolution_problem: EvolutionProblem) -> EvolutionResult: - """ - 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 the evolution time is not positive or the timestep is too small. - 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: - evaluate_observables = _get_observable_evaluator( - self.ansatz, observables, self.expectation, self._sampler - ) - observable_values = [evaluate_observables(self.initial_parameters)] - - fidelities = [1] - 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.bind_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 = {"quantum_instance", "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/evolvers/pvqd/pvqd_result.py b/qiskit/algorithms/evolvers/pvqd/pvqd_result.py deleted file mode 100644 index e14e7a0c9db5..000000000000 --- a/qiskit/algorithms/evolvers/pvqd/pvqd_result.py +++ /dev/null @@ -1,55 +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 typing import Union, Optional, List, Tuple -import numpy as np - -from qiskit.circuit import QuantumCircuit -from qiskit.opflow import StateFn, OperatorBase - -from ..evolution_result import EvolutionResult - - -class PVQDResult(EvolutionResult): - """The result object for the p-VQD algorithm.""" - - def __init__( - self, - evolved_state: Union[StateFn, QuantumCircuit, OperatorBase], - aux_ops_evaluated: Optional[List[Tuple[complex, complex]]] = None, - times: Optional[List[float]] = None, - parameters: Optional[List[np.ndarray]] = None, - fidelities: Optional[List[float]] = None, - estimated_error: Optional[float] = None, - observables: Optional[List[List[float]]] = 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/evolvers/pvqd/utils.py b/qiskit/algorithms/evolvers/pvqd/utils.py deleted file mode 100644 index 589f12005de8..000000000000 --- a/qiskit/algorithms/evolvers/pvqd/utils.py +++ /dev/null @@ -1,100 +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 typing import Union, List, Callable -import logging - -import numpy as np - -from qiskit.circuit import QuantumCircuit, Parameter, ParameterExpression -from qiskit.compiler import transpile -from qiskit.exceptions import QiskitError -from qiskit.opflow import ListOp, CircuitSampler, ExpectationBase, StateFn, OperatorBase -from qiskit.opflow.gradients.circuit_gradients import ParamShift - -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: Union[OperatorBase, List[OperatorBase]], - expectation: ExpectationBase, - sampler: CircuitSampler, -) -> Callable[[np.ndarray], Union[float, List[float]]]: - """Get a callable to evaluate a (list of) observable(s) for given circuit parameters.""" - - if isinstance(observables, list): - observables = ListOp(observables) - - expectation_value = StateFn(observables, is_measurement=True) @ StateFn(ansatz) - converted = expectation.convert(expectation_value) - - ansatz_parameters = ansatz.parameters - - def evaluate_observables(theta: np.ndarray) -> Union[float, List[float]]: - """Evaluate the observables for the ansatz parameters ``theta``. - - Args: - theta: The ansatz parameters. - - Returns: - The observables evaluated at the ansatz parameters. - """ - value_dict = dict(zip(ansatz_parameters, theta)) - sampled = sampler.convert(converted, params=value_dict) - return sampled.eval() - - return evaluate_observables diff --git a/test/python/algorithms/evolvers/test_pvqd.py b/test/python/algorithms/evolvers/test_pvqd.py deleted file mode 100644 index 5c450fa24822..000000000000 --- a/test/python/algorithms/evolvers/test_pvqd.py +++ /dev/null @@ -1,325 +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.""" - -from functools import partial -from ddt import ddt, data, unpack -import numpy as np - -from qiskit.test import QiskitTestCase - -from qiskit import BasicAer, QiskitError -from qiskit.circuit import QuantumCircuit, Parameter, Gate -from qiskit.algorithms.evolvers import EvolutionProblem -from qiskit.algorithms.evolvers.pvqd import PVQD -from qiskit.algorithms.optimizers import L_BFGS_B, GradientDescent, SPSA, OptimizerResult -from qiskit.circuit.library import EfficientSU2 -from qiskit.opflow import X, Z, I, MatrixExpectation, PauliExpectation - - -# 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): - """An 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(QiskitTestCase): - """Tests for the pVQD algorithm.""" - - def setUp(self): - super().setUp() - self.sv_backend = BasicAer.get_backend("statevector_simulator") - self.qasm_backend = BasicAer.get_backend("qasm_simulator") - self.expectation = MatrixExpectation() - self.hamiltonian = 0.1 * (Z ^ Z) + (I ^ X) + (X ^ I) - self.observable = Z ^ Z - self.ansatz = EfficientSU2(2, reps=1) - self.initial_parameters = np.zeros(self.ansatz.num_parameters) - - @data( - ("ising", MatrixExpectation, True, "sv", 2), - ("ising_matrix", MatrixExpectation, True, "sv", None), - ("ising", PauliExpectation, True, "qasm", 2), - ("pauli", PauliExpectation, False, "qasm", None), - ) - @unpack - def test_pvqd(self, hamiltonian_type, expectation_cls, gradient, backend_type, num_timesteps): - """Test a simple evolution.""" - time = 0.02 - - if hamiltonian_type == "ising": - hamiltonian = self.hamiltonian - elif hamiltonian_type == "ising_matrix": - hamiltonian = self.hamiltonian.to_matrix_op() - else: # hamiltonian_type == "pauli": - hamiltonian = X ^ X - - # parse input arguments - if gradient: - optimizer = GradientDescent(maxiter=1) - else: - optimizer = L_BFGS_B(maxiter=1) - - backend = self.sv_backend if backend_type == "sv" else self.qasm_backend - expectation = expectation_cls() - - # run pVQD keeping track of the energy and the magnetization - pvqd = PVQD( - self.ansatz, - self.initial_parameters, - num_timesteps=num_timesteps, - optimizer=optimizer, - quantum_instance=backend, - expectation=expectation, - ) - problem = EvolutionProblem(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.""" - - pvqd = PVQD( - self.ansatz, - self.initial_parameters, - optimizer=L_BFGS_B(maxiter=100), - quantum_instance=self.sv_backend, - expectation=MatrixExpectation(), - ) - - # 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.to_matrix_op(), - 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.""" - - pvqd = PVQD( - self.ansatz, - self.initial_parameters, - quantum_instance=self.sv_backend, - expectation=MatrixExpectation(), - 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.""" - pvqd = PVQD( - self.ansatz, - self.initial_parameters, - num_timesteps=0, - optimizer=L_BFGS_B(), - quantum_instance=self.sv_backend, - expectation=self.expectation, - ) - problem = EvolutionProblem( - 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) - - pvqd = PVQD( - self.ansatz, - self.initial_parameters, - num_timesteps=10, - optimizer=SPSA(maxiter=0, learning_rate=0.1, perturbation=0.01), - initial_guess=initial_guess, - quantum_instance=self.sv_backend, - expectation=self.expectation, - ) - problem = EvolutionProblem( - 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_missing_attributesquantum_instance(self): - """Test appropriate error is raised if the quantum instance is missing.""" - pvqd = PVQD( - self.ansatz, - self.initial_parameters, - optimizer=L_BFGS_B(maxiter=1), - expectation=self.expectation, - ) - problem = EvolutionProblem(self.hamiltonian, time=0.01) - - attrs_to_test = [ - ("optimizer", L_BFGS_B(maxiter=1)), - ("quantum_instance", self.qasm_backend), - ] - - for attr, value in attrs_to_test: - with self.subTest(msg=f"missing: {attr}"): - # set attribute to None to invalidate the setup - setattr(pvqd, attr, None) - - with self.assertRaises(ValueError): - _ = pvqd.evolve(problem) - - # set the correct value again - setattr(pvqd, attr, value) - - with self.subTest(msg="all set again"): - result = pvqd.evolve(problem) - self.assertIsNotNone(result.evolved_state) - - def test_zero_parameters(self): - """Test passing an ansatz with zero parameters raises an error.""" - problem = EvolutionProblem(self.hamiltonian, time=0.02) - - pvqd = PVQD( - QuantumCircuit(2), - np.array([]), - optimizer=SPSA(maxiter=10, learning_rate=0.1, perturbation=0.01), - quantum_instance=self.sv_backend, - expectation=self.expectation, - ) - - 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 = EvolutionProblem( - self.hamiltonian, - time=0.02, - initial_state=initial_state, - ) - - pvqd = PVQD( - self.ansatz, - self.initial_parameters, - optimizer=SPSA(maxiter=0, learning_rate=0.1, perturbation=0.01), - quantum_instance=self.sv_backend, - expectation=self.expectation, - ) - - with self.assertRaises(NotImplementedError): - _ = pvqd.evolve(problem) - - -class TestPVQDUtils(QiskitTestCase): - """Test some utility functions for PVQD.""" - - def setUp(self): - super().setUp() - self.sv_backend = BasicAer.get_backend("statevector_simulator") - self.expectation = MatrixExpectation() - self.hamiltonian = 0.1 * (Z ^ Z) + (I ^ X) + (X ^ I) - 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) - - pvqd = PVQD( - ansatz=None, - initial_parameters=np.array([]), - optimizer=optimizer, - quantum_instance=self.sv_backend, - expectation=self.expectation, - ) - problem = EvolutionProblem(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) From 387ce780dfb32fb41c011955943c45dbf6794046 Mon Sep 17 00:00:00 2001 From: Manoel Marques Date: Thu, 8 Sep 2022 11:13:47 -0400 Subject: [PATCH 30/48] Add pending deprecation for evolvers --- .../algorithms/evolvers/evolution_problem.py | 15 +++++++++++++- .../algorithms/evolvers/evolution_result.py | 17 +++++++++++++++- .../algorithms/evolvers/imaginary_evolver.py | 20 ++++++++++++++++++- qiskit/algorithms/evolvers/pvqd/pvqd.py | 1 + qiskit/algorithms/evolvers/real_evolver.py | 20 ++++++++++++++++++- .../evolvers/trotterization/trotter_qrte.py | 20 ++++++++++++++++++- 6 files changed, 88 insertions(+), 5 deletions(-) diff --git a/qiskit/algorithms/evolvers/evolution_problem.py b/qiskit/algorithms/evolvers/evolution_problem.py index 175beecd6bf6..bc815c7425d5 100644 --- a/qiskit/algorithms/evolvers/evolution_problem.py +++ b/qiskit/algorithms/evolvers/evolution_problem.py @@ -17,16 +17,29 @@ from qiskit import QuantumCircuit from qiskit.circuit import Parameter from qiskit.opflow import OperatorBase, StateFn +from qiskit.utils.deprecation import deprecate_function from ..list_or_dict import ListOrDict class EvolutionProblem: - """Evolution problem class. + """Pending deprecation: Evolution problem class. + + The EvolutionProblem class has been superseded by the + :class:`qiskit.algorithms.time_evolvers.EvolutionProblem` 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_function( + "The EvolutionProblem class has been superseded by the " + "qiskit.algorithms.time_evolvers.EvolutionProblem class. " + "This class will be deprecated in a future release and subsequently " + "removed after that.", + category=PendingDeprecationWarning, + ) def __init__( self, hamiltonian: OperatorBase, diff --git a/qiskit/algorithms/evolvers/evolution_result.py b/qiskit/algorithms/evolvers/evolution_result.py index 1dd91d705d28..1f2c1d50c5de 100644 --- a/qiskit/algorithms/evolvers/evolution_result.py +++ b/qiskit/algorithms/evolvers/evolution_result.py @@ -17,12 +17,27 @@ from qiskit import QuantumCircuit from qiskit.algorithms.list_or_dict import ListOrDict from qiskit.opflow import StateFn, OperatorBase +from qiskit.utils.deprecation import deprecate_function from ..algorithm_result import AlgorithmResult class EvolutionResult(AlgorithmResult): - """Class for holding evolution result.""" + """Pending deprecation: Class for holding evolution result. + The EvolutionResult class has been superseded by the + :class:`qiskit.algorithms.time_evolvers.EvolutionResult` class. + This class will be deprecated in a future release and subsequently + removed after that. + + """ + + @deprecate_function( + "The EvolutionResult class has been superseded by the " + "qiskit.algorithms.time_evolvers.EvolutionResult class. " + "This class will be deprecated in a future release and subsequently " + "removed after that.", + category=PendingDeprecationWarning, + ) def __init__( self, evolved_state: Union[StateFn, QuantumCircuit, OperatorBase], diff --git a/qiskit/algorithms/evolvers/imaginary_evolver.py b/qiskit/algorithms/evolvers/imaginary_evolver.py index 309bb73b08af..520de1bbaae1 100644 --- a/qiskit/algorithms/evolvers/imaginary_evolver.py +++ b/qiskit/algorithms/evolvers/imaginary_evolver.py @@ -14,12 +14,30 @@ from abc import ABC, abstractmethod +from qiskit.utils.deprecation import deprecate_function from .evolution_problem import EvolutionProblem from .evolution_result import EvolutionResult class ImaginaryEvolver(ABC): - """Interface for Quantum Imaginary Time Evolution.""" + """Pending deprecation: Interface for Quantum Imaginary Time Evolution. + + The ImaginaryEvolver interface has been superseded by the + :class:`qiskit.algorithms.time_evolvers.ImaginaryEvolver` interface. + This interface will be deprecated in a future release and subsequently + removed after that. + + """ + + @deprecate_function( + "The ImaginaryEvolver interface has been superseded by the " + "qiskit.algorithms.time_evolvers.ImaginaryEvolver interface. " + "This interface will be deprecated in a future release and subsequently " + "removed after that.", + category=PendingDeprecationWarning, + ) + def __init__(self) -> None: + pass @abstractmethod def evolve(self, evolution_problem: EvolutionProblem) -> EvolutionResult: diff --git a/qiskit/algorithms/evolvers/pvqd/pvqd.py b/qiskit/algorithms/evolvers/pvqd/pvqd.py index e4a1d5893bb5..323f32aa9db5 100644 --- a/qiskit/algorithms/evolvers/pvqd/pvqd.py +++ b/qiskit/algorithms/evolvers/pvqd/pvqd.py @@ -149,6 +149,7 @@ def __init__( a random vector with elements in the interval :math:`[-0.01, 0.01]`. quantum_instance: The backend or quantum instance used to evaluate the circuits. """ + super().__init__() if evolution is None: evolution = LieTrotter() diff --git a/qiskit/algorithms/evolvers/real_evolver.py b/qiskit/algorithms/evolvers/real_evolver.py index 6107facfe542..0ccaad7fa9b2 100644 --- a/qiskit/algorithms/evolvers/real_evolver.py +++ b/qiskit/algorithms/evolvers/real_evolver.py @@ -13,13 +13,31 @@ """Interface for Quantum Real Time Evolution.""" from abc import ABC, abstractmethod +from qiskit.utils.deprecation import deprecate_function from .evolution_problem import EvolutionProblem from .evolution_result import EvolutionResult class RealEvolver(ABC): - """Interface for Quantum Real Time Evolution.""" + """Pending deprecation: Interface for Quantum Real Time Evolution. + + The RealEvolver interface has been superseded by the + :class:`qiskit.algorithms.time_evolvers.RealEvolver` interface. + This interface will be deprecated in a future release and subsequently + removed after that. + + """ + + @deprecate_function( + "The RealEvolver interface has been superseded by the " + "qiskit.algorithms.time_evolvers.RealEvolver interface. " + "This interface will be deprecated in a future release and subsequently " + "removed after that.", + category=PendingDeprecationWarning, + ) + def __init__(self) -> None: + pass @abstractmethod def evolve(self, evolution_problem: EvolutionProblem) -> EvolutionResult: diff --git a/qiskit/algorithms/evolvers/trotterization/trotter_qrte.py b/qiskit/algorithms/evolvers/trotterization/trotter_qrte.py index 05b8266605b7..e91793570d51 100644 --- a/qiskit/algorithms/evolvers/trotterization/trotter_qrte.py +++ b/qiskit/algorithms/evolvers/trotterization/trotter_qrte.py @@ -13,6 +13,7 @@ """An algorithm to implement a Trotterization real time-evolution.""" from typing import Union, Optional +import warnings from qiskit import QuantumCircuit from qiskit.algorithms.aux_ops_evaluator import eval_observables @@ -32,10 +33,17 @@ from qiskit.providers import Backend from qiskit.synthesis import ProductFormula, LieTrotter from qiskit.utils import QuantumInstance +from qiskit.utils.deprecation import deprecate_function class TrotterQRTE(RealEvolver): - """Quantum Real Time Evolution using Trotterization. + """Pending deprecation: 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: @@ -58,6 +66,13 @@ class TrotterQRTE(RealEvolver): evolved_state = trotter_qrte.evolve(evolution_problem).evolved_state """ + @deprecate_function( + "The TrotterQRTE class has been superseded by the " + "qiskit.algorithms.time_evolvers.trotterization.TrotterQRTE class. " + "This class will be deprecated in a future release and subsequently " + "removed after that.", + category=PendingDeprecationWarning, + ) def __init__( self, product_formula: Optional[ProductFormula] = None, @@ -73,6 +88,9 @@ def __init__( 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 From a7e2f607afb4cb372afb58b2ef5f3497b02f99a6 Mon Sep 17 00:00:00 2001 From: Dariusz Lasecki Date: Thu, 8 Sep 2022 20:12:04 +0200 Subject: [PATCH 31/48] Renamed classes and linked to algorithms init. --- qiskit/algorithms/__init__.py | 25 +++++++++++++++++++ ...y_evolver.py => imaginary_time_evolver.py} | 8 +++--- .../{real_evolver.py => real_time_evolver.py} | 8 +++--- ...n_problem.py => time_evolution_problem.py} | 2 +- ...ion_result.py => time_evolution_result.py} | 2 +- ...framework-primitives-c86779b5d0dffd25.yaml | 8 +++--- ...blem.py => test_time_evolution_problem.py} | 12 ++++----- ...esult.py => test_time_evolution_result.py} | 8 +++--- 8 files changed, 49 insertions(+), 24 deletions(-) rename qiskit/algorithms/time_evolvers/{imaginary_evolver.py => imaginary_time_evolver.py} (83%) rename qiskit/algorithms/time_evolvers/{real_evolver.py => real_time_evolver.py} (82%) rename qiskit/algorithms/time_evolvers/{evolution_problem.py => time_evolution_problem.py} (99%) rename qiskit/algorithms/time_evolvers/{evolution_result.py => time_evolution_result.py} (97%) rename test/python/algorithms/time_evolvers/{test_evolution_problem.py => test_time_evolution_problem.py} (90%) rename test/python/algorithms/time_evolvers/{test_evolution_result.py => test_time_evolution_result.py} (85%) diff --git a/qiskit/algorithms/__init__.py b/qiskit/algorithms/__init__.py index 30d71a59e59c..b1419d3f4811 100644 --- a/qiskit/algorithms/__init__.py +++ b/qiskit/algorithms/__init__.py @@ -127,6 +127,23 @@ EvolutionProblem +Time Evolvers +-------- + +Primitives-enabled 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. + +.. autosummary:: + :toctree: ../stubs/ + :nosignatures: + + RealTimeEvolver + ImaginaryTimeEvolver + TimeEvolutionResult + TimeEvolutionProblem + + Factorizers ----------- @@ -225,6 +242,10 @@ from .evolvers import EvolutionResult, EvolutionProblem from .evolvers.real_evolver import RealEvolver from .evolvers.imaginary_evolver import ImaginaryEvolver +from .time_evolvers.imaginary_time_evolver import ImaginaryTimeEvolver +from .time_evolvers.real_time_evolver import RealTimeEvolver +from .time_evolvers.time_evolution_problem import TimeEvolutionProblem +from .time_evolvers.time_evolution_result import TimeEvolutionResult from .variational_algorithm import VariationalAlgorithm, VariationalResult from .amplitude_amplifiers import Grover, GroverResult, AmplificationProblem, AmplitudeAmplifier from .amplitude_estimators import ( @@ -288,11 +309,15 @@ "NumPyEigensolver", "RealEvolver", "ImaginaryEvolver", + "RealTimeEvolver", + "ImaginaryTimeEvolver", "TrotterQRTE", "VarQITE", "VarQRTE", "EvolutionResult", "EvolutionProblem", + "TimeEvolutionResult", + "TimeEvolutionProblem", "LinearSolverResult", "Eigensolver", "EigensolverResult", diff --git a/qiskit/algorithms/time_evolvers/imaginary_evolver.py b/qiskit/algorithms/time_evolvers/imaginary_time_evolver.py similarity index 83% rename from qiskit/algorithms/time_evolvers/imaginary_evolver.py rename to qiskit/algorithms/time_evolvers/imaginary_time_evolver.py index 309bb73b08af..e62d02e5ab9c 100644 --- a/qiskit/algorithms/time_evolvers/imaginary_evolver.py +++ b/qiskit/algorithms/time_evolvers/imaginary_time_evolver.py @@ -14,15 +14,15 @@ from abc import ABC, abstractmethod -from .evolution_problem import EvolutionProblem -from .evolution_result import EvolutionResult +from .time_evolution_problem import TimeEvolutionProblem +from .time_evolution_result import TimeEvolutionResult -class ImaginaryEvolver(ABC): +class ImaginaryTimeEvolver(ABC): """Interface for Quantum Imaginary Time Evolution.""" @abstractmethod - def evolve(self, evolution_problem: EvolutionProblem) -> EvolutionResult: + 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` diff --git a/qiskit/algorithms/time_evolvers/real_evolver.py b/qiskit/algorithms/time_evolvers/real_time_evolver.py similarity index 82% rename from qiskit/algorithms/time_evolvers/real_evolver.py rename to qiskit/algorithms/time_evolvers/real_time_evolver.py index 6107facfe542..587f85d436e2 100644 --- a/qiskit/algorithms/time_evolvers/real_evolver.py +++ b/qiskit/algorithms/time_evolvers/real_time_evolver.py @@ -14,15 +14,15 @@ from abc import ABC, abstractmethod -from .evolution_problem import EvolutionProblem -from .evolution_result import EvolutionResult +from .time_evolution_problem import TimeEvolutionProblem +from .time_evolution_result import TimeEvolutionResult -class RealEvolver(ABC): +class RealTimeEvolver(ABC): """Interface for Quantum Real Time Evolution.""" @abstractmethod - def evolve(self, evolution_problem: EvolutionProblem) -> EvolutionResult: + 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` diff --git a/qiskit/algorithms/time_evolvers/evolution_problem.py b/qiskit/algorithms/time_evolvers/time_evolution_problem.py similarity index 99% rename from qiskit/algorithms/time_evolvers/evolution_problem.py rename to qiskit/algorithms/time_evolvers/time_evolution_problem.py index 8b517c4e04ed..59868cbe51c6 100644 --- a/qiskit/algorithms/time_evolvers/evolution_problem.py +++ b/qiskit/algorithms/time_evolvers/time_evolution_problem.py @@ -23,7 +23,7 @@ from ...quantum_info.operators.base_operator import BaseOperator -class EvolutionProblem: +class TimeEvolutionProblem: """Evolution problem class. This class is the input to time evolution algorithms and must contain information on the total diff --git a/qiskit/algorithms/time_evolvers/evolution_result.py b/qiskit/algorithms/time_evolvers/time_evolution_result.py similarity index 97% rename from qiskit/algorithms/time_evolvers/evolution_result.py rename to qiskit/algorithms/time_evolvers/time_evolution_result.py index d049e68da58d..2ff47e3d08e5 100644 --- a/qiskit/algorithms/time_evolvers/evolution_result.py +++ b/qiskit/algorithms/time_evolvers/time_evolution_result.py @@ -18,7 +18,7 @@ from ..algorithm_result import AlgorithmResult -class EvolutionResult(AlgorithmResult): +class TimeEvolutionResult(AlgorithmResult): """ Class for holding evolution result. diff --git a/releasenotes/notes/evolution-framework-primitives-c86779b5d0dffd25.yaml b/releasenotes/notes/evolution-framework-primitives-c86779b5d0dffd25.yaml index 1ea0b3485541..bfc708ca9c72 100644 --- a/releasenotes/notes/evolution-framework-primitives-c86779b5d0dffd25.yaml +++ b/releasenotes/notes/evolution-framework-primitives-c86779b5d0dffd25.yaml @@ -3,10 +3,10 @@ features: - | Added `~qiskit.algorithms.time_evolvers` package with interfaces that will cover primitive-enabled time evolution algorithms: - :class:`~qiskit.algorithms.time_evolvers.EvolutionProblem`, - :class:`~qiskit.algorithms.time_evolvers.EvolutionResult`, - :class:`~qiskit.algorithms.time_evolvers.ImaginaryEvolver`, - :class:`~qiskit.algorithms.time_evolvers.RealEvolver`. + :class:`~qiskit.algorithms.time_evolvers.TimeEvolutionProblem`, + :class:`~qiskit.algorithms.time_evolvers.TimeEvolutionResult`, + :class:`~qiskit.algorithms.time_evolvers.ImaginaryTimeEvolver`, + :class:`~qiskit.algorithms.time_evolvers.RealTimeEvolver`. deprecations: - | `~qiskit.algorithms.evolvers` package will now issue a ``PendingDeprecationWarning``. diff --git a/test/python/algorithms/time_evolvers/test_evolution_problem.py b/test/python/algorithms/time_evolvers/test_time_evolution_problem.py similarity index 90% rename from test/python/algorithms/time_evolvers/test_evolution_problem.py rename to test/python/algorithms/time_evolvers/test_time_evolution_problem.py index 315245b20ebd..227583f8aef7 100644 --- a/test/python/algorithms/time_evolvers/test_evolution_problem.py +++ b/test/python/algorithms/time_evolvers/test_time_evolution_problem.py @@ -16,14 +16,14 @@ from ddt import data, ddt, unpack from numpy.testing import assert_raises from qiskit import QuantumCircuit -from qiskit.algorithms.time_evolvers.evolution_problem import EvolutionProblem +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 TestEvolutionProblem(QiskitAlgorithmsTestCase): +class TestTimeEvolutionProblem(QiskitAlgorithmsTestCase): """Test evolver problem class.""" def test_init_default(self): @@ -32,7 +32,7 @@ def test_init_default(self): time = 2.5 initial_state = One - evo_problem = EvolutionProblem(hamiltonian, time, initial_state) + evo_problem = TimeEvolutionProblem(hamiltonian, time, initial_state) expected_hamiltonian = Y expected_time = 2.5 @@ -57,7 +57,7 @@ def test_init_all(self, initial_state): aux_operators = [X, Y] param_value_dict = {t_parameter: 3.2} - evo_problem = EvolutionProblem( + evo_problem = TimeEvolutionProblem( hamiltonian, time, initial_state, @@ -85,14 +85,14 @@ def test_init_all(self, initial_state): def test_init_errors(self, hamiltonian, time, initial_state): """Tests expected errors are thrown on invalid time argument.""" with assert_raises(ValueError): - _ = EvolutionProblem(hamiltonian, time, initial_state) + _ = TimeEvolutionProblem(hamiltonian, time, initial_state) 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."): hamiltonian = PauliSumOp(SparsePauliOp([Pauli("X"), Pauli("Y")]), param_x) - evolution_problem = EvolutionProblem(hamiltonian, 2, Zero) + evolution_problem = TimeEvolutionProblem(hamiltonian, 2, Zero) with assert_raises(ValueError): evolution_problem.validate_params() diff --git a/test/python/algorithms/time_evolvers/test_evolution_result.py b/test/python/algorithms/time_evolvers/test_time_evolution_result.py similarity index 85% rename from test/python/algorithms/time_evolvers/test_evolution_result.py rename to test/python/algorithms/time_evolvers/test_time_evolution_result.py index 52b7f3faaf98..26f21ba93627 100644 --- a/test/python/algorithms/time_evolvers/test_evolution_result.py +++ b/test/python/algorithms/time_evolvers/test_time_evolution_result.py @@ -12,17 +12,17 @@ """Class for testing evolution result.""" import unittest from test.python.algorithms import QiskitAlgorithmsTestCase -from qiskit.algorithms.time_evolvers.evolution_result import EvolutionResult +from qiskit.algorithms import TimeEvolutionResult from qiskit.opflow import Zero -class TestEvolutionResult(QiskitAlgorithmsTestCase): +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 = EvolutionResult(evolved_state=evolved_state) + evo_result = TimeEvolutionResult(evolved_state=evolved_state) expected_state = Zero expected_aux_ops_evaluated = None @@ -34,7 +34,7 @@ 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 = EvolutionResult(evolved_state, evolved_aux_ops_evaluated) + 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)] From 5c8725562d8adacc8a30f00e254d59476580411f Mon Sep 17 00:00:00 2001 From: Manoel Marques Date: Thu, 8 Sep 2022 15:35:11 -0400 Subject: [PATCH 32/48] fix docstring --- qiskit/algorithms/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qiskit/algorithms/__init__.py b/qiskit/algorithms/__init__.py index 408aac4c922e..ef79ed3431f5 100644 --- a/qiskit/algorithms/__init__.py +++ b/qiskit/algorithms/__init__.py @@ -128,7 +128,7 @@ Time Evolvers --------- +------------- Primitives-enabled 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 From d03bddfe4e76d458bfc992e3956c97f26757e5f2 Mon Sep 17 00:00:00 2001 From: Dariusz Lasecki Date: Fri, 9 Sep 2022 09:41:09 +0200 Subject: [PATCH 33/48] Improved reno. --- ...ion-framework-primitives-c86779b5d0dffd25.yaml | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/releasenotes/notes/evolution-framework-primitives-c86779b5d0dffd25.yaml b/releasenotes/notes/evolution-framework-primitives-c86779b5d0dffd25.yaml index bfc708ca9c72..2adf8a637293 100644 --- a/releasenotes/notes/evolution-framework-primitives-c86779b5d0dffd25.yaml +++ b/releasenotes/notes/evolution-framework-primitives-c86779b5d0dffd25.yaml @@ -1,12 +1,15 @@ --- features: - | - Added `~qiskit.algorithms.time_evolvers` package with interfaces that will cover + Added :class:`qiskit.algorithms.time_evolvers` package with interfaces that will cover primitive-enabled time evolution algorithms: - :class:`~qiskit.algorithms.time_evolvers.TimeEvolutionProblem`, - :class:`~qiskit.algorithms.time_evolvers.TimeEvolutionResult`, - :class:`~qiskit.algorithms.time_evolvers.ImaginaryTimeEvolver`, - :class:`~qiskit.algorithms.time_evolvers.RealTimeEvolver`. + :class:`qiskit.algorithms.time_evolvers.TimeEvolutionProblem`, + :class:`qiskit.algorithms.time_evolvers.TimeEvolutionResult`, + :class:`qiskit.algorithms.time_evolvers.ImaginaryTimeEvolver`, + :class:`qiskit.algorithms.time_evolvers.RealTimeEvolver`. deprecations: - | - `~qiskit.algorithms.evolvers` package will now issue a ``PendingDeprecationWarning``. + :class:`qiskit.algorithms.evolvers` package will now issue a ``PendingDeprecationWarning``. It + will be deprecated in a future release and subsequently removed after that. This is being + replaced by the new :class:`qiskit.algorithms.time_evolvers` package that will host + primitive-enabled algorithms. From 4a0bdd0bf5d331e307d3c6d04d60c201b80bdc18 Mon Sep 17 00:00:00 2001 From: Dariusz Lasecki Date: Fri, 9 Sep 2022 10:15:38 +0200 Subject: [PATCH 34/48] Updated classes names. --- qiskit/algorithms/time_evolvers/pvqd/pvqd.py | 10 +++++----- qiskit/algorithms/time_evolvers/pvqd/pvqd_result.py | 4 ++-- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/qiskit/algorithms/time_evolvers/pvqd/pvqd.py b/qiskit/algorithms/time_evolvers/pvqd/pvqd.py index 6d51ea1f095a..14a29a23b9f3 100644 --- a/qiskit/algorithms/time_evolvers/pvqd/pvqd.py +++ b/qiskit/algorithms/time_evolvers/pvqd/pvqd.py @@ -29,15 +29,15 @@ from qiskit.synthesis import EvolutionSynthesis, LieTrotter from .pvqd_result import PVQDResult from .utils import _get_observable_evaluator, _is_gradient_supported -from ..evolution_problem import EvolutionProblem -from ..evolution_result import EvolutionResult -from ..real_evolver import RealEvolver +from ..time_evolution_problem import TimeEvolutionProblem +from ..time_evolution_result import TimeEvolutionResult +from ..real_time_evolver import RealTimeEvolver from ...state_fidelities import BaseStateFidelity logger = logging.getLogger(__name__) -class PVQD(RealEvolver): +class PVQD(RealTimeEvolver): """The projected Variational Quantum Dynamics (p-VQD) Algorithm. In each timestep, this algorithm computes the next state with a Trotter formula @@ -320,7 +320,7 @@ def _transpose_param_dicts(self, params: dict) -> list[dict[Parameter, float]]: return param_bindings - def evolve(self, evolution_problem: EvolutionProblem) -> EvolutionResult: + def evolve(self, evolution_problem: TimeEvolutionProblem) -> TimeEvolutionResult: """ Args: evolution_problem: The evolution problem containing the hamiltonian, total evolution diff --git a/qiskit/algorithms/time_evolvers/pvqd/pvqd_result.py b/qiskit/algorithms/time_evolvers/pvqd/pvqd_result.py index 71b332c4e142..6f8a34cb89a9 100644 --- a/qiskit/algorithms/time_evolvers/pvqd/pvqd_result.py +++ b/qiskit/algorithms/time_evolvers/pvqd/pvqd_result.py @@ -16,10 +16,10 @@ import numpy as np from qiskit.circuit import QuantumCircuit -from ..evolution_result import EvolutionResult +from ..time_evolution_result import TimeEvolutionResult -class PVQDResult(EvolutionResult): +class PVQDResult(TimeEvolutionResult): """The result object for the p-VQD algorithm.""" def __init__( From 76800190163fb6fec727c30381b610a8ced994ba Mon Sep 17 00:00:00 2001 From: Dariusz Lasecki Date: Fri, 9 Sep 2022 10:17:49 +0200 Subject: [PATCH 35/48] Code refactoring. --- qiskit/algorithms/time_evolvers/time_evolution_problem.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/qiskit/algorithms/time_evolvers/time_evolution_problem.py b/qiskit/algorithms/time_evolvers/time_evolution_problem.py index 59868cbe51c6..ceb4b97c4175 100644 --- a/qiskit/algorithms/time_evolvers/time_evolution_problem.py +++ b/qiskit/algorithms/time_evolvers/time_evolution_problem.py @@ -13,7 +13,7 @@ """Evolution problem class.""" from __future__ import annotations -from typing import Mapping +from collections.abc import Mapping from qiskit import QuantumCircuit from qiskit.circuit import Parameter, ParameterExpression @@ -54,8 +54,7 @@ def __init__( aux_operators: ListOrDict[BaseOperator | PauliSumOp] | None = None, truncation_threshold: float = 1e-12, t_param: Parameter | None = None, - param_value_dict: Mapping[Parameter, complex] - | None = None, # parametrization will become supported in BaseOperator soon + param_value_dict: Mapping[Parameter, complex] | None = None, # parametrization will become supported in BaseOperator soon ): """ Args: From 2d8b5329ee0d0bb036a7f4cf1573758aaf43eee5 Mon Sep 17 00:00:00 2001 From: Dariusz Lasecki Date: Fri, 9 Sep 2022 10:41:17 +0200 Subject: [PATCH 36/48] Applied CR comments. --- qiskit/algorithms/time_evolvers/pvqd/pvqd.py | 25 ++++++++----------- ...-dynamics-primitives-6003336d0866ca19.yaml | 5 ++-- .../algorithms/time_evolvers/test_pvqd.py | 19 +++++++------- 3 files changed, 22 insertions(+), 27 deletions(-) diff --git a/qiskit/algorithms/time_evolvers/pvqd/pvqd.py b/qiskit/algorithms/time_evolvers/pvqd/pvqd.py index 14a29a23b9f3..bed388af9cb4 100644 --- a/qiskit/algorithms/time_evolvers/pvqd/pvqd.py +++ b/qiskit/algorithms/time_evolvers/pvqd/pvqd.py @@ -14,6 +14,7 @@ from __future__ import annotations import logging +import warnings from typing import Callable import numpy as np @@ -22,8 +23,7 @@ from qiskit.algorithms.optimizers import Optimizer, Minimizer from qiskit.circuit import QuantumCircuit, ParameterVector, Parameter from qiskit.circuit.library import PauliEvolutionGate -from qiskit.extensions import HamiltonianGate -from qiskit.opflow import MatrixOp, PauliSumOp +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 @@ -119,7 +119,7 @@ class PVQD(RealTimeEvolver): def __init__( self, - fidelity_primitive: BaseStateFidelity, + fidelity: BaseStateFidelity, ansatz: QuantumCircuit, initial_parameters: np.ndarray, estimator: BaseEstimator | None = None, @@ -131,13 +131,13 @@ def __init__( ) -> None: """ Args: - fidelity_primitive: A fidelity primitive used by the algorithm. + 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). + 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 @@ -165,7 +165,7 @@ def __init__( self.optimizer = optimizer self.initial_guess = initial_guess self.estimator = estimator - self.fidelity_primitive = fidelity_primitive + self.fidelity_primitive = fidelity self.evolution = evolution self.use_parameter_shift = use_parameter_shift @@ -236,10 +236,7 @@ def get_loss( # use Trotterization to evolve the current state trotterized = ansatz.bind_parameters(current_parameters) - if isinstance(hamiltonian, MatrixOp): - evolution_gate = HamiltonianGate(hamiltonian.primitive, time=dt) - else: - evolution_gate = PauliEvolutionGate(hamiltonian, time=dt, synthesis=self.evolution) + evolution_gate = PauliEvolutionGate(hamiltonian, time=dt, synthesis=self.evolution) trotterized.append(evolution_gate, ansatz.qubits) @@ -332,7 +329,6 @@ def evolve(self, evolution_problem: TimeEvolutionProblem) -> TimeEvolutionResult Raises: ValueError: If the evolution time is not positive or the timestep is too small. NotImplementedError: If the evolution problem contains an initial state. - Warning: If observables provided but an estimator not provided. """ self._validate_setup() @@ -354,8 +350,9 @@ def evolve(self, evolution_problem: TimeEvolutionProblem) -> TimeEvolutionResult # 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 Warning( - "Observables provided to be estimated but the estimator was not provided. " + warnings.warn( + "The evolution problem contained aux_operators but no estimator was provided. " + "The algorithm continues without calculating these quantities. " ) evaluate_observables = _get_observable_evaluator( self.ansatz, observables, self.estimator @@ -403,7 +400,7 @@ def _validate_setup(self, skip=None): if skip is None: skip = {} - required_attributes = {"fidelity_primitive", "optimizer"}.difference(skip) + required_attributes = {"optimizer"}.difference(skip) for attr in required_attributes: if getattr(self, attr, None) is None: diff --git a/releasenotes/notes/project-dynamics-primitives-6003336d0866ca19.yaml b/releasenotes/notes/project-dynamics-primitives-6003336d0866ca19.yaml index 3238127e1f14..a665b63319b5 100644 --- a/releasenotes/notes/project-dynamics-primitives-6003336d0866ca19.yaml +++ b/releasenotes/notes/project-dynamics-primitives-6003336d0866ca19.yaml @@ -17,14 +17,13 @@ features: from qiskit.primitives import Estimator from qiskit import BasicAer from qiskit.circuit.library import EfficientSU2 - from qiskit.opflow import X, Z, I - from qiskit.quantum_info import Pauli + from qiskit.quantum_info import Pauli, SparsePauliOp from qiskit.algorithms.optimizers import L_BFGS_B sampler = Sampler() fidelity = ComputeUncompute(sampler) estimator = Estimator() - hamiltonian = 0.1 * (Z ^ Z) + (I ^ X) + (X ^ I) + hamiltonian = 0.1 * SparsePauliOp([Pauli("ZZ"), Pauli("IX"), Pauli("XI")]) observable = Pauli("ZZ") ansatz = EfficientSU2(2, reps=1) initial_parameters = np.zeros(ansatz.num_parameters) diff --git a/test/python/algorithms/time_evolvers/test_pvqd.py b/test/python/algorithms/time_evolvers/test_pvqd.py index 04870ef578ec..958679683301 100644 --- a/test/python/algorithms/time_evolvers/test_pvqd.py +++ b/test/python/algorithms/time_evolvers/test_pvqd.py @@ -18,8 +18,9 @@ import numpy as np from qiskit.algorithms.state_fidelities import ComputeUncompute +from qiskit.opflow import PauliSumOp from qiskit.primitives import Sampler, Estimator -from qiskit.quantum_info import Pauli +from qiskit.quantum_info import Pauli, SparsePauliOp from qiskit.test import QiskitTestCase from qiskit import QiskitError from qiskit.circuit import QuantumCircuit, Parameter, Gate @@ -27,7 +28,6 @@ from qiskit.algorithms.time_evolvers.pvqd import PVQD from qiskit.algorithms.optimizers import L_BFGS_B, GradientDescent, SPSA, OptimizerResult from qiskit.circuit.library import EfficientSU2 -from qiskit.opflow import X, Z, I # pylint: disable=unused-argument, invalid-name @@ -57,15 +57,12 @@ class TestPVQD(QiskitTestCase): def setUp(self): super().setUp() - self.hamiltonian = 0.1 * (Z ^ Z) + (I ^ X) + (X ^ I) + 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) - @data( - ("ising", True, 2), - ("pauli", False, None), - ) + @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.""" @@ -73,6 +70,8 @@ def test_pvqd(self, hamiltonian_type, gradient, num_timesteps): if hamiltonian_type == "ising": hamiltonian = self.hamiltonian + elif hamiltonian_type == "pauli_sum_op": + hamiltonian = PauliSumOp(self.hamiltonian) else: # hamiltonian_type == "pauli": hamiltonian = Pauli("XX") @@ -123,7 +122,7 @@ def test_step(self): # 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.to_matrix_op(), + self.hamiltonian, self.ansatz, self.initial_parameters, dt=0.0, @@ -254,7 +253,7 @@ class TestPVQDUtils(QiskitTestCase): def setUp(self): super().setUp() - self.hamiltonian = 0.1 * (Z ^ Z) + (I ^ X) + (X ^ I) + self.hamiltonian = 0.1 * SparsePauliOp([Pauli("ZZ"), Pauli("IX"), Pauli("XI")]) self.ansatz = EfficientSU2(2, reps=1) def test_gradient_supported(self): @@ -294,7 +293,7 @@ def test_gradient_supported(self): fidelity_primitive = ComputeUncompute(sampler) pvqd = PVQD( - fidelity_primitive=fidelity_primitive, + fidelity=fidelity_primitive, ansatz=None, initial_parameters=np.array([]), estimator=estimator, From d73c7cfdc19d43f3d7bdf7a047220f2768fdd07f Mon Sep 17 00:00:00 2001 From: Dariusz Lasecki Date: Fri, 9 Sep 2022 10:41:26 +0200 Subject: [PATCH 37/48] Removed unnecessary files. --- qiskit/algorithms/observables_evaluator.py | 162 ------------------ ...able-eval-primitives-e1fd989e15c7760c.yaml | 9 - .../algorithms/test_observables_evaluator.py | 142 --------------- 3 files changed, 313 deletions(-) delete mode 100644 qiskit/algorithms/observables_evaluator.py delete mode 100644 releasenotes/notes/observable-eval-primitives-e1fd989e15c7760c.yaml delete mode 100644 test/python/algorithms/test_observables_evaluator.py diff --git a/qiskit/algorithms/observables_evaluator.py b/qiskit/algorithms/observables_evaluator.py deleted file mode 100644 index cc6368c34672..000000000000 --- a/qiskit/algorithms/observables_evaluator.py +++ /dev/null @@ -1,162 +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 - -from typing import Tuple, List - -import numpy as np - -from qiskit import QuantumCircuit -from qiskit.opflow import ( - PauliSumOp, -) -from . import AlgorithmError -from .list_or_dict import ListOrDict -from ..primitives import EstimatorResult, BaseEstimator -from ..quantum_info.operators.base_operator import BaseOperator - - -def eval_observables( - estimator: BaseEstimator, - quantum_state: QuantumCircuit, - observables: ListOrDict[BaseOperator | PauliSumOp], - threshold: float = 1e-12, -) -> ListOrDict[Tuple[complex, complex]]: - """ - Accepts a sequence of operators and calculates their expectation values - means - and standard deviations. They are calculated with respect to a quantum state provided. A user - can optionally provide a threshold value which filters mean values falling below the threshold. - - Args: - estimator: An estimator primitive used for calculations. - quantum_state: An unparametrized quantum circuit representing a quantum state that - expectation values are computed against. - observables: A list or a dictionary of operators whose expectation values are to be - calculated. - threshold: A threshold value that defines which mean values should be neglected (helpful for - ignoring numerical instabilities close to 0). - - Returns: - A list or a dictionary of tuples (mean, standard deviation). - - Raises: - ValueError: If a ``quantum_state`` with free parameters is provided. - AlgorithmError: If a primitive job is not successful. - """ - - if ( - isinstance(quantum_state, QuantumCircuit) # State cannot be parametrized - and len(quantum_state.parameters) > 0 - ): - raise ValueError( - "A parametrized representation of a quantum_state was provided. It is not " - "allowed - it cannot have free parameters." - ) - if isinstance(observables, dict): - observables_list = list(observables.values()) - else: - observables_list = observables - - observables_list = _handle_zero_ops(observables_list) - quantum_state = [quantum_state] * len(observables) - try: - estimator_job = estimator.run(quantum_state, observables_list) - expectation_values = estimator_job.result().values - except Exception as exc: - raise AlgorithmError("The primitive job failed!") from exc - - std_devs = _compute_std_devs(estimator_job, len(expectation_values)) - - # Discard values below threshold - observables_means = expectation_values * (np.abs(expectation_values) > threshold) - # zip means and standard deviations into tuples - observables_results = list(zip(observables_means, std_devs)) - - # Return None eigenvalues for None operators if observables is a list. - 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.""" - zero_op = PauliSumOp.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, complex]], - observables: ListOrDict[BaseOperator | PauliSumOp], -) -> ListOrDict[Tuple[complex, complex]]: - """ - Prepares a list of eigenvalues and standard deviations 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 = [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( - estimator_result: EstimatorResult, - results_length: int, -) -> List[complex | None]: - """ - Calculates a list of standard deviations from expectation values of observables provided. If - there is no variance data available from a primitive, the standard deviation values will be set - to ``None``. - - Args: - estimator_result: An estimator result. - results_length: Number of expectation values calculated. - - Returns: - A list of standard deviations. - """ - if not estimator_result.metadata: - return [0] * results_length - - std_devs = [] - for metadata in estimator_result.metadata: - if metadata and "variance" in metadata.keys() and "shots" in metadata.keys(): - variance = metadata["variance"] - shots = metadata["shots"] - if variance is None or shots is None: - std_devs.append(None) - else: - std_devs.append(np.sqrt(variance / shots)) - else: - std_devs.append(0) - - return std_devs diff --git a/releasenotes/notes/observable-eval-primitives-e1fd989e15c7760c.yaml b/releasenotes/notes/observable-eval-primitives-e1fd989e15c7760c.yaml deleted file mode 100644 index a7e30f4fc8b1..000000000000 --- a/releasenotes/notes/observable-eval-primitives-e1fd989e15c7760c.yaml +++ /dev/null @@ -1,9 +0,0 @@ ---- -features: - - | - Added `~qiskit.algorithms.observables_evaluator` with - :class:`~qiskit.primitives.BaseEstimator` as ``init`` parameter. It will soon replace - `~qiskit.algorithms.aux_ops_evaluator`. -deprecations: - - | - Using `~qiskit.algorithms.aux_ops_evaluator` will now issue a ``PendingDeprecationWarning``. diff --git a/test/python/algorithms/test_observables_evaluator.py b/test/python/algorithms/test_observables_evaluator.py deleted file mode 100644 index 23e3cbda3d2a..000000000000 --- a/test/python/algorithms/test_observables_evaluator.py +++ /dev/null @@ -1,142 +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 evaluator of auxiliary operators for algorithms.""" -from __future__ import annotations -import unittest -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.observables_evaluator import eval_observables -from qiskit.primitives import Estimator -from qiskit.quantum_info import Statevector -from qiskit import QuantumCircuit -from qiskit.circuit.library import EfficientSU2 -from qiskit.opflow import ( - PauliSumOp, - X, - Y, -) -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 - 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) 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: QuantumCircuit, - decimal: int, - observables: ListOrDict[BaseOperator | PauliSumOp], - estimator: Estimator, - ): - result = eval_observables(estimator, quantum_state, observables, 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[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.bind_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_eval_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.bind_parameters(parameters) - state = bound_ansatz - estimator = Estimator() - observables = [(X ^ X) + (Y ^ Y), 0] - result = eval_observables(estimator, state, observables, self.threshold) - expected_result = [(0.015607318055509564, 0), (0.0, 0)] - np.testing.assert_array_almost_equal(result, expected_result) - - -if __name__ == "__main__": - unittest.main() From ba0f2a273b95fbae5c33bc5e115f676fc1c9beca Mon Sep 17 00:00:00 2001 From: Dariusz Lasecki Date: Fri, 9 Sep 2022 10:55:09 +0200 Subject: [PATCH 38/48] Applied CR comments. --- qiskit/algorithms/time_evolvers/pvqd/pvqd.py | 12 ++++++++++-- qiskit/algorithms/time_evolvers/pvqd/utils.py | 12 ++++++++++-- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/qiskit/algorithms/time_evolvers/pvqd/pvqd.py b/qiskit/algorithms/time_evolvers/pvqd/pvqd.py index bed388af9cb4..3b9efec7ec79 100644 --- a/qiskit/algorithms/time_evolvers/pvqd/pvqd.py +++ b/qiskit/algorithms/time_evolvers/pvqd/pvqd.py @@ -32,6 +32,7 @@ from ..time_evolution_problem import TimeEvolutionProblem from ..time_evolution_result import TimeEvolutionResult from ..real_time_evolver import RealTimeEvolver +from ... import AlgorithmError from ...state_fidelities import BaseStateFidelity logger = logging.getLogger(__name__) @@ -252,6 +253,9 @@ def evaluate_loss(displacement: np.ndarray | list[np.ndarray]) -> float | list[f 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) @@ -265,8 +269,12 @@ def evaluate_loss(displacement: np.ndarray | list[np.ndarray]) -> float | list[f 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 - job = self.fidelity_primitive.run(states1, states2, values_2=param_dicts2) - fidelities = job.result().fidelities + try: + job = self.fidelity_primitive.run(states1, states2, values_2=param_dicts2) + fidelities = job.result().fidelities + except Exception as exc: + raise AlgorithmError("The primitive job failed!") from exc + if len(fidelities) == 1: fidelities = fidelities[0] diff --git a/qiskit/algorithms/time_evolvers/pvqd/utils.py b/qiskit/algorithms/time_evolvers/pvqd/utils.py index 27db241f3f37..17bf6ec1bf9d 100644 --- a/qiskit/algorithms/time_evolvers/pvqd/utils.py +++ b/qiskit/algorithms/time_evolvers/pvqd/utils.py @@ -18,6 +18,7 @@ import numpy as np +from qiskit.algorithms import AlgorithmError from qiskit.circuit import QuantumCircuit, Parameter, ParameterExpression from qiskit.compiler import transpile from qiskit.exceptions import QiskitError @@ -84,6 +85,9 @@ def evaluate_observables(theta: np.ndarray) -> float | list[float]: Returns: The observables evaluated at the ansatz parameters. + + Raises: + AlgorithmError: If a primitive job fails. """ if isinstance(observables, list): num_observables = len(observables) @@ -94,8 +98,12 @@ def evaluate_observables(theta: np.ndarray) -> float | list[float]: states = [ansatz] * num_observables parameter_values = [theta] * num_observables - estimator_job = estimator.run(states, obs, parameter_values=parameter_values) + 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 estimator_job.result().values + return results return evaluate_observables From 1dce47391e218a01838004e285335109b8ad72b9 Mon Sep 17 00:00:00 2001 From: Dariusz Lasecki Date: Fri, 9 Sep 2022 14:36:45 +0200 Subject: [PATCH 39/48] Black fix. --- qiskit/algorithms/time_evolvers/time_evolution_problem.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/qiskit/algorithms/time_evolvers/time_evolution_problem.py b/qiskit/algorithms/time_evolvers/time_evolution_problem.py index ceb4b97c4175..ec058bdfb4b0 100644 --- a/qiskit/algorithms/time_evolvers/time_evolution_problem.py +++ b/qiskit/algorithms/time_evolvers/time_evolution_problem.py @@ -54,7 +54,8 @@ def __init__( aux_operators: ListOrDict[BaseOperator | PauliSumOp] | None = None, truncation_threshold: float = 1e-12, t_param: Parameter | None = None, - param_value_dict: Mapping[Parameter, complex] | None = None, # parametrization will become supported in BaseOperator soon + param_value_dict: Mapping[Parameter, complex] + | None = None, # parametrization will become supported in BaseOperator soon ): """ Args: From d3c601e2e66e5fb8efd085ea761d02cdfd2e77de Mon Sep 17 00:00:00 2001 From: Dariusz Lasecki Date: Sun, 11 Sep 2022 10:30:26 +0200 Subject: [PATCH 40/48] Applied CR comments. --- qiskit/algorithms/evolvers/evolution_problem.py | 4 ++-- qiskit/algorithms/evolvers/evolution_result.py | 4 ++-- qiskit/algorithms/evolvers/imaginary_evolver.py | 4 ++-- qiskit/algorithms/evolvers/real_evolver.py | 4 ++-- .../time_evolvers/time_evolution_problem.py | 13 ++++++------- .../time_evolvers/time_evolution_result.py | 4 ++-- .../time_evolvers/test_time_evolution_problem.py | 6 +++--- 7 files changed, 19 insertions(+), 20 deletions(-) diff --git a/qiskit/algorithms/evolvers/evolution_problem.py b/qiskit/algorithms/evolvers/evolution_problem.py index bc815c7425d5..cd34c5a34f8a 100644 --- a/qiskit/algorithms/evolvers/evolution_problem.py +++ b/qiskit/algorithms/evolvers/evolution_problem.py @@ -25,7 +25,7 @@ class EvolutionProblem: """Pending deprecation: Evolution problem class. The EvolutionProblem class has been superseded by the - :class:`qiskit.algorithms.time_evolvers.EvolutionProblem` class. + :class:`qiskit.algorithms.time_evolvers.TimeEvolutionProblem` class. This class will be deprecated in a future release and subsequently removed after that. @@ -35,7 +35,7 @@ class EvolutionProblem: @deprecate_function( "The EvolutionProblem class has been superseded by the " - "qiskit.algorithms.time_evolvers.EvolutionProblem class. " + "qiskit.algorithms.time_evolvers.TimeEvolutionProblem class. " "This class will be deprecated in a future release and subsequently " "removed after that.", category=PendingDeprecationWarning, diff --git a/qiskit/algorithms/evolvers/evolution_result.py b/qiskit/algorithms/evolvers/evolution_result.py index 1f2c1d50c5de..9ae567ca91c9 100644 --- a/qiskit/algorithms/evolvers/evolution_result.py +++ b/qiskit/algorithms/evolvers/evolution_result.py @@ -25,7 +25,7 @@ class EvolutionResult(AlgorithmResult): """Pending deprecation: Class for holding evolution result. The EvolutionResult class has been superseded by the - :class:`qiskit.algorithms.time_evolvers.EvolutionResult` class. + :class:`qiskit.algorithms.time_evolvers.TimeEvolutionResult` class. This class will be deprecated in a future release and subsequently removed after that. @@ -33,7 +33,7 @@ class EvolutionResult(AlgorithmResult): @deprecate_function( "The EvolutionResult class has been superseded by the " - "qiskit.algorithms.time_evolvers.EvolutionResult class. " + "qiskit.algorithms.time_evolvers.TimeEvolutionResult class. " "This class will be deprecated in a future release and subsequently " "removed after that.", category=PendingDeprecationWarning, diff --git a/qiskit/algorithms/evolvers/imaginary_evolver.py b/qiskit/algorithms/evolvers/imaginary_evolver.py index 520de1bbaae1..37a24d266a30 100644 --- a/qiskit/algorithms/evolvers/imaginary_evolver.py +++ b/qiskit/algorithms/evolvers/imaginary_evolver.py @@ -23,7 +23,7 @@ class ImaginaryEvolver(ABC): """Pending deprecation: Interface for Quantum Imaginary Time Evolution. The ImaginaryEvolver interface has been superseded by the - :class:`qiskit.algorithms.time_evolvers.ImaginaryEvolver` interface. + :class:`qiskit.algorithms.time_evolvers.ImaginaryTimeEvolver` interface. This interface will be deprecated in a future release and subsequently removed after that. @@ -31,7 +31,7 @@ class ImaginaryEvolver(ABC): @deprecate_function( "The ImaginaryEvolver interface has been superseded by the " - "qiskit.algorithms.time_evolvers.ImaginaryEvolver interface. " + "qiskit.algorithms.time_evolvers.ImaginaryTimeEvolver interface. " "This interface will be deprecated in a future release and subsequently " "removed after that.", category=PendingDeprecationWarning, diff --git a/qiskit/algorithms/evolvers/real_evolver.py b/qiskit/algorithms/evolvers/real_evolver.py index 0ccaad7fa9b2..c869344a19ba 100644 --- a/qiskit/algorithms/evolvers/real_evolver.py +++ b/qiskit/algorithms/evolvers/real_evolver.py @@ -23,7 +23,7 @@ class RealEvolver(ABC): """Pending deprecation: Interface for Quantum Real Time Evolution. The RealEvolver interface has been superseded by the - :class:`qiskit.algorithms.time_evolvers.RealEvolver` interface. + :class:`qiskit.algorithms.time_evolvers.RealTimeEvolver` interface. This interface will be deprecated in a future release and subsequently removed after that. @@ -31,7 +31,7 @@ class RealEvolver(ABC): @deprecate_function( "The RealEvolver interface has been superseded by the " - "qiskit.algorithms.time_evolvers.RealEvolver interface. " + "qiskit.algorithms.time_evolvers.RealTimeEvolver interface. " "This interface will be deprecated in a future release and subsequently " "removed after that.", category=PendingDeprecationWarning, diff --git a/qiskit/algorithms/time_evolvers/time_evolution_problem.py b/qiskit/algorithms/time_evolvers/time_evolution_problem.py index ec058bdfb4b0..9b2226f4764f 100644 --- a/qiskit/algorithms/time_evolvers/time_evolution_problem.py +++ b/qiskit/algorithms/time_evolvers/time_evolution_problem.py @@ -10,7 +10,7 @@ # copyright notice, and modified files need to carry a notice indicating # that they have been altered from the originals. -"""Evolution problem class.""" +"""Time evolution problem class.""" from __future__ import annotations from collections.abc import Mapping @@ -24,7 +24,7 @@ class TimeEvolutionProblem: - """Evolution problem class. + """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. @@ -41,7 +41,7 @@ class TimeEvolutionProblem: 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_dict (dict[Parameter, complex] | None): Maps free parameters in the problem to + 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. """ @@ -54,8 +54,7 @@ def __init__( aux_operators: ListOrDict[BaseOperator | PauliSumOp] | None = None, truncation_threshold: float = 1e-12, t_param: Parameter | None = None, - param_value_dict: Mapping[Parameter, complex] - | None = None, # parametrization will become supported in BaseOperator soon + param_value_map: Mapping[Parameter, complex] | None = None, ): """ Args: @@ -70,7 +69,7 @@ def __init__( 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 + 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: @@ -78,7 +77,7 @@ def __init__( """ self.t_param = t_param - self.param_value_dict = param_value_dict + self.param_value_map = param_value_map self.hamiltonian = hamiltonian self.time = time if isinstance(initial_state, Statevector): diff --git a/qiskit/algorithms/time_evolvers/time_evolution_result.py b/qiskit/algorithms/time_evolvers/time_evolution_result.py index 2ff47e3d08e5..60c900945543 100644 --- a/qiskit/algorithms/time_evolvers/time_evolution_result.py +++ b/qiskit/algorithms/time_evolvers/time_evolution_result.py @@ -10,7 +10,7 @@ # copyright notice, and modified files need to carry a notice indicating # that they have been altered from the originals. -"""Class for holding evolution result.""" +"""Class for holding time evolution result.""" from __future__ import annotations from qiskit import QuantumCircuit @@ -20,7 +20,7 @@ class TimeEvolutionResult(AlgorithmResult): """ - Class for holding evolution result. + Class for holding time evolution result. Attributes: evolved_state (QuantumCircuit): An evolved quantum state. diff --git a/test/python/algorithms/time_evolvers/test_time_evolution_problem.py b/test/python/algorithms/time_evolvers/test_time_evolution_problem.py index 227583f8aef7..83a7fd4d5e3a 100644 --- a/test/python/algorithms/time_evolvers/test_time_evolution_problem.py +++ b/test/python/algorithms/time_evolvers/test_time_evolution_problem.py @@ -46,7 +46,7 @@ def test_init_default(self): 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) + self.assertEqual(evo_problem.param_value_map, expected_param_value_dict) @data(QuantumCircuit(1), Statevector([1, 0])) def test_init_all(self, initial_state): @@ -63,7 +63,7 @@ def test_init_all(self, initial_state): initial_state, aux_operators, t_param=t_parameter, - param_value_dict=param_value_dict, + param_value_map=param_value_dict, ) expected_hamiltonian = Y + t_parameter * Z @@ -78,7 +78,7 @@ def test_init_all(self, initial_state): 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_dict, expected_param_value_dict) + self.assertEqual(evo_problem.param_value_map, expected_param_value_dict) @data([Y, -1, One], [Y, -1.2, One], [Y, 0, One]) @unpack From 17b9dfa51cc28efbceebc81053214688b21fb7b7 Mon Sep 17 00:00:00 2001 From: Dariusz Lasecki Date: Tue, 13 Sep 2022 20:39:08 +0100 Subject: [PATCH 41/48] Init updates. --- qiskit/algorithms/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/qiskit/algorithms/__init__.py b/qiskit/algorithms/__init__.py index 983b2094a92f..3abe29812330 100644 --- a/qiskit/algorithms/__init__.py +++ b/qiskit/algorithms/__init__.py @@ -121,8 +121,6 @@ TrotterQRTE VarQITE VarQRTE - PVQD - PVQDResult EvolutionResult EvolutionProblem @@ -140,6 +138,8 @@ RealTimeEvolver ImaginaryTimeEvolver + PVQD + PVQDResult TimeEvolutionResult TimeEvolutionProblem From 142808682b154b71f9d29946e2720997bdc4f2cd Mon Sep 17 00:00:00 2001 From: Dariusz Lasecki Date: Thu, 15 Sep 2022 13:51:38 +0200 Subject: [PATCH 42/48] Reduced number of cyclic import. --- qiskit/algorithms/time_evolvers/pvqd/pvqd.py | 7 +++---- qiskit/algorithms/time_evolvers/pvqd/utils.py | 2 +- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/qiskit/algorithms/time_evolvers/pvqd/pvqd.py b/qiskit/algorithms/time_evolvers/pvqd/pvqd.py index 3b9efec7ec79..bdd274f85cfa 100644 --- a/qiskit/algorithms/time_evolvers/pvqd/pvqd.py +++ b/qiskit/algorithms/time_evolvers/pvqd/pvqd.py @@ -19,21 +19,20 @@ import numpy as np -from qiskit import QiskitError -from qiskit.algorithms.optimizers import Optimizer, Minimizer from qiskit.circuit import QuantumCircuit, ParameterVector, Parameter 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 ...exceptions import AlgorithmError, QiskitError from .pvqd_result import PVQDResult from .utils import _get_observable_evaluator, _is_gradient_supported from ..time_evolution_problem import TimeEvolutionProblem from ..time_evolution_result import TimeEvolutionResult from ..real_time_evolver import RealTimeEvolver -from ... import AlgorithmError -from ...state_fidelities import BaseStateFidelity +from ...state_fidelities.base_state_fidelity import BaseStateFidelity +from ...optimizers import Optimizer, Minimizer logger = logging.getLogger(__name__) diff --git a/qiskit/algorithms/time_evolvers/pvqd/utils.py b/qiskit/algorithms/time_evolvers/pvqd/utils.py index 17bf6ec1bf9d..47e638d97011 100644 --- a/qiskit/algorithms/time_evolvers/pvqd/utils.py +++ b/qiskit/algorithms/time_evolvers/pvqd/utils.py @@ -18,13 +18,13 @@ import numpy as np -from qiskit.algorithms import AlgorithmError 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__) From e40e175c625f2db16c4b98b232275006ec59efcb Mon Sep 17 00:00:00 2001 From: woodsp-ibm Date: Tue, 20 Sep 2022 13:36:14 -0400 Subject: [PATCH 43/48] Fix for cyclic import --- qiskit/algorithms/state_fidelities/base_state_fidelity.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qiskit/algorithms/state_fidelities/base_state_fidelity.py b/qiskit/algorithms/state_fidelities/base_state_fidelity.py index 75f4d632396d..a711adacf0fa 100644 --- a/qiskit/algorithms/state_fidelities/base_state_fidelity.py +++ b/qiskit/algorithms/state_fidelities/base_state_fidelity.py @@ -19,7 +19,7 @@ import numpy as np from qiskit import QuantumCircuit -from qiskit.algorithms import AlgorithmJob +from qiskit.algorithms.algorithm_job import AlgorithmJob from qiskit.circuit import ParameterVector from .state_fidelity_result import StateFidelityResult From a30dcb09babe6f37b2cb4637a4a9bb1c6e5c56cd Mon Sep 17 00:00:00 2001 From: woodsp-ibm Date: Tue, 20 Sep 2022 14:25:56 -0400 Subject: [PATCH 44/48] Doc fix --- qiskit/algorithms/time_evolvers/pvqd/pvqd.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qiskit/algorithms/time_evolvers/pvqd/pvqd.py b/qiskit/algorithms/time_evolvers/pvqd/pvqd.py index bdd274f85cfa..3f7a7df624c1 100644 --- a/qiskit/algorithms/time_evolvers/pvqd/pvqd.py +++ b/qiskit/algorithms/time_evolvers/pvqd/pvqd.py @@ -70,7 +70,7 @@ class PVQD(RealTimeEvolver): .. code-block:: python - import numpy as np + import numpy as np from qiskit.algorithms.state_fidelities import ComputeUncompute from qiskit.algorithms.time_evolvers.pvqd import PVQD From 88d3e5c88f36cac51a28ed645dc3154eb6eb28ad Mon Sep 17 00:00:00 2001 From: Dariusz Lasecki Date: Wed, 21 Sep 2022 09:09:41 +0200 Subject: [PATCH 45/48] Updated init. --- qiskit/algorithms/__init__.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/qiskit/algorithms/__init__.py b/qiskit/algorithms/__init__.py index 2525cffabe75..d70f1cde60b3 100644 --- a/qiskit/algorithms/__init__.py +++ b/qiskit/algorithms/__init__.py @@ -113,10 +113,8 @@ RealEvolver ImaginaryEvolver TrotterQRTE - PVQD - PVQDResult - TimeEvolutionResult - TimeEvolutionProblem + EvolutionResult + EvolutionProblem Time Evolvers @@ -132,6 +130,8 @@ RealTimeEvolver ImaginaryTimeEvolver + PVQD + PVQDResult TimeEvolutionResult TimeEvolutionProblem From c297048c05170c0bdd902aba2a6a456b01933d54 Mon Sep 17 00:00:00 2001 From: Dariusz Lasecki Date: Wed, 21 Sep 2022 09:10:00 +0200 Subject: [PATCH 46/48] Added error raising and unit test. --- qiskit/algorithms/time_evolvers/pvqd/pvqd.py | 7 +++---- .../algorithms/time_evolvers/test_pvqd.py | 20 +++++++++++++++++++ 2 files changed, 23 insertions(+), 4 deletions(-) diff --git a/qiskit/algorithms/time_evolvers/pvqd/pvqd.py b/qiskit/algorithms/time_evolvers/pvqd/pvqd.py index 3f7a7df624c1..d4e2ef53bb69 100644 --- a/qiskit/algorithms/time_evolvers/pvqd/pvqd.py +++ b/qiskit/algorithms/time_evolvers/pvqd/pvqd.py @@ -14,7 +14,6 @@ from __future__ import annotations import logging -import warnings from typing import Callable import numpy as np @@ -334,7 +333,8 @@ def evolve(self, evolution_problem: TimeEvolutionProblem) -> TimeEvolutionResult A result object containing the evolution information and evaluated observables. Raises: - ValueError: If the evolution time is not positive or the timestep is too small. + 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() @@ -357,9 +357,8 @@ def evolve(self, evolution_problem: TimeEvolutionProblem) -> TimeEvolutionResult # get the function to evaluate the observables for a given set of ansatz parameters if observables is not None: if self.estimator is None: - warnings.warn( + raise ValueError( "The evolution problem contained aux_operators but no estimator was provided. " - "The algorithm continues without calculating these quantities. " ) evaluate_observables = _get_observable_evaluator( self.ansatz, observables, self.estimator diff --git a/test/python/algorithms/time_evolvers/test_pvqd.py b/test/python/algorithms/time_evolvers/test_pvqd.py index 958679683301..8d52b284f0d7 100644 --- a/test/python/algorithms/time_evolvers/test_pvqd.py +++ b/test/python/algorithms/time_evolvers/test_pvqd.py @@ -247,6 +247,26 @@ def test_initial_state_raises(self): with self.assertRaises(NotImplementedError): _ = pvqd.evolve(problem) + def test_aux_ops_raises(self): + """Test passing auxiliary operators with no estimator raises an error.""" + + problem = EvolutionProblem( + 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.""" From 33f05880e7e848b8a9e3cf91f07e7a9ef50c21ce Mon Sep 17 00:00:00 2001 From: dlasecki Date: Thu, 22 Sep 2022 09:07:24 +0200 Subject: [PATCH 47/48] Apply suggestions from code review Co-authored-by: Steve Wood <40241007+woodsp-ibm@users.noreply.github.com> --- qiskit/algorithms/time_evolvers/pvqd/pvqd.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/qiskit/algorithms/time_evolvers/pvqd/pvqd.py b/qiskit/algorithms/time_evolvers/pvqd/pvqd.py index d4e2ef53bb69..58934c8af43e 100644 --- a/qiskit/algorithms/time_evolvers/pvqd/pvqd.py +++ b/qiskit/algorithms/time_evolvers/pvqd/pvqd.py @@ -324,7 +324,11 @@ def _transpose_param_dicts(self, params: dict) -> list[dict[Parameter, float]]: 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. From c730c567923770ca3ae306e05d2c2d510b031e87 Mon Sep 17 00:00:00 2001 From: Dariusz Lasecki Date: Thu, 22 Sep 2022 10:35:23 +0200 Subject: [PATCH 48/48] Fixed code example. --- qiskit/algorithms/time_evolvers/pvqd/pvqd.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/qiskit/algorithms/time_evolvers/pvqd/pvqd.py b/qiskit/algorithms/time_evolvers/pvqd/pvqd.py index 58934c8af43e..bb2418640e8d 100644 --- a/qiskit/algorithms/time_evolvers/pvqd/pvqd.py +++ b/qiskit/algorithms/time_evolvers/pvqd/pvqd.py @@ -76,14 +76,13 @@ class PVQD(RealTimeEvolver): from qiskit.primitives import Estimator from qiskit import BasicAer from qiskit.circuit.library import EfficientSU2 - from qiskit.opflow import X, Z, I - from qiskit.quantum_info import Pauli + from qiskit.quantum_info import Pauli, SparsePauliOp from qiskit.algorithms.optimizers import L_BFGS_B sampler = Sampler() fidelity = ComputeUncompute(sampler) estimator = Estimator() - hamiltonian = 0.1 * (Z ^ Z) + (I ^ X) + (X ^ I) + hamiltonian = 0.1 * SparsePauliOp([Pauli("ZZ"), Pauli("IX"), Pauli("XI")]) observable = Pauli("ZZ") ansatz = EfficientSU2(2, reps=1) initial_parameters = np.zeros(ansatz.num_parameters)