mirror of https://github.com/Qiskit/qiskit.git
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 <Manoel.Marques@ibm.com> Co-authored-by: woodsp-ibm <woodsp@us.ibm.com> Co-authored-by: Steve Wood <40241007+woodsp-ibm@users.noreply.github.com> Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
This commit is contained in:
parent
0f688eb305
commit
cde535cf3d
|
@ -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",
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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:
|
|
@ -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:
|
|
@ -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
|
|
@ -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. <https://quantum-journal.org/papers/q-2021-07-28-512/>`_.
|
||||
|
||||
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
|
|
@ -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()
|
Loading…
Reference in New Issue