mirror of https://github.com/Qiskit/qiskit.git
Projected Variational Quantum Dynamics (#8304)
* pvqd dradt * update as theta + difference * black * refactor test * update to new time evo framework * add gradients * use gradient only if supported * polishing! - reno - remove old pvqd file - allow attributes to be None - more tests * fix algorithms import * changes from code review * add more tests for different ops * refactor PVQD to multiple files * remove todo * comments from review * rm OrderedDict from unitary no idea why this unused import existed, that should be caught before this PR? * changes from code review * remove function to attach intial states * include comments from review - support MatrixOp - default for timestep - update reno with refs - test step and get_loss * make only quantum instance and optimizer optional, and use num_timesteps * fix docs * add comment why Optimizer is optional * use class attributes to document mutable attrs * rm duplicate quantum_instance doc * fix attributes docs Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
This commit is contained in:
parent
6a1a10287e
commit
37057987f4
|
@ -108,6 +108,8 @@ used to train Quantum Boltzmann Machine Neural Networks for example.
|
|||
RealEvolver
|
||||
ImaginaryEvolver
|
||||
TrotterQRTE
|
||||
PVQD
|
||||
PVQDResult
|
||||
EvolutionResult
|
||||
EvolutionProblem
|
||||
|
||||
|
@ -246,6 +248,7 @@ from .phase_estimators import (
|
|||
from .exceptions import AlgorithmError
|
||||
from .aux_ops_evaluator import eval_observables
|
||||
from .evolvers.trotterization import TrotterQRTE
|
||||
from .evolvers.pvqd import PVQD, PVQDResult
|
||||
|
||||
__all__ = [
|
||||
"AlgorithmResult",
|
||||
|
@ -292,6 +295,8 @@ __all__ = [
|
|||
"PhaseEstimationScale",
|
||||
"PhaseEstimation",
|
||||
"PhaseEstimationResult",
|
||||
"PVQD",
|
||||
"PVQDResult",
|
||||
"IterativePhaseEstimation",
|
||||
"AlgorithmError",
|
||||
"eval_observables",
|
||||
|
|
|
@ -31,7 +31,7 @@ class EvolutionProblem:
|
|||
self,
|
||||
hamiltonian: OperatorBase,
|
||||
time: float,
|
||||
initial_state: Union[StateFn, QuantumCircuit],
|
||||
initial_state: Optional[Union[StateFn, QuantumCircuit]] = None,
|
||||
aux_operators: Optional[ListOrDict[OperatorBase]] = None,
|
||||
truncation_threshold: float = 1e-12,
|
||||
t_param: Optional[Parameter] = None,
|
||||
|
@ -41,7 +41,9 @@ class EvolutionProblem:
|
|||
Args:
|
||||
hamiltonian: The Hamiltonian under which to evolve the system.
|
||||
time: Total time of evolution.
|
||||
initial_state: Quantum state to be evolved.
|
||||
initial_state: The quantum state to be evolved for methods like Trotterization.
|
||||
For variational time evolutions, where the evolution happens in an ansatz,
|
||||
this argument is not required.
|
||||
aux_operators: Optional list of auxiliary operators to be evaluated with the
|
||||
evolved ``initial_state`` and their expectation values returned.
|
||||
truncation_threshold: Defines a threshold under which values can be assumed to be 0.
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
# This code is part of Qiskit.
|
||||
#
|
||||
# (C) Copyright IBM 2022.
|
||||
#
|
||||
# This code is licensed under the Apache License, Version 2.0. You may
|
||||
# obtain a copy of this license in the LICENSE.txt file in the root directory
|
||||
# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.
|
||||
#
|
||||
# Any modifications or derivative works of this code must retain this
|
||||
# copyright notice, and modified files need to carry a notice indicating
|
||||
# that they have been altered from the originals.
|
||||
|
||||
"""The projected Variational Quantum Dynamic (p-VQD) module."""
|
||||
|
||||
from .pvqd_result import PVQDResult
|
||||
from .pvqd import PVQD
|
||||
|
||||
__all__ = ["PVQD", "PVQDResult"]
|
|
@ -0,0 +1,414 @@
|
|||
# This code is part of Qiskit.
|
||||
#
|
||||
# (C) Copyright IBM 2019, 2022.
|
||||
#
|
||||
# This code is licensed under the Apache License, Version 2.0. You may
|
||||
# obtain a copy of this license in the LICENSE.txt file in the root directory
|
||||
# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.
|
||||
#
|
||||
# Any modifications or derivative works of this code must retain this
|
||||
# copyright notice, and modified files need to carry a notice indicating
|
||||
# that they have been altered from the originals.
|
||||
|
||||
"""The projected Variational Quantum Dynamics Algorithm."""
|
||||
|
||||
from typing import Optional, Union, List, Tuple, Callable
|
||||
|
||||
import logging
|
||||
import numpy as np
|
||||
|
||||
from qiskit import QiskitError
|
||||
from qiskit.algorithms.optimizers import Optimizer, Minimizer
|
||||
from qiskit.circuit import QuantumCircuit, ParameterVector
|
||||
from qiskit.circuit.library import PauliEvolutionGate
|
||||
from qiskit.extensions import HamiltonianGate
|
||||
from qiskit.providers import Backend
|
||||
from qiskit.opflow import OperatorBase, CircuitSampler, ExpectationBase, StateFn, MatrixOp
|
||||
from qiskit.synthesis import EvolutionSynthesis, LieTrotter
|
||||
from qiskit.utils import QuantumInstance
|
||||
|
||||
from .pvqd_result import PVQDResult
|
||||
from .utils import _get_observable_evaluator, _is_gradient_supported
|
||||
|
||||
from ..evolution_problem import EvolutionProblem
|
||||
from ..evolution_result import EvolutionResult
|
||||
from ..real_evolver import RealEvolver
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class PVQD(RealEvolver):
|
||||
"""The projected Variational Quantum Dynamics (p-VQD) Algorithm.
|
||||
|
||||
In each timestep, this algorithm computes the next state with a Trotter formula
|
||||
(specified by the ``evolution`` argument) and projects the timestep onto a variational form
|
||||
(``ansatz``). The projection is determined by maximizing the fidelity of the Trotter-evolved
|
||||
state and the ansatz, using a classical optimization routine. See Ref. [1] for details.
|
||||
|
||||
The following attributes can be set via the initializer but can also be read and
|
||||
updated once the PVQD object has been constructed.
|
||||
|
||||
Attributes:
|
||||
|
||||
ansatz (QuantumCircuit): The parameterized circuit representing the time-evolved state.
|
||||
initial_parameters (np.ndarray): The parameters of the ansatz at time 0.
|
||||
expectation (ExpectationBase): The method to compute expectation values.
|
||||
optimizer (Optional[Union[Optimizer, Minimizer]]): The classical optimization routine
|
||||
used to maximize the fidelity of the Trotter step and ansatz.
|
||||
num_timesteps (Optional[int]): The number of timesteps to take. If None, it is automatically
|
||||
selected to achieve a timestep of approximately 0.01.
|
||||
evolution (Optional[EvolutionSynthesis]): The method to perform the Trotter step.
|
||||
Defaults to first-order Lie-Trotter evolution.
|
||||
use_parameter_shift (bool): If True, use the parameter shift rule for loss function
|
||||
gradients (if the ansatz supports).
|
||||
initial_guess (Optional[np.ndarray]): The starting point for the first classical optimization
|
||||
run, at time 0. Defaults to random values in :math:`[-0.01, 0.01]`.
|
||||
|
||||
Example:
|
||||
|
||||
This snippet computes the real time evolution of a quantum Ising model on two
|
||||
neighboring sites and keeps track of the magnetization.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
import numpy as np
|
||||
|
||||
from qiskit import BasicAer
|
||||
from qiskit.circuit.library import EfficientSU2
|
||||
from qiskit.opflow import X, Z, I, MatrixExpectation
|
||||
|
||||
backend = BasicAer.get_backend("statevector_simulator")
|
||||
expectation = MatrixExpectation()
|
||||
hamiltonian = 0.1 * (Z ^ Z) + (I ^ X) + (X ^ I)
|
||||
observable = Z ^ Z
|
||||
ansatz = EfficientSU2(2, reps=1)
|
||||
initial_parameters = np.zeros(ansatz.num_parameters)
|
||||
|
||||
time = 1
|
||||
optimizer = L_BFGS_B()
|
||||
|
||||
# setup the algorithm
|
||||
pvqd = PVQD(
|
||||
ansatz,
|
||||
initial_parameters,
|
||||
num_timesteps=100,
|
||||
optimizer=optimizer,
|
||||
quantum_instance=backend,
|
||||
expectation=expectation
|
||||
)
|
||||
|
||||
# specify the evolution problem
|
||||
problem = EvolutionProblem(
|
||||
hamiltonian, time, aux_operators=[hamiltonian, observable]
|
||||
)
|
||||
|
||||
# and evolve!
|
||||
result = pvqd.evolve(problem)
|
||||
|
||||
References:
|
||||
|
||||
[1] Stefano Barison, Filippo Vicentini, and Giuseppe Carleo (2021), An efficient
|
||||
quantum algorithm for the time evolution of parameterized circuits,
|
||||
`Quantum 5, 512 <https://quantum-journal.org/papers/q-2021-07-28-512/>`_.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
ansatz: QuantumCircuit,
|
||||
initial_parameters: np.ndarray,
|
||||
expectation: ExpectationBase,
|
||||
optimizer: Optional[Union[Optimizer, Minimizer]] = None,
|
||||
num_timesteps: Optional[int] = None,
|
||||
evolution: Optional[EvolutionSynthesis] = None,
|
||||
use_parameter_shift: bool = True,
|
||||
initial_guess: Optional[np.ndarray] = None,
|
||||
quantum_instance: Optional[Union[Backend, QuantumInstance]] = None,
|
||||
) -> None:
|
||||
"""
|
||||
Args:
|
||||
ansatz: A parameterized circuit preparing the variational ansatz to model the
|
||||
time evolved quantum state.
|
||||
initial_parameters: The initial parameters for the ansatz. Together with the ansatz,
|
||||
these define the initial state of the time evolution.
|
||||
expectation: The expectation converter to evaluate expectation values.
|
||||
optimizer: The classical optimizers used to minimize the overlap between
|
||||
Trotterization and ansatz. Can be either a :class:`.Optimizer` or a callable
|
||||
using the :class:`.Minimizer` protocol. This argument is optional since it is
|
||||
not required for :meth:`get_loss`, but it has to be set before :meth:`evolve`
|
||||
is called.
|
||||
num_timestep: The number of time steps. If ``None`` it will be set such that the timestep
|
||||
is close to 0.01.
|
||||
evolution: The evolution synthesis to use for the construction of the Trotter step.
|
||||
Defaults to first-order Lie-Trotter decomposition, see also
|
||||
:mod:`~qiskit.synthesis.evolution` for different options.
|
||||
use_parameter_shift: If True, use the parameter shift rule to compute gradients.
|
||||
If False, the optimizer will not be passed a gradient callable. In that case,
|
||||
Qiskit optimizers will use a finite difference rule to approximate the gradients.
|
||||
initial_guess: The initial guess for the first VQE optimization. Afterwards the
|
||||
previous iteration result is used as initial guess. If None, this is set to
|
||||
a random vector with elements in the interval :math:`[-0.01, 0.01]`.
|
||||
quantum_instance: The backend or quantum instance used to evaluate the circuits.
|
||||
"""
|
||||
if evolution is None:
|
||||
evolution = LieTrotter()
|
||||
|
||||
self.ansatz = ansatz
|
||||
self.initial_parameters = initial_parameters
|
||||
self.num_timesteps = num_timesteps
|
||||
self.optimizer = optimizer
|
||||
self.initial_guess = initial_guess
|
||||
self.expectation = expectation
|
||||
self.evolution = evolution
|
||||
self.use_parameter_shift = use_parameter_shift
|
||||
|
||||
self._sampler = None
|
||||
self.quantum_instance = quantum_instance
|
||||
|
||||
@property
|
||||
def quantum_instance(self) -> Optional[QuantumInstance]:
|
||||
"""Return the current quantum instance."""
|
||||
return self._quantum_instance
|
||||
|
||||
@quantum_instance.setter
|
||||
def quantum_instance(self, quantum_instance: Optional[Union[Backend, QuantumInstance]]) -> None:
|
||||
"""Set the quantum instance and circuit sampler."""
|
||||
if quantum_instance is not None:
|
||||
if not isinstance(quantum_instance, QuantumInstance):
|
||||
quantum_instance = QuantumInstance(quantum_instance)
|
||||
self._sampler = CircuitSampler(quantum_instance)
|
||||
|
||||
self._quantum_instance = quantum_instance
|
||||
|
||||
def step(
|
||||
self,
|
||||
hamiltonian: OperatorBase,
|
||||
ansatz: QuantumCircuit,
|
||||
theta: np.ndarray,
|
||||
dt: float,
|
||||
initial_guess: np.ndarray,
|
||||
) -> Tuple[np.ndarray, float]:
|
||||
"""Perform a single time step.
|
||||
|
||||
Args:
|
||||
hamiltonian: The Hamiltonian under which to evolve.
|
||||
ansatz: The parameterized quantum circuit which attempts to approximate the
|
||||
time-evolved state.
|
||||
theta: The current parameters.
|
||||
dt: The time step.
|
||||
initial_guess: The initial guess for the classical optimization of the
|
||||
fidelity between the next variational state and the Trotter-evolved last state.
|
||||
If None, this is set to a random vector with elements in the interval
|
||||
:math:`[-0.01, 0.01]`.
|
||||
|
||||
Returns:
|
||||
A tuple consisting of the next parameters and the fidelity of the optimization.
|
||||
"""
|
||||
self._validate_setup()
|
||||
|
||||
loss, gradient = self.get_loss(hamiltonian, ansatz, dt, theta)
|
||||
|
||||
if initial_guess is None:
|
||||
initial_guess = np.random.random(self.initial_parameters.size) * 0.01
|
||||
|
||||
if isinstance(self.optimizer, Optimizer):
|
||||
optimizer_result = self.optimizer.minimize(loss, initial_guess, gradient)
|
||||
else:
|
||||
optimizer_result = self.optimizer(loss, initial_guess, gradient)
|
||||
|
||||
# clip the fidelity to [0, 1]
|
||||
fidelity = np.clip(1 - optimizer_result.fun, 0, 1)
|
||||
|
||||
return theta + optimizer_result.x, fidelity
|
||||
|
||||
def get_loss(
|
||||
self,
|
||||
hamiltonian: OperatorBase,
|
||||
ansatz: QuantumCircuit,
|
||||
dt: float,
|
||||
current_parameters: np.ndarray,
|
||||
) -> Tuple[Callable[[np.ndarray], float], Optional[Callable[[np.ndarray], np.ndarray]]]:
|
||||
|
||||
"""Get a function to evaluate the infidelity between Trotter step and ansatz.
|
||||
|
||||
Args:
|
||||
hamiltonian: The Hamiltonian under which to evolve.
|
||||
ansatz: The parameterized quantum circuit which attempts to approximate the
|
||||
time-evolved state.
|
||||
dt: The time step.
|
||||
current_parameters: The current parameters.
|
||||
|
||||
Returns:
|
||||
A callable to evaluate the infidelity and, if gradients are supported and required,
|
||||
a second callable to evaluate the gradient of the infidelity.
|
||||
"""
|
||||
self._validate_setup(skip={"optimizer"})
|
||||
|
||||
# use Trotterization to evolve the current state
|
||||
trotterized = ansatz.bind_parameters(current_parameters)
|
||||
|
||||
if isinstance(hamiltonian, MatrixOp):
|
||||
evolution_gate = HamiltonianGate(hamiltonian.primitive, time=dt)
|
||||
else:
|
||||
evolution_gate = PauliEvolutionGate(hamiltonian, time=dt, synthesis=self.evolution)
|
||||
|
||||
trotterized.append(evolution_gate, ansatz.qubits)
|
||||
|
||||
# define the overlap of the Trotterized state and the ansatz
|
||||
x = ParameterVector("w", ansatz.num_parameters)
|
||||
shifted = ansatz.assign_parameters(current_parameters + x)
|
||||
overlap = StateFn(trotterized).adjoint() @ StateFn(shifted)
|
||||
|
||||
converted = self.expectation.convert(overlap)
|
||||
|
||||
def evaluate_loss(
|
||||
displacement: Union[np.ndarray, List[np.ndarray]]
|
||||
) -> Union[float, List[float]]:
|
||||
"""Evaluate the overlap of the ansatz with the Trotterized evolution.
|
||||
|
||||
Args:
|
||||
displacement: The parameters for the ansatz.
|
||||
|
||||
Returns:
|
||||
The fidelity of the ansatz with parameters ``theta`` and the Trotterized evolution.
|
||||
"""
|
||||
if isinstance(displacement, list):
|
||||
displacement = np.asarray(displacement)
|
||||
value_dict = {x_i: displacement[:, i].tolist() for i, x_i in enumerate(x)}
|
||||
else:
|
||||
value_dict = dict(zip(x, displacement))
|
||||
|
||||
sampled = self._sampler.convert(converted, params=value_dict)
|
||||
|
||||
# in principle we could add different loss functions here, but we're currently
|
||||
# not aware of a use-case for a different one than in the paper
|
||||
return 1 - np.abs(sampled.eval()) ** 2
|
||||
|
||||
if _is_gradient_supported(ansatz) and self.use_parameter_shift:
|
||||
|
||||
def evaluate_gradient(displacement: np.ndarray) -> np.ndarray:
|
||||
"""Evaluate the gradient with the parameter-shift rule.
|
||||
|
||||
This is hardcoded here since the gradient framework does not support computing
|
||||
gradients for overlaps.
|
||||
|
||||
Args:
|
||||
displacement: The parameters for the ansatz.
|
||||
|
||||
Returns:
|
||||
The gradient.
|
||||
"""
|
||||
# construct lists where each element is shifted by plus (or minus) pi/2
|
||||
dim = displacement.size
|
||||
plus_shifts = (displacement + np.pi / 2 * np.identity(dim)).tolist()
|
||||
minus_shifts = (displacement - np.pi / 2 * np.identity(dim)).tolist()
|
||||
|
||||
evaluated = evaluate_loss(plus_shifts + minus_shifts)
|
||||
|
||||
gradient = (evaluated[:dim] - evaluated[dim:]) / 2
|
||||
|
||||
return gradient
|
||||
|
||||
else:
|
||||
evaluate_gradient = None
|
||||
|
||||
return evaluate_loss, evaluate_gradient
|
||||
|
||||
def evolve(self, evolution_problem: EvolutionProblem) -> EvolutionResult:
|
||||
"""
|
||||
Args:
|
||||
evolution_problem: The evolution problem containing the hamiltonian, total evolution
|
||||
time and observables to evaluate.
|
||||
|
||||
Returns:
|
||||
A result object containing the evolution information and evaluated observables.
|
||||
|
||||
Raises:
|
||||
ValueError: If the evolution time is not positive or the timestep is too small.
|
||||
NotImplementedError: If the evolution problem contains an initial state.
|
||||
"""
|
||||
self._validate_setup()
|
||||
|
||||
time = evolution_problem.time
|
||||
observables = evolution_problem.aux_operators
|
||||
hamiltonian = evolution_problem.hamiltonian
|
||||
|
||||
# determine the number of timesteps and set the timestep
|
||||
num_timesteps = (
|
||||
int(np.ceil(time / 0.01)) if self.num_timesteps is None else self.num_timesteps
|
||||
)
|
||||
timestep = time / num_timesteps
|
||||
|
||||
if evolution_problem.initial_state is not None:
|
||||
raise NotImplementedError(
|
||||
"Setting an initial state for the evolution is not yet supported for PVQD."
|
||||
)
|
||||
|
||||
# get the function to evaluate the observables for a given set of ansatz parameters
|
||||
if observables is not None:
|
||||
evaluate_observables = _get_observable_evaluator(
|
||||
self.ansatz, observables, self.expectation, self._sampler
|
||||
)
|
||||
observable_values = [evaluate_observables(self.initial_parameters)]
|
||||
|
||||
fidelities = [1]
|
||||
parameters = [self.initial_parameters]
|
||||
times = np.linspace(0, time, num_timesteps + 1).tolist() # +1 to include initial time 0
|
||||
|
||||
initial_guess = self.initial_guess
|
||||
|
||||
for _ in range(num_timesteps):
|
||||
# perform VQE to find the next parameters
|
||||
next_parameters, fidelity = self.step(
|
||||
hamiltonian, self.ansatz, parameters[-1], timestep, initial_guess
|
||||
)
|
||||
|
||||
# set initial guess to last parameter update
|
||||
initial_guess = next_parameters - parameters[-1]
|
||||
|
||||
parameters.append(next_parameters)
|
||||
fidelities.append(fidelity)
|
||||
if observables is not None:
|
||||
observable_values.append(evaluate_observables(next_parameters))
|
||||
|
||||
evolved_state = self.ansatz.bind_parameters(parameters[-1])
|
||||
|
||||
result = PVQDResult(
|
||||
evolved_state=evolved_state,
|
||||
times=times,
|
||||
parameters=parameters,
|
||||
fidelities=fidelities,
|
||||
estimated_error=1 - np.prod(fidelities),
|
||||
)
|
||||
if observables is not None:
|
||||
result.observables = observable_values
|
||||
result.aux_ops_evaluated = observable_values[-1]
|
||||
|
||||
return result
|
||||
|
||||
def _validate_setup(self, skip=None):
|
||||
"""Validate the current setup and raise an error if something misses to run."""
|
||||
|
||||
if skip is None:
|
||||
skip = {}
|
||||
|
||||
required_attributes = {"quantum_instance", "optimizer"}.difference(skip)
|
||||
|
||||
for attr in required_attributes:
|
||||
if getattr(self, attr, None) is None:
|
||||
raise ValueError(f"The {attr} cannot be None.")
|
||||
|
||||
if self.num_timesteps is not None and self.num_timesteps <= 0:
|
||||
raise ValueError(
|
||||
f"The number of timesteps must be positive but is {self.num_timesteps}."
|
||||
)
|
||||
|
||||
if self.ansatz.num_parameters == 0:
|
||||
raise QiskitError(
|
||||
"The ansatz cannot have 0 parameters, otherwise it cannot be trained."
|
||||
)
|
||||
|
||||
if len(self.initial_parameters) != self.ansatz.num_parameters:
|
||||
raise QiskitError(
|
||||
f"Mismatching number of parameters in the ansatz ({self.ansatz.num_parameters}) "
|
||||
f"and the initial parameters ({len(self.initial_parameters)})."
|
||||
)
|
|
@ -0,0 +1,55 @@
|
|||
# This code is part of Qiskit.
|
||||
#
|
||||
# (C) Copyright IBM 2022.
|
||||
#
|
||||
# This code is licensed under the Apache License, Version 2.0. You may
|
||||
# obtain a copy of this license in the LICENSE.txt file in the root directory
|
||||
# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.
|
||||
#
|
||||
# Any modifications or derivative works of this code must retain this
|
||||
# copyright notice, and modified files need to carry a notice indicating
|
||||
# that they have been altered from the originals.
|
||||
|
||||
"""Result object for p-VQD."""
|
||||
|
||||
from typing import Union, Optional, List, Tuple
|
||||
import numpy as np
|
||||
|
||||
from qiskit.circuit import QuantumCircuit
|
||||
from qiskit.opflow import StateFn, OperatorBase
|
||||
|
||||
from ..evolution_result import EvolutionResult
|
||||
|
||||
|
||||
class PVQDResult(EvolutionResult):
|
||||
"""The result object for the p-VQD algorithm."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
evolved_state: Union[StateFn, QuantumCircuit, OperatorBase],
|
||||
aux_ops_evaluated: Optional[List[Tuple[complex, complex]]] = None,
|
||||
times: Optional[List[float]] = None,
|
||||
parameters: Optional[List[np.ndarray]] = None,
|
||||
fidelities: Optional[List[float]] = None,
|
||||
estimated_error: Optional[float] = None,
|
||||
observables: Optional[List[List[float]]] = None,
|
||||
):
|
||||
"""
|
||||
Args:
|
||||
evolved_state: An evolved quantum state.
|
||||
aux_ops_evaluated: Optional list of observables for which expected values on an evolved
|
||||
state are calculated. These values are in fact tuples formatted as (mean, standard
|
||||
deviation).
|
||||
times: The times evaluated during the time integration.
|
||||
parameters: The parameter values at each evaluation time.
|
||||
fidelities: The fidelity of the Trotter step and variational update at each iteration.
|
||||
estimated_error: The overall estimated error evaluated as one minus the
|
||||
product of all fidelities.
|
||||
observables: The value of the observables evaluated at each iteration.
|
||||
"""
|
||||
super().__init__(evolved_state, aux_ops_evaluated)
|
||||
self.times = times
|
||||
self.parameters = parameters
|
||||
self.fidelities = fidelities
|
||||
self.estimated_error = estimated_error
|
||||
self.observables = observables
|
|
@ -0,0 +1,100 @@
|
|||
# This code is part of Qiskit.
|
||||
#
|
||||
# (C) Copyright IBM 2022.
|
||||
#
|
||||
# This code is licensed under the Apache License, Version 2.0. You may
|
||||
# obtain a copy of this license in the LICENSE.txt file in the root directory
|
||||
# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.
|
||||
#
|
||||
# Any modifications or derivative works of this code must retain this
|
||||
# copyright notice, and modified files need to carry a notice indicating
|
||||
# that they have been altered from the originals.
|
||||
|
||||
|
||||
"""Utilities for p-VQD."""
|
||||
|
||||
from typing import Union, List, Callable
|
||||
import logging
|
||||
|
||||
import numpy as np
|
||||
|
||||
from qiskit.circuit import QuantumCircuit, Parameter, ParameterExpression
|
||||
from qiskit.compiler import transpile
|
||||
from qiskit.exceptions import QiskitError
|
||||
from qiskit.opflow import ListOp, CircuitSampler, ExpectationBase, StateFn, OperatorBase
|
||||
from qiskit.opflow.gradients.circuit_gradients import ParamShift
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _is_gradient_supported(ansatz: QuantumCircuit) -> bool:
|
||||
"""Check whether we can apply a simple parameter shift rule to obtain gradients."""
|
||||
|
||||
# check whether the circuit can be unrolled to supported gates
|
||||
try:
|
||||
unrolled = transpile(ansatz, basis_gates=ParamShift.SUPPORTED_GATES, optimization_level=0)
|
||||
except QiskitError:
|
||||
# failed to map to supported basis
|
||||
logger.log(
|
||||
logging.INFO,
|
||||
"No gradient support: Failed to unroll to gates supported by parameter-shift.",
|
||||
)
|
||||
return False
|
||||
|
||||
# check whether all parameters are unique and we do not need to apply the chain rule
|
||||
# (since it's not implemented yet)
|
||||
total_num_parameters = 0
|
||||
for circuit_instruction in unrolled.data:
|
||||
for param in circuit_instruction.operation.params:
|
||||
if isinstance(param, ParameterExpression):
|
||||
if isinstance(param, Parameter):
|
||||
total_num_parameters += 1
|
||||
else:
|
||||
logger.log(
|
||||
logging.INFO,
|
||||
"No gradient support: Circuit is only allowed to have plain parameters, "
|
||||
"as the chain rule is not yet implemented.",
|
||||
)
|
||||
return False
|
||||
|
||||
if total_num_parameters != ansatz.num_parameters:
|
||||
logger.log(
|
||||
logging.INFO,
|
||||
"No gradient support: Circuit is only allowed to have unique parameters, "
|
||||
"as the product rule is not yet implemented.",
|
||||
)
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def _get_observable_evaluator(
|
||||
ansatz: QuantumCircuit,
|
||||
observables: Union[OperatorBase, List[OperatorBase]],
|
||||
expectation: ExpectationBase,
|
||||
sampler: CircuitSampler,
|
||||
) -> Callable[[np.ndarray], Union[float, List[float]]]:
|
||||
"""Get a callable to evaluate a (list of) observable(s) for given circuit parameters."""
|
||||
|
||||
if isinstance(observables, list):
|
||||
observables = ListOp(observables)
|
||||
|
||||
expectation_value = StateFn(observables, is_measurement=True) @ StateFn(ansatz)
|
||||
converted = expectation.convert(expectation_value)
|
||||
|
||||
ansatz_parameters = ansatz.parameters
|
||||
|
||||
def evaluate_observables(theta: np.ndarray) -> Union[float, List[float]]:
|
||||
"""Evaluate the observables for the ansatz parameters ``theta``.
|
||||
|
||||
Args:
|
||||
theta: The ansatz parameters.
|
||||
|
||||
Returns:
|
||||
The observables evaluated at the ansatz parameters.
|
||||
"""
|
||||
value_dict = dict(zip(ansatz_parameters, theta))
|
||||
sampled = sampler.convert(converted, params=value_dict)
|
||||
return sampled.eval()
|
||||
|
||||
return evaluate_observables
|
|
@ -0,0 +1,45 @@
|
|||
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
|
||||
`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
|
||||
onto a variational form. The projection is determined by maximizing the fidelity of the
|
||||
Trotter-evolved state and the ansatz, using a classical optimization routine.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
import numpy as np
|
||||
|
||||
from qiskit import BasicAer
|
||||
from qiskit.circuit.library import EfficientSU2
|
||||
from qiskit.opflow import X, Z, I, MatrixExpectation
|
||||
|
||||
backend = BasicAer.get_backend("statevector_simulator")
|
||||
expectation = MatrixExpectation()
|
||||
hamiltonian = 0.1 * (Z ^ Z) + (I ^ X) + (X ^ I)
|
||||
observable = Z ^ Z
|
||||
ansatz = EfficientSU2(2, reps=1)
|
||||
initial_parameters = np.zeros(ansatz.num_parameters)
|
||||
|
||||
time = 1
|
||||
optimizer = L_BFGS_B()
|
||||
|
||||
# setup the algorithm
|
||||
pvqd = PVQD(
|
||||
ansatz,
|
||||
initial_parameters,
|
||||
num_timesteps=100,
|
||||
optimizer=optimizer,
|
||||
quantum_instance=backend,
|
||||
expectation=expectation
|
||||
)
|
||||
|
||||
# specify the evolution problem
|
||||
problem = EvolutionProblem(
|
||||
hamiltonian, time, aux_operators=[hamiltonian, observable]
|
||||
)
|
||||
|
||||
# and evolve!
|
||||
result = pvqd.evolve(problem)
|
|
@ -0,0 +1,325 @@
|
|||
# This code is part of Qiskit.
|
||||
#
|
||||
# (C) Copyright IBM 2018, 2022.
|
||||
#
|
||||
# This code is licensed under the Apache License, Version 2.0. You may
|
||||
# obtain a copy of this license in the LICENSE.txt file in the root directory
|
||||
# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.
|
||||
#
|
||||
# Any modifications or derivative works of this code must retain this
|
||||
# copyright notice, and modified files need to carry a notice indicating
|
||||
# that they have been altered from the originals.
|
||||
|
||||
"""Tests for PVQD."""
|
||||
|
||||
from functools import partial
|
||||
from ddt import ddt, data, unpack
|
||||
import numpy as np
|
||||
|
||||
from qiskit.test import QiskitTestCase
|
||||
|
||||
from qiskit import BasicAer, QiskitError
|
||||
from qiskit.circuit import QuantumCircuit, Parameter, Gate
|
||||
from qiskit.algorithms.evolvers import EvolutionProblem
|
||||
from qiskit.algorithms.evolvers.pvqd import PVQD
|
||||
from qiskit.algorithms.optimizers import L_BFGS_B, GradientDescent, SPSA, OptimizerResult
|
||||
from qiskit.circuit.library import EfficientSU2
|
||||
from qiskit.opflow import X, Z, I, MatrixExpectation, PauliExpectation
|
||||
|
||||
|
||||
# pylint: disable=unused-argument, invalid-name
|
||||
def gradient_supplied(fun, x0, jac, info):
|
||||
"""A mock optimizer that checks whether the gradient is supported or not."""
|
||||
result = OptimizerResult()
|
||||
result.x = x0
|
||||
result.fun = 0
|
||||
info["has_gradient"] = jac is not None
|
||||
|
||||
return result
|
||||
|
||||
|
||||
class WhatAmI(Gate):
|
||||
"""An custom opaque gate that can be inverted but not decomposed."""
|
||||
|
||||
def __init__(self, angle):
|
||||
super().__init__(name="whatami", num_qubits=2, params=[angle])
|
||||
|
||||
def inverse(self):
|
||||
return WhatAmI(-self.params[0])
|
||||
|
||||
|
||||
@ddt
|
||||
class TestPVQD(QiskitTestCase):
|
||||
"""Tests for the pVQD algorithm."""
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.sv_backend = BasicAer.get_backend("statevector_simulator")
|
||||
self.qasm_backend = BasicAer.get_backend("qasm_simulator")
|
||||
self.expectation = MatrixExpectation()
|
||||
self.hamiltonian = 0.1 * (Z ^ Z) + (I ^ X) + (X ^ I)
|
||||
self.observable = Z ^ Z
|
||||
self.ansatz = EfficientSU2(2, reps=1)
|
||||
self.initial_parameters = np.zeros(self.ansatz.num_parameters)
|
||||
|
||||
@data(
|
||||
("ising", MatrixExpectation, True, "sv", 2),
|
||||
("ising_matrix", MatrixExpectation, True, "sv", None),
|
||||
("ising", PauliExpectation, True, "qasm", 2),
|
||||
("pauli", PauliExpectation, False, "qasm", None),
|
||||
)
|
||||
@unpack
|
||||
def test_pvqd(self, hamiltonian_type, expectation_cls, gradient, backend_type, num_timesteps):
|
||||
"""Test a simple evolution."""
|
||||
time = 0.02
|
||||
|
||||
if hamiltonian_type == "ising":
|
||||
hamiltonian = self.hamiltonian
|
||||
elif hamiltonian_type == "ising_matrix":
|
||||
hamiltonian = self.hamiltonian.to_matrix_op()
|
||||
else: # hamiltonian_type == "pauli":
|
||||
hamiltonian = X ^ X
|
||||
|
||||
# parse input arguments
|
||||
if gradient:
|
||||
optimizer = GradientDescent(maxiter=1)
|
||||
else:
|
||||
optimizer = L_BFGS_B(maxiter=1)
|
||||
|
||||
backend = self.sv_backend if backend_type == "sv" else self.qasm_backend
|
||||
expectation = expectation_cls()
|
||||
|
||||
# run pVQD keeping track of the energy and the magnetization
|
||||
pvqd = PVQD(
|
||||
self.ansatz,
|
||||
self.initial_parameters,
|
||||
num_timesteps=num_timesteps,
|
||||
optimizer=optimizer,
|
||||
quantum_instance=backend,
|
||||
expectation=expectation,
|
||||
)
|
||||
problem = EvolutionProblem(hamiltonian, time, aux_operators=[hamiltonian, self.observable])
|
||||
result = pvqd.evolve(problem)
|
||||
|
||||
self.assertTrue(len(result.fidelities) == 3)
|
||||
self.assertTrue(np.all(result.times == [0.0, 0.01, 0.02]))
|
||||
self.assertTrue(np.asarray(result.observables).shape == (3, 2))
|
||||
num_parameters = self.ansatz.num_parameters
|
||||
self.assertTrue(
|
||||
len(result.parameters) == 3
|
||||
and np.all([len(params) == num_parameters for params in result.parameters])
|
||||
)
|
||||
|
||||
def test_step(self):
|
||||
"""Test calling the step method directly."""
|
||||
|
||||
pvqd = PVQD(
|
||||
self.ansatz,
|
||||
self.initial_parameters,
|
||||
optimizer=L_BFGS_B(maxiter=100),
|
||||
quantum_instance=self.sv_backend,
|
||||
expectation=MatrixExpectation(),
|
||||
)
|
||||
|
||||
# perform optimization for a timestep of 0, then the optimal parameters are the current
|
||||
# ones and the fidelity is 1
|
||||
theta_next, fidelity = pvqd.step(
|
||||
self.hamiltonian.to_matrix_op(),
|
||||
self.ansatz,
|
||||
self.initial_parameters,
|
||||
dt=0.0,
|
||||
initial_guess=np.zeros_like(self.initial_parameters),
|
||||
)
|
||||
|
||||
self.assertTrue(np.allclose(theta_next, self.initial_parameters))
|
||||
self.assertAlmostEqual(fidelity, 1)
|
||||
|
||||
def test_get_loss(self):
|
||||
"""Test getting the loss function directly."""
|
||||
|
||||
pvqd = PVQD(
|
||||
self.ansatz,
|
||||
self.initial_parameters,
|
||||
quantum_instance=self.sv_backend,
|
||||
expectation=MatrixExpectation(),
|
||||
use_parameter_shift=False,
|
||||
)
|
||||
|
||||
theta = np.ones(self.ansatz.num_parameters)
|
||||
loss, gradient = pvqd.get_loss(
|
||||
self.hamiltonian, self.ansatz, dt=0.0, current_parameters=theta
|
||||
)
|
||||
|
||||
displacement = np.arange(self.ansatz.num_parameters)
|
||||
|
||||
with self.subTest(msg="check gradient is None"):
|
||||
self.assertIsNone(gradient)
|
||||
|
||||
with self.subTest(msg="check loss works"):
|
||||
self.assertGreater(loss(displacement), 0)
|
||||
self.assertAlmostEqual(loss(np.zeros_like(theta)), 0)
|
||||
|
||||
def test_invalid_num_timestep(self):
|
||||
"""Test raises if the num_timestep is not positive."""
|
||||
pvqd = PVQD(
|
||||
self.ansatz,
|
||||
self.initial_parameters,
|
||||
num_timesteps=0,
|
||||
optimizer=L_BFGS_B(),
|
||||
quantum_instance=self.sv_backend,
|
||||
expectation=self.expectation,
|
||||
)
|
||||
problem = EvolutionProblem(
|
||||
self.hamiltonian, time=0.01, aux_operators=[self.hamiltonian, self.observable]
|
||||
)
|
||||
|
||||
with self.assertRaises(ValueError):
|
||||
_ = pvqd.evolve(problem)
|
||||
|
||||
def test_initial_guess_and_observables(self):
|
||||
"""Test doing no optimizations stays at initial guess."""
|
||||
initial_guess = np.zeros(self.ansatz.num_parameters)
|
||||
|
||||
pvqd = PVQD(
|
||||
self.ansatz,
|
||||
self.initial_parameters,
|
||||
num_timesteps=10,
|
||||
optimizer=SPSA(maxiter=0, learning_rate=0.1, perturbation=0.01),
|
||||
initial_guess=initial_guess,
|
||||
quantum_instance=self.sv_backend,
|
||||
expectation=self.expectation,
|
||||
)
|
||||
problem = EvolutionProblem(
|
||||
self.hamiltonian, time=0.1, aux_operators=[self.hamiltonian, self.observable]
|
||||
)
|
||||
|
||||
result = pvqd.evolve(problem)
|
||||
|
||||
observables = result.aux_ops_evaluated
|
||||
self.assertEqual(observables[0], 0.1) # expected energy
|
||||
self.assertEqual(observables[1], 1) # expected magnetization
|
||||
|
||||
def test_missing_attributesquantum_instance(self):
|
||||
"""Test appropriate error is raised if the quantum instance is missing."""
|
||||
pvqd = PVQD(
|
||||
self.ansatz,
|
||||
self.initial_parameters,
|
||||
optimizer=L_BFGS_B(maxiter=1),
|
||||
expectation=self.expectation,
|
||||
)
|
||||
problem = EvolutionProblem(self.hamiltonian, time=0.01)
|
||||
|
||||
attrs_to_test = [
|
||||
("optimizer", L_BFGS_B(maxiter=1)),
|
||||
("quantum_instance", self.qasm_backend),
|
||||
]
|
||||
|
||||
for attr, value in attrs_to_test:
|
||||
with self.subTest(msg=f"missing: {attr}"):
|
||||
# set attribute to None to invalidate the setup
|
||||
setattr(pvqd, attr, None)
|
||||
|
||||
with self.assertRaises(ValueError):
|
||||
_ = pvqd.evolve(problem)
|
||||
|
||||
# set the correct value again
|
||||
setattr(pvqd, attr, value)
|
||||
|
||||
with self.subTest(msg="all set again"):
|
||||
result = pvqd.evolve(problem)
|
||||
self.assertIsNotNone(result.evolved_state)
|
||||
|
||||
def test_zero_parameters(self):
|
||||
"""Test passing an ansatz with zero parameters raises an error."""
|
||||
problem = EvolutionProblem(self.hamiltonian, time=0.02)
|
||||
|
||||
pvqd = PVQD(
|
||||
QuantumCircuit(2),
|
||||
np.array([]),
|
||||
optimizer=SPSA(maxiter=10, learning_rate=0.1, perturbation=0.01),
|
||||
quantum_instance=self.sv_backend,
|
||||
expectation=self.expectation,
|
||||
)
|
||||
|
||||
with self.assertRaises(QiskitError):
|
||||
_ = pvqd.evolve(problem)
|
||||
|
||||
def test_initial_state_raises(self):
|
||||
"""Test passing an initial state raises an error for now."""
|
||||
initial_state = QuantumCircuit(2)
|
||||
initial_state.x(0)
|
||||
|
||||
problem = EvolutionProblem(
|
||||
self.hamiltonian,
|
||||
time=0.02,
|
||||
initial_state=initial_state,
|
||||
)
|
||||
|
||||
pvqd = PVQD(
|
||||
self.ansatz,
|
||||
self.initial_parameters,
|
||||
optimizer=SPSA(maxiter=0, learning_rate=0.1, perturbation=0.01),
|
||||
quantum_instance=self.sv_backend,
|
||||
expectation=self.expectation,
|
||||
)
|
||||
|
||||
with self.assertRaises(NotImplementedError):
|
||||
_ = pvqd.evolve(problem)
|
||||
|
||||
|
||||
class TestPVQDUtils(QiskitTestCase):
|
||||
"""Test some utility functions for PVQD."""
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.sv_backend = BasicAer.get_backend("statevector_simulator")
|
||||
self.expectation = MatrixExpectation()
|
||||
self.hamiltonian = 0.1 * (Z ^ Z) + (I ^ X) + (X ^ I)
|
||||
self.ansatz = EfficientSU2(2, reps=1)
|
||||
|
||||
def test_gradient_supported(self):
|
||||
"""Test the gradient support is correctly determined."""
|
||||
# gradient supported here
|
||||
wrapped = EfficientSU2(2) # a circuit wrapped into a big instruction
|
||||
plain = wrapped.decompose() # a plain circuit with already supported instructions
|
||||
|
||||
# gradients not supported on the following circuits
|
||||
x = Parameter("x")
|
||||
duplicated = QuantumCircuit(2)
|
||||
duplicated.rx(x, 0)
|
||||
duplicated.rx(x, 1)
|
||||
|
||||
needs_chainrule = QuantumCircuit(2)
|
||||
needs_chainrule.rx(2 * x, 0)
|
||||
|
||||
custom_gate = WhatAmI(x)
|
||||
unsupported = QuantumCircuit(2)
|
||||
unsupported.append(custom_gate, [0, 1])
|
||||
|
||||
tests = [
|
||||
(wrapped, True), # tuple: (circuit, gradient support)
|
||||
(plain, True),
|
||||
(duplicated, False),
|
||||
(needs_chainrule, False),
|
||||
(unsupported, False),
|
||||
]
|
||||
|
||||
# used to store the info if a gradient callable is passed into the
|
||||
# optimizer of not
|
||||
info = {"has_gradient": None}
|
||||
optimizer = partial(gradient_supplied, info=info)
|
||||
|
||||
pvqd = PVQD(
|
||||
ansatz=None,
|
||||
initial_parameters=np.array([]),
|
||||
optimizer=optimizer,
|
||||
quantum_instance=self.sv_backend,
|
||||
expectation=self.expectation,
|
||||
)
|
||||
problem = EvolutionProblem(self.hamiltonian, time=0.01)
|
||||
for circuit, expected_support in tests:
|
||||
with self.subTest(circuit=circuit, expected_support=expected_support):
|
||||
pvqd.ansatz = circuit
|
||||
pvqd.initial_parameters = np.zeros(circuit.num_parameters)
|
||||
_ = pvqd.evolve(problem)
|
||||
self.assertEqual(info["has_gradient"], expected_support)
|
Loading…
Reference in New Issue