From cde535cf3dc19719e743e60ee092870b094e1a94 Mon Sep 17 00:00:00 2001 From: dlasecki Date: Thu, 22 Sep 2022 21:06:14 +0200 Subject: [PATCH] Implemented PVQD algorithm with primitives. (#8705) * Implemented observables_evaluator.py with primitives. * Added evolvers problems and interfaces to time_evolvers package. * Mostly updated trotter_qrte.py to use primitives. * Added observables_evaluator.py that uses primitives. * Added observables_evaluator.py that uses primitives. * Updated trotter_qrte.py to use primitives. * Updated imports * Added estimator to pvqd.py (draft) * Updated typehints and limited use of opflow. * Updated typehints and limited use of opflow. * Removed files out of scope for this PR. * Added annotations import. * Added observables_evaluator.py with primitives. * Added ListOrDict support to observables_evaluator.py. * Drafted pvqd.py with the fidelity primitive; type hints updated. * Accepting fidelity primitive. * Included CR suggestions. * Applied some CR comments. * Applied some CR comments. * Added reno. * Added reno. * Accepting Statevector. * Added attributes docs. * Support for 0 operator. * Implemented pvqd unit tests with primitives; code refactoring. * Added reno. * Updated algorithms init. * Code refactoring. * Removed old pvqd algorithm and related files. * Add pending deprecation for evolvers * Renamed classes and linked to algorithms init. * fix docstring * Improved reno. * Updated classes names. * Code refactoring. * Applied CR comments. * Removed unnecessary files. * Applied CR comments. * Black fix. * Applied CR comments. * Init updates. * Reduced number of cyclic import. * Fix for cyclic import * Doc fix * Updated init. * Added error raising and unit test. * Apply suggestions from code review Co-authored-by: Steve Wood <40241007+woodsp-ibm@users.noreply.github.com> * Fixed code example. Co-authored-by: Manoel Marques Co-authored-by: woodsp-ibm Co-authored-by: Steve Wood <40241007+woodsp-ibm@users.noreply.github.com> Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com> --- qiskit/algorithms/__init__.py | 6 +- .../state_fidelities/base_state_fidelity.py | 2 +- .../pvqd/__init__.py | 0 .../{evolvers => time_evolvers}/pvqd/pvqd.py | 165 ++++++++++-------- .../pvqd/pvqd_result.py | 22 ++- .../{evolvers => time_evolvers}/pvqd/utils.py | 47 +++-- ...dynamics-primitives-6003336d0866ca19.yaml} | 23 ++- .../{evolvers => time_evolvers}/test_pvqd.py | 161 +++++++++-------- 8 files changed, 231 insertions(+), 195 deletions(-) rename qiskit/algorithms/{evolvers => time_evolvers}/pvqd/__init__.py (100%) rename qiskit/algorithms/{evolvers => time_evolvers}/pvqd/pvqd.py (74%) rename qiskit/algorithms/{evolvers => time_evolvers}/pvqd/pvqd_result.py (74%) rename qiskit/algorithms/{evolvers => time_evolvers}/pvqd/utils.py (72%) rename releasenotes/notes/{project-dynamics-2f848a5f89655429.yaml => project-dynamics-primitives-6003336d0866ca19.yaml} (58%) rename test/python/algorithms/{evolvers => time_evolvers}/test_pvqd.py (73%) diff --git a/qiskit/algorithms/__init__.py b/qiskit/algorithms/__init__.py index 987951253e..d70f1cde60 100644 --- a/qiskit/algorithms/__init__.py +++ b/qiskit/algorithms/__init__.py @@ -113,8 +113,6 @@ used to train Quantum Boltzmann Machine Neural Networks for example. RealEvolver ImaginaryEvolver TrotterQRTE - PVQD - PVQDResult EvolutionResult EvolutionProblem @@ -132,6 +130,8 @@ Time Evolution might be used to train Quantum Boltzmann Machine Neural Networks RealTimeEvolver ImaginaryTimeEvolver + PVQD + PVQDResult TimeEvolutionResult TimeEvolutionProblem @@ -310,7 +310,7 @@ from .aux_ops_evaluator import eval_observables from .observables_evaluator import estimate_observables from .evolvers.trotterization import TrotterQRTE -from .evolvers.pvqd import PVQD, PVQDResult +from .time_evolvers.pvqd import PVQD, PVQDResult __all__ = [ "AlgorithmJob", diff --git a/qiskit/algorithms/state_fidelities/base_state_fidelity.py b/qiskit/algorithms/state_fidelities/base_state_fidelity.py index 75f4d63239..a711adacf0 100644 --- a/qiskit/algorithms/state_fidelities/base_state_fidelity.py +++ b/qiskit/algorithms/state_fidelities/base_state_fidelity.py @@ -19,7 +19,7 @@ from collections.abc import Sequence, Mapping 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 diff --git a/qiskit/algorithms/evolvers/pvqd/__init__.py b/qiskit/algorithms/time_evolvers/pvqd/__init__.py similarity index 100% rename from qiskit/algorithms/evolvers/pvqd/__init__.py rename to qiskit/algorithms/time_evolvers/pvqd/__init__.py diff --git a/qiskit/algorithms/evolvers/pvqd/pvqd.py b/qiskit/algorithms/time_evolvers/pvqd/pvqd.py similarity index 74% rename from qiskit/algorithms/evolvers/pvqd/pvqd.py rename to qiskit/algorithms/time_evolvers/pvqd/pvqd.py index 323f32aa9d..bb2418640e 100644 --- a/qiskit/algorithms/evolvers/pvqd/pvqd.py +++ b/qiskit/algorithms/time_evolvers/pvqd/pvqd.py @@ -11,33 +11,32 @@ # that they have been altered from the originals. """The projected Variational Quantum Dynamics Algorithm.""" - -from typing import Optional, Union, List, Tuple, Callable +from __future__ import annotations 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.providers import Backend -from qiskit.opflow import OperatorBase, CircuitSampler, ExpectationBase, StateFn, MatrixOp +from qiskit.opflow import PauliSumOp +from qiskit.primitives import BaseEstimator +from qiskit.quantum_info.operators.base_operator import BaseOperator from qiskit.synthesis import EvolutionSynthesis, LieTrotter -from qiskit.utils import QuantumInstance - +from ...exceptions import AlgorithmError, QiskitError 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.base_state_fidelity import BaseStateFidelity +from ...optimizers import Optimizer, Minimizer 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 @@ -52,7 +51,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 @@ -73,14 +71,19 @@ class PVQD(RealEvolver): 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.quantum_info import Pauli, SparsePauliOp + from qiskit.algorithms.optimizers import L_BFGS_B - backend = BasicAer.get_backend("statevector_simulator") - expectation = MatrixExpectation() - hamiltonian = 0.1 * (Z ^ Z) + (I ^ X) + (X ^ I) - observable = Z ^ Z + sampler = Sampler() + fidelity = ComputeUncompute(sampler) + estimator = Estimator() + 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) @@ -89,12 +92,12 @@ class PVQD(RealEvolver): # setup the algorithm pvqd = PVQD( + fidelity, ansatz, + estimator, initial_parameters, num_timesteps=100, optimizer=optimizer, - quantum_instance=backend, - expectation=expectation ) # specify the evolution problem @@ -114,30 +117,32 @@ class PVQD(RealEvolver): def __init__( self, + fidelity: BaseStateFidelity, ansatz: QuantumCircuit, initial_parameters: np.ndarray, - expectation: ExpectationBase, - optimizer: Optional[Union[Optimizer, Minimizer]] = None, - num_timesteps: Optional[int] = None, - evolution: Optional[EvolutionSynthesis] = None, + 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, - quantum_instance: Optional[Union[Backend, QuantumInstance]] = None, + initial_guess: np.ndarray | None = None, ) -> None: """ Args: + fidelity: A fidelity primitive used by the algorithm. ansatz: A parameterized circuit preparing the variational ansatz to model the time evolved quantum state. initial_parameters: The initial parameters for the ansatz. Together with the ansatz, these define the initial state of the time evolution. - expectation: The expectation converter to evaluate expectation values. + estimator: An estimator primitive used for calculating expected values of auxiliary + operators (if provided via the problem). optimizer: The classical optimizers used to minimize the overlap between Trotterization and ansatz. Can be either a :class:`.Optimizer` or a callable using the :class:`.Minimizer` protocol. This argument is optional since it is not required for :meth:`get_loss`, but it has to be set before :meth:`evolve` is called. - num_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. @@ -147,7 +152,6 @@ class PVQD(RealEvolver): 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. """ super().__init__() if evolution is None: @@ -158,36 +162,19 @@ class PVQD(RealEvolver): self.num_timesteps = num_timesteps self.optimizer = optimizer self.initial_guess = initial_guess - self.expectation = expectation + self.estimator = estimator + self.fidelity_primitive = fidelity 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, + hamiltonian: BaseOperator | PauliSumOp, ansatz: QuantumCircuit, theta: np.ndarray, dt: float, initial_guess: np.ndarray, - ) -> Tuple[np.ndarray, float]: + ) -> tuple[np.ndarray, float]: """Perform a single time step. Args: @@ -223,11 +210,11 @@ class PVQD(RealEvolver): 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. @@ -247,23 +234,15 @@ class PVQD(RealEvolver): # 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) # 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: @@ -271,6 +250,9 @@ class PVQD(RealEvolver): 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) @@ -278,11 +260,24 @@ class PVQD(RealEvolver): else: value_dict = dict(zip(x, displacement)) - sampled = self._sampler.convert(converted, params=value_dict) + param_dicts = self._transpose_param_dicts(value_dict) + num_of_param_sets = len(param_dicts) + states1 = [trotterized] * num_of_param_sets + states2 = [shifted] * num_of_param_sets + param_dicts2 = [list(param_dict.values()) for param_dict in param_dicts] + # the first state does not have free parameters so values_1 will be None by default + try: + job = self.fidelity_primitive.run(states1, states2, values_2=param_dicts2) + fidelities = job.result().fidelities + except Exception as exc: + raise AlgorithmError("The primitive job failed!") from exc - # in principle we could add different loss functions here, but we're currently + 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(sampled.eval()) ** 2 + return 1 - np.abs(fidelities) ** 2 if _is_gradient_supported(ansatz) and self.use_parameter_shift: @@ -314,8 +309,25 @@ class PVQD(RealEvolver): return evaluate_loss, evaluate_gradient - def evolve(self, evolution_problem: EvolutionProblem) -> EvolutionResult: - """ + def _transpose_param_dicts(self, params: dict) -> list[dict[Parameter, float]]: + p_0 = list(params.values())[0] + if isinstance(p_0, (list, np.ndarray)): + num_parameterizations = len(p_0) + param_bindings = [ + {param: value_list[i] for param, value_list in params.items()} # type: ignore + for i in range(num_parameterizations) + ] + else: + param_bindings = [params] + + return param_bindings + + def evolve(self, evolution_problem: TimeEvolutionProblem) -> TimeEvolutionResult: + r"""Perform real time evolution :math:`\exp(-i t H)|\Psi\rangle`. + + Evolves an initial state :math:`|\Psi\rangle` for a time :math:`t` + under a Hamiltonian :math:`H`, as provided in the ``evolution_problem``. + Args: evolution_problem: The evolution problem containing the hamiltonian, total evolution time and observables to evaluate. @@ -324,7 +336,8 @@ class PVQD(RealEvolver): 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() @@ -346,8 +359,12 @@ class PVQD(RealEvolver): # get the function to evaluate the observables for a given set of ansatz parameters if observables is not None: + if self.estimator is None: + raise ValueError( + "The evolution problem contained aux_operators but no estimator was provided. " + ) evaluate_observables = _get_observable_evaluator( - self.ansatz, observables, self.expectation, self._sampler + self.ansatz, observables, self.estimator ) observable_values = [evaluate_observables(self.initial_parameters)] @@ -392,7 +409,7 @@ class PVQD(RealEvolver): if skip is None: skip = {} - required_attributes = {"quantum_instance", "optimizer"}.difference(skip) + required_attributes = {"optimizer"}.difference(skip) for attr in required_attributes: if getattr(self, attr, None) is None: diff --git a/qiskit/algorithms/evolvers/pvqd/pvqd_result.py b/qiskit/algorithms/time_evolvers/pvqd/pvqd_result.py similarity index 74% rename from qiskit/algorithms/evolvers/pvqd/pvqd_result.py rename to qiskit/algorithms/time_evolvers/pvqd/pvqd_result.py index e14e7a0c9d..6f8a34cb89 100644 --- a/qiskit/algorithms/evolvers/pvqd/pvqd_result.py +++ b/qiskit/algorithms/time_evolvers/pvqd/pvqd_result.py @@ -11,28 +11,26 @@ # that they have been altered from the originals. """Result object for p-VQD.""" +from __future__ import annotations -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 +from ..time_evolution_result import TimeEvolutionResult -class PVQDResult(EvolutionResult): +class PVQDResult(TimeEvolutionResult): """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, + 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/evolvers/pvqd/utils.py b/qiskit/algorithms/time_evolvers/pvqd/utils.py similarity index 72% rename from qiskit/algorithms/evolvers/pvqd/utils.py rename to qiskit/algorithms/time_evolvers/pvqd/utils.py index 589f12005d..47e638d970 100644 --- a/qiskit/algorithms/evolvers/pvqd/utils.py +++ b/qiskit/algorithms/time_evolvers/pvqd/utils.py @@ -12,17 +12,19 @@ """Utilities for p-VQD.""" - -from typing import Union, List, Callable +from __future__ import annotations import logging +from typing import Callable import numpy as np from qiskit.circuit import QuantumCircuit, Parameter, ParameterExpression from qiskit.compiler import transpile from qiskit.exceptions import QiskitError -from qiskit.opflow import ListOp, CircuitSampler, ExpectationBase, StateFn, OperatorBase 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__) @@ -70,21 +72,12 @@ def _is_gradient_supported(ansatz: QuantumCircuit) -> bool: def _get_observable_evaluator( ansatz: QuantumCircuit, - observables: Union[OperatorBase, List[OperatorBase]], - expectation: ExpectationBase, - sampler: CircuitSampler, -) -> 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) - - 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]]: + def evaluate_observables(theta: np.ndarray) -> float | list[float]: """Evaluate the observables for the ansatz parameters ``theta``. Args: @@ -92,9 +85,25 @@ def _get_observable_evaluator( Returns: The observables evaluated at the ansatz parameters. + + Raises: + AlgorithmError: If a primitive job fails. """ - value_dict = dict(zip(ansatz_parameters, theta)) - sampled = sampler.convert(converted, params=value_dict) - return sampled.eval() + if isinstance(observables, list): + num_observables = len(observables) + obs = observables + else: + num_observables = 1 + obs = [observables] + states = [ansatz] * num_observables + parameter_values = [theta] * num_observables + + try: + estimator_job = estimator.run(states, obs, parameter_values=parameter_values) + results = estimator_job.result().values + except Exception as exc: + raise AlgorithmError("The primitive job failed!") from exc + + return results return evaluate_observables diff --git a/releasenotes/notes/project-dynamics-2f848a5f89655429.yaml b/releasenotes/notes/project-dynamics-primitives-6003336d0866ca19.yaml similarity index 58% rename from releasenotes/notes/project-dynamics-2f848a5f89655429.yaml rename to releasenotes/notes/project-dynamics-primitives-6003336d0866ca19.yaml index a33fdfefed..a665b63319 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,19 @@ 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.quantum_info import Pauli, SparsePauliOp + from qiskit.algorithms.optimizers import L_BFGS_B - backend = BasicAer.get_backend("statevector_simulator") - expectation = MatrixExpectation() - hamiltonian = 0.1 * (Z ^ Z) + (I ^ X) + (X ^ I) - observable = Z ^ Z + sampler = Sampler() + fidelity = ComputeUncompute(sampler) + estimator = Estimator() + 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) @@ -28,12 +33,12 @@ features: # setup the algorithm pvqd = PVQD( + fidelity, ansatz, + estimator, initial_parameters, num_timesteps=100, optimizer=optimizer, - quantum_instance=backend, - expectation=expectation ) # specify the evolution problem diff --git a/test/python/algorithms/evolvers/test_pvqd.py b/test/python/algorithms/time_evolvers/test_pvqd.py similarity index 73% rename from test/python/algorithms/evolvers/test_pvqd.py rename to test/python/algorithms/time_evolvers/test_pvqd.py index 5c450fa248..8d52b284f0 100644 --- a/test/python/algorithms/evolvers/test_pvqd.py +++ b/test/python/algorithms/time_evolvers/test_pvqd.py @@ -11,20 +11,23 @@ # 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.opflow import PauliSumOp +from qiskit.primitives import Sampler, Estimator +from qiskit.quantum_info import Pauli, SparsePauliOp from qiskit.test import QiskitTestCase - -from qiskit import BasicAer, QiskitError +from qiskit import QiskitError from qiskit.circuit import QuantumCircuit, Parameter, Gate from qiskit.algorithms.evolvers import EvolutionProblem -from qiskit.algorithms.evolvers.pvqd import PVQD +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, MatrixExpectation, PauliExpectation # pylint: disable=unused-argument, invalid-name @@ -39,7 +42,7 @@ def gradient_supplied(fun, x0, jac, info): class WhatAmI(Gate): - """An custom opaque gate that can be inverted but not decomposed.""" + """A custom opaque gate that can be inverted but not decomposed.""" def __init__(self, angle): super().__init__(name="whatami", num_qubits=2, params=[angle]) @@ -54,31 +57,23 @@ class TestPVQD(QiskitTestCase): 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.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", MatrixExpectation, True, "sv", 2), - ("ising_matrix", MatrixExpectation, True, "sv", None), - ("ising", PauliExpectation, True, "qasm", 2), - ("pauli", PauliExpectation, False, "qasm", None), - ) + @data(("ising", True, 2), ("pauli", False, None), ("pauli_sum_op", True, 2)) @unpack - def test_pvqd(self, hamiltonian_type, expectation_cls, gradient, backend_type, num_timesteps): + def test_pvqd(self, hamiltonian_type, gradient, num_timesteps): """Test a simple evolution.""" time = 0.02 if hamiltonian_type == "ising": hamiltonian = self.hamiltonian - elif hamiltonian_type == "ising_matrix": - hamiltonian = self.hamiltonian.to_matrix_op() + elif hamiltonian_type == "pauli_sum_op": + hamiltonian = PauliSumOp(self.hamiltonian) else: # hamiltonian_type == "pauli": - hamiltonian = X ^ X + hamiltonian = Pauli("XX") # parse input arguments if gradient: @@ -86,17 +81,18 @@ class TestPVQD(QiskitTestCase): else: optimizer = L_BFGS_B(maxiter=1) - backend = self.sv_backend if backend_type == "sv" else self.qasm_backend - expectation = expectation_cls() + sampler = Sampler() + estimator = Estimator() + fidelity_primitive = ComputeUncompute(sampler) # run pVQD keeping track of the energy and the magnetization pvqd = PVQD( + fidelity_primitive, self.ansatz, self.initial_parameters, - num_timesteps=num_timesteps, + estimator, optimizer=optimizer, - quantum_instance=backend, - expectation=expectation, + num_timesteps=num_timesteps, ) problem = EvolutionProblem(hamiltonian, time, aux_operators=[hamiltonian, self.observable]) result = pvqd.evolve(problem) @@ -112,19 +108,21 @@ class TestPVQD(QiskitTestCase): def test_step(self): """Test calling the step method directly.""" - + sampler = Sampler() + estimator = Estimator() + fidelity_primitive = ComputeUncompute(sampler) pvqd = PVQD( + fidelity_primitive, self.ansatz, self.initial_parameters, + estimator, optimizer=L_BFGS_B(maxiter=100), - 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.hamiltonian, self.ansatz, self.initial_parameters, dt=0.0, @@ -137,11 +135,15 @@ class TestPVQD(QiskitTestCase): def test_get_loss(self): """Test getting the loss function directly.""" + sampler = Sampler() + estimator = Estimator() + fidelity_primitive = ComputeUncompute(sampler) + pvqd = PVQD( + fidelity_primitive, self.ansatz, self.initial_parameters, - quantum_instance=self.sv_backend, - expectation=MatrixExpectation(), + estimator, use_parameter_shift=False, ) @@ -161,13 +163,16 @@ class TestPVQD(QiskitTestCase): def test_invalid_num_timestep(self): """Test raises if the num_timestep is not positive.""" + sampler = Sampler() + estimator = Estimator() + fidelity_primitive = ComputeUncompute(sampler) pvqd = PVQD( + fidelity_primitive, self.ansatz, self.initial_parameters, - num_timesteps=0, + estimator, optimizer=L_BFGS_B(), - quantum_instance=self.sv_backend, - expectation=self.expectation, + num_timesteps=0, ) problem = EvolutionProblem( self.hamiltonian, time=0.01, aux_operators=[self.hamiltonian, self.observable] @@ -179,15 +184,18 @@ class TestPVQD(QiskitTestCase): def test_initial_guess_and_observables(self): """Test doing no optimizations stays at initial guess.""" initial_guess = np.zeros(self.ansatz.num_parameters) + sampler = Sampler() + estimator = Estimator() + fidelity_primitive = ComputeUncompute(sampler) pvqd = PVQD( + fidelity_primitive, self.ansatz, self.initial_parameters, - num_timesteps=10, + estimator, optimizer=SPSA(maxiter=0, learning_rate=0.1, perturbation=0.01), + num_timesteps=10, 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] @@ -199,46 +207,17 @@ class TestPVQD(QiskitTestCase): 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) + sampler = Sampler() + fidelity_primitive = ComputeUncompute(sampler) pvqd = PVQD( + fidelity_primitive, QuantumCircuit(2), np.array([]), optimizer=SPSA(maxiter=10, learning_rate=0.1, perturbation=0.01), - quantum_instance=self.sv_backend, - expectation=self.expectation, ) with self.assertRaises(QiskitError): @@ -255,26 +234,46 @@ class TestPVQD(QiskitTestCase): initial_state=initial_state, ) + sampler = Sampler() + fidelity_primitive = ComputeUncompute(sampler) + pvqd = PVQD( + fidelity_primitive, self.ansatz, self.initial_parameters, optimizer=SPSA(maxiter=0, learning_rate=0.1, perturbation=0.01), - quantum_instance=self.sv_backend, - expectation=self.expectation, ) 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.""" 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.hamiltonian = 0.1 * SparsePauliOp([Pauli("ZZ"), Pauli("IX"), Pauli("XI")]) self.ansatz = EfficientSU2(2, reps=1) def test_gradient_supported(self): @@ -309,12 +308,16 @@ class TestPVQDUtils(QiskitTestCase): info = {"has_gradient": None} optimizer = partial(gradient_supplied, info=info) + sampler = Sampler() + estimator = Estimator() + fidelity_primitive = ComputeUncompute(sampler) + pvqd = PVQD( + fidelity=fidelity_primitive, ansatz=None, initial_parameters=np.array([]), + estimator=estimator, optimizer=optimizer, - quantum_instance=self.sv_backend, - expectation=self.expectation, ) problem = EvolutionProblem(self.hamiltonian, time=0.01) for circuit, expected_support in tests: @@ -323,3 +326,7 @@ class TestPVQDUtils(QiskitTestCase): pvqd.initial_parameters = np.zeros(circuit.num_parameters) _ = pvqd.evolve(problem) self.assertEqual(info["has_gradient"], expected_support) + + +if __name__ == "__main__": + unittest.main()