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:
dlasecki 2022-09-22 21:06:14 +02:00 committed by GitHub
parent 0f688eb305
commit cde535cf3d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 231 additions and 195 deletions

View File

@ -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",

View File

@ -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

View File

@ -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:

View File

@ -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:

View File

@ -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

View File

@ -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

View File

@ -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()