mirror of https://github.com/Qiskit/qiskit.git
Adding variational Quantum Deflation Algorithm for computing higher energy states (#7747)
* initial commit * initial commits * changed init, get_enegery_value and compute_eigenvalue * working kstatevqe * bug fixing * fixing overlap term * fixed kstatevqe * fixed bounds, gradient and aux_ops in kstatevqe.py * changing kstatevqe to vqd * changing filename and bug fixes * adding tests for vqd * mirroring changes in deprecated methods * style check and linting * modifying doc string * adding release notes * lint fixing releasenote and vqe.py * lint fixes * added warning and formatted * test fix * Update qiskit/algorithms/eigen_solvers/vqd.py Co-authored-by: Julien Gacon <gaconju@gmail.com> * Update qiskit/algorithms/eigen_solvers/vqd.py Co-authored-by: Julien Gacon <gaconju@gmail.com> * Update qiskit/algorithms/eigen_solvers/vqd.py Co-authored-by: Julien Gacon <gaconju@gmail.com> * Update qiskit/algorithms/eigen_solvers/vqd.py Co-authored-by: dlasecki <dal@zurich.ibm.com> * Update qiskit/algorithms/eigen_solvers/vqd.py Co-authored-by: Julien Gacon <gaconju@gmail.com> * docstring fixes * modified to use eval_observables and allow the use of any optimiser * lint fixes and fixing OPTIMIZER import * Apply suggestions from code review: grammatical fixes in docs and docstrings Co-authored-by: dlasecki <dal@zurich.ibm.com> * fixing minimizer class * fixed release note, pylint, tox and resolved comments * fixing lint, minimizer and docstring ordering * lint fix * lint fixes and release note fix * test, lint, releasenote fixes * removed dprecated optimizer and related fixes * modifying optimizer.minimize * modifying get_energy_evaluate typehints and docstring Co-authored-by: Pauline Ollitrault <pauline.ollitrault1@gmail.com> Co-authored-by: Julien Gacon <gaconju@gmail.com> Co-authored-by: dlasecki <dal@zurich.ibm.com>
This commit is contained in:
parent
5c51d3a1ce
commit
a5b5f81fce
|
@ -91,6 +91,7 @@ knowledge to do this in that application domain.
|
|||
:nosignatures:
|
||||
|
||||
NumPyEigensolver
|
||||
VQD
|
||||
|
||||
|
||||
Evolvers
|
||||
|
@ -223,7 +224,7 @@ from .amplitude_estimators import (
|
|||
MaximumLikelihoodAmplitudeEstimationResult,
|
||||
EstimationProblem,
|
||||
)
|
||||
from .eigen_solvers import NumPyEigensolver, Eigensolver, EigensolverResult
|
||||
from .eigen_solvers import NumPyEigensolver, Eigensolver, EigensolverResult, VQD, VQDResult
|
||||
from .factorizers import Shor, ShorResult
|
||||
from .linear_solvers import HHL, LinearSolver, NumPyLinearSolver, LinearSolverResult
|
||||
from .minimum_eigen_solvers import (
|
||||
|
@ -287,6 +288,7 @@ __all__ = [
|
|||
"MinimumEigensolverResult",
|
||||
"HamiltonianPhaseEstimation",
|
||||
"HamiltonianPhaseEstimationResult",
|
||||
"VQD",
|
||||
"PhaseEstimationScale",
|
||||
"PhaseEstimation",
|
||||
"PhaseEstimationResult",
|
||||
|
|
|
@ -14,5 +14,6 @@
|
|||
|
||||
from .numpy_eigen_solver import NumPyEigensolver
|
||||
from .eigen_solver import Eigensolver, EigensolverResult
|
||||
from .vqd import VQD, VQDResult
|
||||
|
||||
__all__ = ["NumPyEigensolver", "Eigensolver", "EigensolverResult"]
|
||||
__all__ = ["NumPyEigensolver", "Eigensolver", "EigensolverResult", "VQD", "VQDResult"]
|
||||
|
|
|
@ -0,0 +1,757 @@
|
|||
# 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.
|
||||
|
||||
"""The Variational Quantum Deflation Algorithm for computing higher energy states.
|
||||
|
||||
See https://arxiv.org/abs/1805.08138.
|
||||
"""
|
||||
|
||||
from typing import Optional, List, Callable, Union, Dict, Tuple
|
||||
import logging
|
||||
from time import time
|
||||
import numpy as np
|
||||
|
||||
from qiskit.circuit import QuantumCircuit, Parameter
|
||||
from qiskit.circuit.library import RealAmplitudes
|
||||
from qiskit.opflow.primitive_ops.pauli_op import PauliOp
|
||||
from qiskit.providers import Backend
|
||||
from qiskit.opflow import (
|
||||
OperatorBase,
|
||||
ExpectationBase,
|
||||
ExpectationFactory,
|
||||
StateFn,
|
||||
CircuitStateFn,
|
||||
ListOp,
|
||||
CircuitSampler,
|
||||
PauliSumOp,
|
||||
)
|
||||
from qiskit.opflow.gradients import GradientBase
|
||||
from qiskit.utils.validation import validate_min
|
||||
from qiskit.utils.backend_utils import is_aer_provider
|
||||
from qiskit.utils import QuantumInstance
|
||||
from ..list_or_dict import ListOrDict
|
||||
from ..optimizers import Optimizer, SLSQP, Minimizer
|
||||
from ..variational_algorithm import VariationalAlgorithm, VariationalResult
|
||||
from .eigen_solver import Eigensolver, EigensolverResult
|
||||
from ..minimum_eigen_solvers.vqe import _validate_bounds, _validate_initial_point
|
||||
from ..exceptions import AlgorithmError
|
||||
from ..aux_ops_evaluator import eval_observables
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class VQD(VariationalAlgorithm, Eigensolver):
|
||||
r"""The Variational Quantum Deflation algorithm.
|
||||
|
||||
`VQD <https://arxiv.org/abs/1805.08138>`__ is a quantum algorithm that uses a
|
||||
variational technique to find
|
||||
the k eigenvalues of the Hamiltonian :math:`H` of a given system.
|
||||
|
||||
The algorithm computes excited state energies of generalised hamiltonians
|
||||
by optimising over a modified cost function where each succesive eigen value
|
||||
is calculated iteratively by introducing an overlap term with all
|
||||
the previously computed eigenstaes that must be minimised, thus ensuring
|
||||
higher energy eigen states are found.
|
||||
|
||||
An instance of VQD requires defining three algorithmic sub-components:
|
||||
an integer k denoting the number of eigenstates to calculate, a trial
|
||||
state (a.k.a. ansatz)which is a :class:`QuantumCircuit`,
|
||||
and one of the classical :mod:`~qiskit.algorithms.optimizers`.
|
||||
The ansatz is varied, via its set of parameters, by the optimizer,
|
||||
such that it works towards a state, as determined by the parameters
|
||||
applied to the ansatz, that will result in the minimum expectation values
|
||||
being measured of the input operator (Hamiltonian). The algorithm does
|
||||
this by iteratively refining each excited state to be orthogonal to all
|
||||
the previous excited states.
|
||||
|
||||
An optional array of parameter values, via the *initial_point*, may be provided as the
|
||||
starting point for the search of the minimum eigenvalue. This feature is particularly useful
|
||||
such as when there are reasons to believe that the solution point is close to a particular
|
||||
point.
|
||||
|
||||
The length of the *initial_point* list value must match the number of the parameters
|
||||
expected by the ansatz being used. If the *initial_point* is left at the default
|
||||
of ``None``, then VQD will look to the ansatz for a preferred value, based on its
|
||||
given initial state. If the ansatz returns ``None``,
|
||||
then a random point will be generated within the parameter bounds set, as per above.
|
||||
If the ansatz provides ``None`` as the lower bound, then VQD
|
||||
will default it to :math:`-2\pi`; similarly, if the ansatz returns ``None``
|
||||
as the upper bound, the default value will be :math:`2\pi`.
|
||||
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
ansatz: Optional[QuantumCircuit] = None,
|
||||
k: int = 2,
|
||||
betas: Optional[List[float]] = None,
|
||||
optimizer: Optional[Union[Optimizer, Minimizer]] = None,
|
||||
initial_point: Optional[np.ndarray] = None,
|
||||
gradient: Optional[Union[GradientBase, Callable]] = None,
|
||||
expectation: Optional[ExpectationBase] = None,
|
||||
include_custom: bool = False,
|
||||
max_evals_grouped: int = 1,
|
||||
callback: Optional[Callable[[int, np.ndarray, float, float], None]] = None,
|
||||
quantum_instance: Optional[Union[QuantumInstance, Backend]] = None,
|
||||
) -> None:
|
||||
"""
|
||||
|
||||
Args:
|
||||
ansatz: A parameterized circuit used as ansatz for the wave function.
|
||||
k: the number of eigenvalues to return. Returns the lowest k eigenvalues.
|
||||
betas: beta parameter in the VQD paper. Should have size k -1, the number of excited states.
|
||||
It is a hyperparameter that balances the contribution of the overlap
|
||||
term to the cost function and has a default value computed as
|
||||
mean square sum of coefficients of observable.
|
||||
optimizer: A classical optimizer. Can either be a Qiskit optimizer or a callable
|
||||
that takes an array as input and returns a Qiskit or SciPy optimization result.
|
||||
initial_point: An optional initial point (i.e. initial parameter values)
|
||||
for the optimizer. If ``None`` then VQD will look to the ansatz for a preferred
|
||||
point and if not will simply compute a random one.
|
||||
gradient: An optional gradient function or operator for optimizer.
|
||||
Only used to compute the ground state at the moment.
|
||||
expectation: The Expectation converter for taking the average value of the
|
||||
Observable over the ansatz state function. When ``None`` (the default) an
|
||||
:class:`~qiskit.opflow.expectations.ExpectationFactory` is used to select
|
||||
an appropriate expectation based on the operator and backend. When using Aer
|
||||
qasm_simulator backend, with paulis, it is however much faster to leverage custom
|
||||
Aer function for the computation but, although VQD performs much faster
|
||||
with it, the outcome is ideal, with no shot noise, like using a state vector
|
||||
simulator. If you are just looking for the quickest performance when choosing Aer
|
||||
qasm_simulator and the lack of shot noise is not an issue then set `include_custom`
|
||||
parameter here to ``True`` (defaults to ``False``).
|
||||
include_custom: When `expectation` parameter here is None setting this to ``True`` will
|
||||
allow the factory to include the custom Aer pauli expectation.
|
||||
max_evals_grouped: Max number of evaluations performed simultaneously. Signals the
|
||||
given optimizer that more than one set of parameters can be supplied so that
|
||||
multiple points to compute the gradient can be passed and if computed in parallel
|
||||
potentially the expectation values can be computed in parallel. Typically this is
|
||||
possible when a finite difference gradient is used by the optimizer such that
|
||||
improve overall execution time. Deprecated if a gradient operator or function is
|
||||
given.
|
||||
callback: a callback that can access the intermediate data during the optimization.
|
||||
Four parameter values are passed to the callback as follows during each evaluation
|
||||
by the optimizer for its current set of parameters as it works towards the minimum.
|
||||
These are: the evaluation count, the optimizer parameters for the
|
||||
ansatz, the evaluated mean and the evaluated standard deviation.`
|
||||
quantum_instance: Quantum Instance or Backend
|
||||
|
||||
"""
|
||||
validate_min("max_evals_grouped", max_evals_grouped, 1)
|
||||
|
||||
super().__init__()
|
||||
|
||||
self._max_evals_grouped = max_evals_grouped
|
||||
self._circuit_sampler = None # type: Optional[CircuitSampler]
|
||||
self._expectation = None
|
||||
self.expectation = expectation
|
||||
self._include_custom = include_custom
|
||||
|
||||
# set ansatz -- still supporting pre 0.18.0 sorting
|
||||
|
||||
self._ansatz = None
|
||||
self.ansatz = ansatz
|
||||
|
||||
self.k = k
|
||||
self.betas = betas
|
||||
|
||||
self._optimizer = None
|
||||
self.optimizer = optimizer
|
||||
|
||||
self._initial_point = None
|
||||
self.initial_point = initial_point
|
||||
self._gradient = None
|
||||
self.gradient = gradient
|
||||
self._quantum_instance = None
|
||||
|
||||
if quantum_instance is not None:
|
||||
self.quantum_instance = quantum_instance
|
||||
|
||||
self._eval_time = None
|
||||
self._eval_count = 0
|
||||
self._callback = None
|
||||
self.callback = callback
|
||||
|
||||
logger.info(self.print_settings())
|
||||
|
||||
@property
|
||||
def ansatz(self) -> QuantumCircuit:
|
||||
"""Returns the ansatz."""
|
||||
return self._ansatz
|
||||
|
||||
@ansatz.setter
|
||||
def ansatz(self, ansatz: Optional[QuantumCircuit]):
|
||||
"""Sets the ansatz.
|
||||
|
||||
Args:
|
||||
ansatz: The parameterized circuit used as an ansatz.
|
||||
If None is passed, RealAmplitudes is used by default.
|
||||
|
||||
"""
|
||||
if ansatz is None:
|
||||
ansatz = RealAmplitudes()
|
||||
|
||||
self._ansatz = ansatz
|
||||
|
||||
@property
|
||||
def gradient(self) -> Optional[Union[GradientBase, Callable]]:
|
||||
"""Returns the gradient."""
|
||||
return self._gradient
|
||||
|
||||
@gradient.setter
|
||||
def gradient(self, gradient: Optional[Union[GradientBase, Callable]]):
|
||||
"""Sets the gradient."""
|
||||
self._gradient = gradient
|
||||
|
||||
@property
|
||||
def quantum_instance(self) -> Optional[QuantumInstance]:
|
||||
"""Returns quantum instance."""
|
||||
return self._quantum_instance
|
||||
|
||||
@quantum_instance.setter
|
||||
def quantum_instance(self, quantum_instance: Union[QuantumInstance, Backend]) -> None:
|
||||
"""Sets a quantum_instance."""
|
||||
if not isinstance(quantum_instance, QuantumInstance):
|
||||
quantum_instance = QuantumInstance(quantum_instance)
|
||||
|
||||
self._quantum_instance = quantum_instance
|
||||
self._circuit_sampler = CircuitSampler(
|
||||
quantum_instance, param_qobj=is_aer_provider(quantum_instance.backend)
|
||||
)
|
||||
|
||||
@property
|
||||
def initial_point(self) -> Optional[np.ndarray]:
|
||||
"""Returns initial point."""
|
||||
return self._initial_point
|
||||
|
||||
@initial_point.setter
|
||||
def initial_point(self, initial_point: np.ndarray):
|
||||
"""Sets initial point"""
|
||||
self._initial_point = initial_point
|
||||
|
||||
@property
|
||||
def max_evals_grouped(self) -> int:
|
||||
"""Returns max_evals_grouped"""
|
||||
return self._max_evals_grouped
|
||||
|
||||
@max_evals_grouped.setter
|
||||
def max_evals_grouped(self, max_evals_grouped: int):
|
||||
"""Sets max_evals_grouped"""
|
||||
self._max_evals_grouped = max_evals_grouped
|
||||
self.optimizer.set_max_evals_grouped(max_evals_grouped)
|
||||
|
||||
@property
|
||||
def include_custom(self) -> bool:
|
||||
"""Returns include_custom"""
|
||||
return self._include_custom
|
||||
|
||||
@include_custom.setter
|
||||
def include_custom(self, include_custom: bool):
|
||||
"""Sets include_custom. If set to another value than the one that was previsously set,
|
||||
the expectation attribute is reset to None.
|
||||
"""
|
||||
if include_custom != self._include_custom:
|
||||
self._include_custom = include_custom
|
||||
self.expectation = None
|
||||
|
||||
@property
|
||||
def callback(self) -> Optional[Callable[[int, np.ndarray, float, float], None]]:
|
||||
"""Returns callback"""
|
||||
return self._callback
|
||||
|
||||
@callback.setter
|
||||
def callback(self, callback: Optional[Callable[[int, np.ndarray, float, float], None]]):
|
||||
"""Sets callback"""
|
||||
self._callback = callback
|
||||
|
||||
@property
|
||||
def expectation(self) -> Optional[ExpectationBase]:
|
||||
"""The expectation value algorithm used to construct the expectation measurement from
|
||||
the observable."""
|
||||
return self._expectation
|
||||
|
||||
@expectation.setter
|
||||
def expectation(self, exp: Optional[ExpectationBase]) -> None:
|
||||
self._expectation = exp
|
||||
|
||||
def _check_operator_ansatz(self, operator: OperatorBase):
|
||||
"""Check that the number of qubits of operator and ansatz match."""
|
||||
if operator is not None and self.ansatz is not None:
|
||||
if operator.num_qubits != self.ansatz.num_qubits:
|
||||
# try to set the number of qubits on the ansatz, if possible
|
||||
try:
|
||||
self.ansatz.num_qubits = operator.num_qubits
|
||||
except AttributeError as ex:
|
||||
raise AlgorithmError(
|
||||
"The number of qubits of the ansatz does not match the "
|
||||
"operator, and the ansatz does not allow setting the "
|
||||
"number of qubits using `num_qubits`."
|
||||
) from ex
|
||||
|
||||
@property
|
||||
def optimizer(self) -> Optimizer:
|
||||
"""Returns optimizer"""
|
||||
return self._optimizer
|
||||
|
||||
@optimizer.setter
|
||||
def optimizer(self, optimizer: Optional[Optimizer]):
|
||||
"""Sets the optimizer attribute.
|
||||
|
||||
Args:
|
||||
optimizer: The optimizer to be used. If None is passed, SLSQP is used by default.
|
||||
|
||||
"""
|
||||
if optimizer is None:
|
||||
optimizer = SLSQP()
|
||||
|
||||
if isinstance(optimizer, Optimizer):
|
||||
optimizer.set_max_evals_grouped(self.max_evals_grouped)
|
||||
|
||||
self._optimizer = optimizer
|
||||
|
||||
@property
|
||||
def setting(self):
|
||||
"""Prepare the setting of VQD as a string."""
|
||||
ret = f"Algorithm: {self.__class__.__name__}\n"
|
||||
params = ""
|
||||
for key, value in self.__dict__.items():
|
||||
if key[0] == "_":
|
||||
if "initial_point" in key and value is None:
|
||||
params += "-- {}: {}\n".format(key[1:], "Random seed")
|
||||
else:
|
||||
params += f"-- {key[1:]}: {value}\n"
|
||||
ret += f"{params}"
|
||||
return ret
|
||||
|
||||
def print_settings(self):
|
||||
"""Preparing the setting of VQD into a string.
|
||||
|
||||
Returns:
|
||||
str: the formatted setting of VQD.
|
||||
"""
|
||||
ret = "\n"
|
||||
ret += "==================== Setting of {} ============================\n".format(
|
||||
self.__class__.__name__
|
||||
)
|
||||
ret += f"{self.setting}"
|
||||
ret += "===============================================================\n"
|
||||
if self.ansatz is not None:
|
||||
ret += "{}".format(self.ansatz.draw(output="text"))
|
||||
else:
|
||||
ret += "ansatz has not been set"
|
||||
ret += "===============================================================\n"
|
||||
ret += f"{self._optimizer.setting}"
|
||||
ret += "===============================================================\n"
|
||||
return ret
|
||||
|
||||
def construct_expectation(
|
||||
self,
|
||||
parameter: Union[List[float], List[Parameter], np.ndarray],
|
||||
operator: OperatorBase,
|
||||
return_expectation: bool = False,
|
||||
) -> Union[OperatorBase, Tuple[OperatorBase, ExpectationBase]]:
|
||||
r"""
|
||||
Generate the ansatz circuit and expectation value measurement, and return their
|
||||
runnable composition.
|
||||
|
||||
Args:
|
||||
parameter: Parameters for the ansatz circuit.
|
||||
operator: Qubit operator of the Observable
|
||||
return_expectation: If True, return the ``ExpectationBase`` expectation converter used
|
||||
in the construction of the expectation value. Useful e.g. to compute the standard
|
||||
deviation of the expectation value.
|
||||
|
||||
Returns:
|
||||
The Operator equalling the measurement of the ansatz :class:`StateFn` by the
|
||||
Observable's expectation :class:`StateFn`, and, optionally, the expectation converter.
|
||||
|
||||
Raises:
|
||||
AlgorithmError: If no operator has been provided.
|
||||
AlgorithmError: If no expectation is passed and None could be inferred via the
|
||||
ExpectationFactory.
|
||||
"""
|
||||
if operator is None:
|
||||
raise AlgorithmError("The operator was never provided.")
|
||||
|
||||
self._check_operator_ansatz(operator)
|
||||
|
||||
# if expectation was never created, try to create one
|
||||
if self.expectation is None:
|
||||
expectation = ExpectationFactory.build(
|
||||
operator=operator,
|
||||
backend=self.quantum_instance,
|
||||
include_custom=self._include_custom,
|
||||
)
|
||||
else:
|
||||
expectation = self.expectation
|
||||
|
||||
wave_function = self.ansatz.assign_parameters(parameter)
|
||||
|
||||
observable_meas = expectation.convert(StateFn(operator, is_measurement=True))
|
||||
ansatz_circuit_op = CircuitStateFn(wave_function)
|
||||
expect_op = observable_meas.compose(ansatz_circuit_op).reduce()
|
||||
|
||||
if return_expectation:
|
||||
return expect_op, expectation
|
||||
|
||||
return expect_op
|
||||
|
||||
def construct_circuit(
|
||||
self,
|
||||
parameter: Union[List[float], List[Parameter], np.ndarray],
|
||||
operator: OperatorBase,
|
||||
) -> List[QuantumCircuit]:
|
||||
"""Return the circuits used to compute the expectation value.
|
||||
|
||||
Args:
|
||||
parameter: Parameters for the ansatz circuit.
|
||||
operator: Qubit operator of the Observable
|
||||
|
||||
Returns:
|
||||
A list of the circuits used to compute the expectation value.
|
||||
"""
|
||||
expect_op = self.construct_expectation(parameter, operator).to_circuit_op()
|
||||
|
||||
circuits = []
|
||||
|
||||
# recursively extract circuits
|
||||
def extract_circuits(op):
|
||||
if isinstance(op, CircuitStateFn):
|
||||
circuits.append(op.primitive)
|
||||
elif isinstance(op, ListOp):
|
||||
for op_i in op.oplist:
|
||||
extract_circuits(op_i)
|
||||
|
||||
extract_circuits(expect_op)
|
||||
|
||||
return circuits
|
||||
|
||||
@classmethod
|
||||
def supports_aux_operators(cls) -> bool:
|
||||
return True
|
||||
|
||||
def _eval_aux_ops(
|
||||
self,
|
||||
parameters: np.ndarray,
|
||||
aux_operators: ListOrDict[OperatorBase],
|
||||
expectation: ExpectationBase,
|
||||
threshold: float = 1e-12,
|
||||
) -> ListOrDict[Tuple[complex, complex]]:
|
||||
# Create new CircuitSampler to avoid breaking existing one's caches.
|
||||
sampler = CircuitSampler(self.quantum_instance)
|
||||
|
||||
if isinstance(aux_operators, dict):
|
||||
list_op = ListOp(list(aux_operators.values()))
|
||||
else:
|
||||
list_op = ListOp(aux_operators)
|
||||
|
||||
aux_op_meas = expectation.convert(StateFn(list_op, is_measurement=True))
|
||||
aux_op_expect = aux_op_meas.compose(CircuitStateFn(self.ansatz.bind_parameters(parameters)))
|
||||
aux_op_expect_sampled = sampler.convert(aux_op_expect)
|
||||
|
||||
# compute means
|
||||
values = np.real(aux_op_expect_sampled.eval())
|
||||
|
||||
# compute standard deviations
|
||||
variances = np.real(expectation.compute_variance(aux_op_expect_sampled))
|
||||
if not isinstance(variances, np.ndarray) and variances == 0.0:
|
||||
# when `variances` is a single value equal to 0., our expectation value is exact and we
|
||||
# manually ensure the variances to be a list of the correct length
|
||||
variances = np.zeros(len(aux_operators), dtype=float)
|
||||
std_devs = np.sqrt(variances / self.quantum_instance.run_config.shots)
|
||||
|
||||
# Discard values below threshold
|
||||
aux_op_means = values * (np.abs(values) > threshold)
|
||||
# zip means and standard deviations into tuples
|
||||
aux_op_results = zip(aux_op_means, std_devs)
|
||||
|
||||
# Return None eigenvalues for None operators if aux_operators is a list.
|
||||
# None operators are already dropped in compute_minimum_eigenvalue if aux_operators is a dict.
|
||||
if isinstance(aux_operators, list):
|
||||
aux_operator_eigenvalues = [None] * len(aux_operators)
|
||||
key_value_iterator = enumerate(aux_op_results)
|
||||
else:
|
||||
aux_operator_eigenvalues = {}
|
||||
key_value_iterator = zip(aux_operators.keys(), aux_op_results)
|
||||
|
||||
for key, value in key_value_iterator:
|
||||
if aux_operators[key] is not None:
|
||||
aux_operator_eigenvalues[key] = value
|
||||
|
||||
return aux_operator_eigenvalues
|
||||
|
||||
def compute_eigenvalues(
|
||||
self, operator: OperatorBase, aux_operators: Optional[ListOrDict[OperatorBase]] = None
|
||||
) -> EigensolverResult:
|
||||
super().compute_eigenvalues(operator, aux_operators)
|
||||
|
||||
if self.quantum_instance is None:
|
||||
raise AlgorithmError(
|
||||
"A QuantumInstance or Backend must be supplied to run the quantum algorithm."
|
||||
)
|
||||
self.quantum_instance.circuit_summary = True
|
||||
|
||||
# this sets the size of the ansatz, so it must be called before the initial point
|
||||
# validation
|
||||
self._check_operator_ansatz(operator)
|
||||
|
||||
# set an expectation for this algorithm run (will be reset to None at the end)
|
||||
initial_point = _validate_initial_point(self.initial_point, self.ansatz)
|
||||
|
||||
bounds = _validate_bounds(self.ansatz)
|
||||
# We need to handle the array entries being zero or Optional i.e. having value None
|
||||
if aux_operators:
|
||||
zero_op = PauliSumOp.from_list([("I" * self.ansatz.num_qubits, 0)])
|
||||
|
||||
# Convert the None and zero values when aux_operators is a list.
|
||||
# Drop None and convert zero values when aux_operators is a dict.
|
||||
if isinstance(aux_operators, list):
|
||||
key_op_iterator = enumerate(aux_operators)
|
||||
converted = [zero_op] * len(aux_operators)
|
||||
else:
|
||||
key_op_iterator = aux_operators.items()
|
||||
converted = {}
|
||||
for key, op in key_op_iterator:
|
||||
if op is not None:
|
||||
converted[key] = zero_op if op == 0 else op
|
||||
|
||||
aux_operators = converted
|
||||
|
||||
else:
|
||||
aux_operators = None
|
||||
|
||||
if self.betas is None:
|
||||
upper_bound = (
|
||||
abs(operator.coeff)
|
||||
if isinstance(operator, PauliOp)
|
||||
else abs(operator.coeff) * sum(abs(operation.coeff) for operation in operator)
|
||||
)
|
||||
self.betas = [upper_bound * 10] * (self.k)
|
||||
logger.info("beta autoevaluated to %s", self.betas[0])
|
||||
|
||||
result = VQDResult()
|
||||
result.optimal_point = []
|
||||
result.optimal_parameters = []
|
||||
result.optimal_value = []
|
||||
result.cost_function_evals = []
|
||||
result.optimizer_time = []
|
||||
result.eigenvalues = []
|
||||
result.eigenstates = []
|
||||
|
||||
if aux_operators is not None:
|
||||
aux_values = []
|
||||
|
||||
for step in range(1, self.k + 1):
|
||||
|
||||
self._eval_count = 0
|
||||
energy_evaluation, expectation = self.get_energy_evaluation(
|
||||
step, operator, return_expectation=True, prev_states=result.optimal_parameters
|
||||
)
|
||||
|
||||
# Convert the gradient operator into a callable function that is compatible with the
|
||||
# optimization routine. Only used for the ground state currently as Gradient() doesnt
|
||||
# support SumOps yet
|
||||
if isinstance(self._gradient, GradientBase):
|
||||
gradient = self._gradient.gradient_wrapper(
|
||||
StateFn(operator, is_measurement=True) @ StateFn(self.ansatz),
|
||||
bind_params=list(self.ansatz.parameters),
|
||||
backend=self._quantum_instance,
|
||||
)
|
||||
else:
|
||||
gradient = self._gradient
|
||||
|
||||
start_time = time()
|
||||
|
||||
if callable(self.optimizer):
|
||||
opt_result = self.optimizer( # pylint: disable=not-callable
|
||||
fun=energy_evaluation, x0=initial_point, jac=gradient, bounds=bounds
|
||||
)
|
||||
else:
|
||||
opt_result = self.optimizer.minimize(
|
||||
fun=energy_evaluation, x0=initial_point, jac=gradient, bounds=bounds
|
||||
)
|
||||
|
||||
eval_time = time() - start_time
|
||||
|
||||
result.optimal_point.append(opt_result.x)
|
||||
result.optimal_parameters.append(dict(zip(self.ansatz.parameters, opt_result.x)))
|
||||
result.optimal_value.append(opt_result.fun)
|
||||
result.cost_function_evals.append(opt_result.nfev)
|
||||
result.optimizer_time.append(eval_time)
|
||||
|
||||
eigenvalue = (
|
||||
StateFn(operator, is_measurement=True)
|
||||
.compose(CircuitStateFn(self.ansatz.bind_parameters(result.optimal_parameters[-1])))
|
||||
.reduce()
|
||||
.eval()
|
||||
)
|
||||
|
||||
result.eigenvalues.append(eigenvalue)
|
||||
result.eigenstates.append(self._get_eigenstate(result.optimal_parameters[-1]))
|
||||
|
||||
if aux_operators is not None:
|
||||
bound_ansatz = self.ansatz.bind_parameters(result.optimal_point[-1])
|
||||
aux_value = eval_observables(
|
||||
self.quantum_instance, bound_ansatz, aux_operators, expectation=expectation
|
||||
)
|
||||
aux_values.append(aux_value)
|
||||
|
||||
if step == 1:
|
||||
|
||||
logger.info(
|
||||
"Ground state optimization complete in %s seconds.\nFound opt_params %s in %s evals",
|
||||
eval_time,
|
||||
result.optimal_point,
|
||||
self._eval_count,
|
||||
)
|
||||
else:
|
||||
logger.info(
|
||||
(
|
||||
"%s excited state optimization complete in %s s.\nFound opt_parms %s in %s evals"
|
||||
),
|
||||
str(step - 1),
|
||||
eval_time,
|
||||
result.optimal_point,
|
||||
self._eval_count,
|
||||
)
|
||||
|
||||
# To match the siignature of NumpyEigenSolver Result
|
||||
result.eigenstates = ListOp([StateFn(vec) for vec in result.eigenstates])
|
||||
result.eigenvalues = np.array(result.eigenvalues)
|
||||
result.optimal_point = np.array(result.optimal_point)
|
||||
result.optimal_value = np.array(result.optimal_value)
|
||||
result.cost_function_evals = np.array(result.cost_function_evals)
|
||||
result.optimizer_time = np.array(result.optimizer_time)
|
||||
|
||||
if aux_operators is not None:
|
||||
result.aux_operator_eigenvalues = aux_values
|
||||
|
||||
return result
|
||||
|
||||
def get_energy_evaluation(
|
||||
self,
|
||||
step: int,
|
||||
operator: OperatorBase,
|
||||
return_expectation: bool = False,
|
||||
prev_states: Optional[List[np.ndarray]] = None,
|
||||
) -> Callable[[np.ndarray], Union[float, List[float]]]:
|
||||
"""Returns a function handle to evaluates the energy at given parameters for the ansatz.
|
||||
|
||||
This return value is the objective function to be passed to the optimizer for evaluation.
|
||||
|
||||
Args:
|
||||
step: level of enegy being calculated. 0 for ground, 1 for first excited state and so on.
|
||||
operator: The operator whose energy to evaluate.
|
||||
return_expectation: If True, return the ``ExpectationBase`` expectation converter used
|
||||
in the construction of the expectation value. Useful e.g. to evaluate other
|
||||
operators with the same expectation value converter.
|
||||
prev_states: List of parameters from previous rounds of optimization.
|
||||
|
||||
|
||||
Returns:
|
||||
A callable that computes and returns the energy of the hamiltonian
|
||||
of each parameter, and, optionally, the expectation
|
||||
|
||||
Raises:
|
||||
RuntimeError: If the circuit is not parameterized (i.e. has 0 free parameters).
|
||||
AlgorithmError: If operator was not provided.
|
||||
|
||||
"""
|
||||
|
||||
num_parameters = self.ansatz.num_parameters
|
||||
if num_parameters == 0:
|
||||
raise RuntimeError("The ansatz must be parameterized, but has 0 free parameters.")
|
||||
|
||||
if operator is None:
|
||||
raise AlgorithmError("The operator was never provided.")
|
||||
|
||||
if step > 1 and (len(prev_states) + 1) != step:
|
||||
raise RuntimeError(
|
||||
f"Passed previous states of the wrong size."
|
||||
f"Passed array has length {str(len(prev_states))}"
|
||||
)
|
||||
|
||||
self._check_operator_ansatz(operator)
|
||||
overlap_op = []
|
||||
|
||||
ansatz_params = self.ansatz.parameters
|
||||
expect_op, expectation = self.construct_expectation(
|
||||
ansatz_params, operator, return_expectation=True
|
||||
)
|
||||
|
||||
for state in range(step - 1):
|
||||
|
||||
prev_circ = self.ansatz.bind_parameters(prev_states[state])
|
||||
overlap_op.append(~CircuitStateFn(prev_circ) @ CircuitStateFn(self.ansatz))
|
||||
|
||||
def energy_evaluation(parameters):
|
||||
parameter_sets = np.reshape(parameters, (-1, num_parameters))
|
||||
# Create dict associating each parameter with the lists of parameterization values for it
|
||||
param_bindings = dict(zip(ansatz_params, parameter_sets.transpose().tolist()))
|
||||
|
||||
sampled_expect_op = self._circuit_sampler.convert(expect_op, params=param_bindings)
|
||||
mean = np.real(sampled_expect_op.eval())
|
||||
|
||||
for state in range(step - 1):
|
||||
sampled_final_op = self._circuit_sampler.convert(
|
||||
overlap_op[state], params=param_bindings
|
||||
)
|
||||
cost = sampled_final_op.eval()
|
||||
mean += np.real(self.betas[state] * np.conj(cost) * cost)
|
||||
|
||||
self._eval_count += len(mean)
|
||||
|
||||
return mean if len(mean) > 1 else mean[0]
|
||||
|
||||
if return_expectation:
|
||||
return energy_evaluation, expectation
|
||||
|
||||
return energy_evaluation
|
||||
|
||||
def _get_eigenstate(self, optimal_parameters) -> Union[List[float], Dict[str, int]]:
|
||||
"""Get the simulation outcome of the ansatz, provided with parameters."""
|
||||
optimal_circuit = self.ansatz.bind_parameters(optimal_parameters)
|
||||
state_fn = self._circuit_sampler.convert(StateFn(optimal_circuit)).eval()
|
||||
if self.quantum_instance.is_statevector:
|
||||
state = state_fn.primitive.data # VectorStateFn -> Statevector -> np.array
|
||||
else:
|
||||
state = state_fn.to_dict_fn().primitive # SparseVectorStateFn -> DictStateFn -> dict
|
||||
|
||||
return state
|
||||
|
||||
|
||||
class VQDResult(VariationalResult, EigensolverResult):
|
||||
"""VQD Result."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self._cost_function_evals = None
|
||||
|
||||
@property
|
||||
def cost_function_evals(self) -> Optional[int]:
|
||||
"""Returns number of cost optimizer evaluations"""
|
||||
return self._cost_function_evals
|
||||
|
||||
@cost_function_evals.setter
|
||||
def cost_function_evals(self, value: int) -> None:
|
||||
"""Sets number of cost function evaluations"""
|
||||
self._cost_function_evals = value
|
||||
|
||||
@property
|
||||
def eigenstates(self) -> Optional[np.ndarray]:
|
||||
"""return eigen state"""
|
||||
return self._eigenstates
|
||||
|
||||
@eigenstates.setter
|
||||
def eigenstates(self, value: np.ndarray) -> None:
|
||||
"""set eigen state"""
|
||||
self._eigenstates = value
|
|
@ -0,0 +1,33 @@
|
|||
---
|
||||
features:
|
||||
- |
|
||||
The algorithm iteratively computes each eigenstate by starting from the ground
|
||||
state (which is computed as in VQE) and then optimising a modified cost function
|
||||
that tries to compute eigen states that are orthogonal to the states computed in
|
||||
the previous iterations and have the lowest energy when computed over the ansatz.
|
||||
The interface implemented is very similar to that of VQE and is of the form:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from qiskit.algorithms import VQD
|
||||
from qiskit.utils import QuantumInstance
|
||||
from qiskit.circuit.library import TwoLocal
|
||||
from qiskit.algorithms.optimizers import COBYLA
|
||||
from qiskit import BasicAer
|
||||
from qiskit.opflow import I,Z,X
|
||||
|
||||
h2_op = (
|
||||
-1.052373245772859 * (I ^ I)
|
||||
+ 0.39793742484318045 * (I ^ Z)
|
||||
- 0.39793742484318045 * (Z ^ I)
|
||||
- 0.01128010425623538 * (Z ^ Z)
|
||||
+ 0.18093119978423156 * (X ^ X)
|
||||
)
|
||||
|
||||
vqd = VQD(k =2, ansatz = TwoLocal(rotation_blocks="ry", entanglement_blocks="cz"),optimizer = COBYLA(maxiter = 0), quantum_instance = QuantumInstance(
|
||||
BasicAer.get_backend("qasm_simulator"), shots = 2048)
|
||||
)
|
||||
vqd_res = vqd.compute_eigenvalues(op)
|
||||
|
||||
This particular code snippet generates 2 eigenvalues (ground and 1st excited state)
|
||||
Tests have also been implemented.
|
|
@ -0,0 +1,582 @@
|
|||
# This code is part of Qiskit.
|
||||
#
|
||||
# (C) Copyright IBM 2018, 2021.
|
||||
#
|
||||
# This code is licensed under the Apache License, Version 2.0. You may
|
||||
# obtain a copy of this license in the LICENSE.txt file in the root directory
|
||||
# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.
|
||||
#
|
||||
# Any modifications or derivative works of this code must retain this
|
||||
# copyright notice, and modified files need to carry a notice indicating
|
||||
# that they have been altered from the originals.
|
||||
|
||||
""" Test VQD """
|
||||
|
||||
import unittest
|
||||
from test.python.algorithms import QiskitAlgorithmsTestCase
|
||||
|
||||
import numpy as np
|
||||
from ddt import data, ddt, unpack
|
||||
|
||||
from qiskit import BasicAer, QuantumCircuit
|
||||
from qiskit.algorithms import VQD, AlgorithmError
|
||||
from qiskit.algorithms.optimizers import (
|
||||
COBYLA,
|
||||
L_BFGS_B,
|
||||
SLSQP,
|
||||
)
|
||||
from qiskit.circuit.library import EfficientSU2, RealAmplitudes, TwoLocal
|
||||
from qiskit.exceptions import MissingOptionalLibraryError
|
||||
from qiskit.opflow import (
|
||||
AerPauliExpectation,
|
||||
I,
|
||||
MatrixExpectation,
|
||||
MatrixOp,
|
||||
PauliExpectation,
|
||||
PauliSumOp,
|
||||
PrimitiveOp,
|
||||
X,
|
||||
Z,
|
||||
)
|
||||
|
||||
from qiskit.utils import QuantumInstance, algorithm_globals, has_aer
|
||||
|
||||
|
||||
if has_aer():
|
||||
from qiskit import Aer
|
||||
|
||||
|
||||
@ddt
|
||||
class TestVQD(QiskitAlgorithmsTestCase):
|
||||
"""Test VQD"""
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.seed = 50
|
||||
algorithm_globals.random_seed = self.seed
|
||||
self.h2_op = (
|
||||
-1.052373245772859 * (I ^ I)
|
||||
+ 0.39793742484318045 * (I ^ Z)
|
||||
- 0.39793742484318045 * (Z ^ I)
|
||||
- 0.01128010425623538 * (Z ^ Z)
|
||||
+ 0.18093119978423156 * (X ^ X)
|
||||
)
|
||||
self.h2_energy = -1.85727503
|
||||
self.h2_energy_excited = [-1.85727503, -1.24458455]
|
||||
|
||||
self.test_op = MatrixOp(np.diagflat([3, 5, -1, 0.8, 0.2, 2, 1, -3])).to_pauli_op()
|
||||
self.test_results = [-3, -1]
|
||||
|
||||
self.ryrz_wavefunction = TwoLocal(
|
||||
rotation_blocks=["ry", "rz"], entanglement_blocks="cz", reps=1
|
||||
)
|
||||
self.ry_wavefunction = TwoLocal(rotation_blocks="ry", entanglement_blocks="cz")
|
||||
|
||||
self.qasm_simulator = QuantumInstance(
|
||||
BasicAer.get_backend("qasm_simulator"),
|
||||
shots=2048,
|
||||
seed_simulator=self.seed,
|
||||
seed_transpiler=self.seed,
|
||||
)
|
||||
self.statevector_simulator = QuantumInstance(
|
||||
BasicAer.get_backend("statevector_simulator"),
|
||||
shots=1,
|
||||
seed_simulator=self.seed,
|
||||
seed_transpiler=self.seed,
|
||||
)
|
||||
|
||||
def test_basic_aer_statevector(self):
|
||||
"""Test the VQD on BasicAer's statevector simulator."""
|
||||
wavefunction = self.ryrz_wavefunction
|
||||
vqd = VQD(
|
||||
k=2,
|
||||
ansatz=wavefunction,
|
||||
optimizer=COBYLA(),
|
||||
quantum_instance=QuantumInstance(
|
||||
BasicAer.get_backend("statevector_simulator"),
|
||||
basis_gates=["u1", "u2", "u3", "cx", "id"],
|
||||
coupling_map=[[0, 1]],
|
||||
seed_simulator=algorithm_globals.random_seed,
|
||||
seed_transpiler=algorithm_globals.random_seed,
|
||||
),
|
||||
)
|
||||
|
||||
result = vqd.compute_eigenvalues(operator=self.h2_op)
|
||||
|
||||
with self.subTest(msg="test eigenvalue"):
|
||||
np.testing.assert_array_almost_equal(
|
||||
result.eigenvalues.real, self.h2_energy_excited, decimal=1
|
||||
)
|
||||
|
||||
with self.subTest(msg="test dimension of optimal point"):
|
||||
self.assertEqual(len(result.optimal_point[-1]), 8)
|
||||
|
||||
with self.subTest(msg="assert cost_function_evals is set"):
|
||||
self.assertIsNotNone(result.cost_function_evals)
|
||||
|
||||
with self.subTest(msg="assert optimizer_time is set"):
|
||||
self.assertIsNotNone(result.optimizer_time)
|
||||
|
||||
def test_mismatching_num_qubits(self):
|
||||
"""Ensuring circuit and operator mismatch is caught"""
|
||||
wavefunction = QuantumCircuit(1)
|
||||
optimizer = SLSQP(maxiter=50)
|
||||
vqd = VQD(
|
||||
k=1,
|
||||
ansatz=wavefunction,
|
||||
optimizer=optimizer,
|
||||
quantum_instance=self.statevector_simulator,
|
||||
)
|
||||
with self.assertRaises(AlgorithmError):
|
||||
_ = vqd.compute_eigenvalues(operator=self.h2_op)
|
||||
|
||||
@data(
|
||||
(MatrixExpectation(), 1),
|
||||
(AerPauliExpectation(), 1),
|
||||
(PauliExpectation(), 2),
|
||||
)
|
||||
@unpack
|
||||
def test_construct_circuit(self, expectation, num_circuits):
|
||||
"""Test construct circuits returns QuantumCircuits and the right number of them."""
|
||||
try:
|
||||
wavefunction = EfficientSU2(2, reps=1)
|
||||
vqd = VQD(k=2, ansatz=wavefunction, expectation=expectation)
|
||||
params = [0] * wavefunction.num_parameters
|
||||
circuits = vqd.construct_circuit(parameter=params, operator=self.h2_op)
|
||||
|
||||
self.assertEqual(len(circuits), num_circuits)
|
||||
for circuit in circuits:
|
||||
self.assertIsInstance(circuit, QuantumCircuit)
|
||||
except MissingOptionalLibraryError as ex:
|
||||
self.skipTest(str(ex))
|
||||
return
|
||||
|
||||
def test_missing_varform_params(self):
|
||||
"""Test specifying a variational form with no parameters raises an error."""
|
||||
circuit = QuantumCircuit(self.h2_op.num_qubits)
|
||||
vqd = VQD(
|
||||
k=1, ansatz=circuit, quantum_instance=BasicAer.get_backend("statevector_simulator")
|
||||
)
|
||||
with self.assertRaises(RuntimeError):
|
||||
vqd.compute_eigenvalues(operator=self.h2_op)
|
||||
|
||||
def test_basic_aer_qasm(self):
|
||||
"""Test the VQD on BasicAer's QASM simulator."""
|
||||
optimizer = COBYLA(maxiter=1000)
|
||||
wavefunction = self.ry_wavefunction
|
||||
|
||||
vqd = VQD(
|
||||
ansatz=wavefunction,
|
||||
optimizer=optimizer,
|
||||
max_evals_grouped=1,
|
||||
quantum_instance=self.qasm_simulator,
|
||||
)
|
||||
|
||||
# TODO benchmark this later.
|
||||
result = vqd.compute_eigenvalues(operator=self.h2_op)
|
||||
np.testing.assert_array_almost_equal(
|
||||
result.eigenvalues.real, self.h2_energy_excited, decimal=1
|
||||
)
|
||||
|
||||
@unittest.skipUnless(has_aer(), "qiskit-aer doesn't appear to be installed.")
|
||||
def test_with_aer_statevector(self):
|
||||
"""Test VQD with Aer's statevector_simulator."""
|
||||
backend = Aer.get_backend("aer_simulator_statevector")
|
||||
wavefunction = self.ry_wavefunction
|
||||
optimizer = L_BFGS_B()
|
||||
|
||||
quantum_instance = QuantumInstance(
|
||||
backend,
|
||||
seed_simulator=algorithm_globals.random_seed,
|
||||
seed_transpiler=algorithm_globals.random_seed,
|
||||
)
|
||||
vqd = VQD(
|
||||
k=2,
|
||||
ansatz=wavefunction,
|
||||
optimizer=optimizer,
|
||||
max_evals_grouped=1,
|
||||
quantum_instance=quantum_instance,
|
||||
)
|
||||
|
||||
result = vqd.compute_eigenvalues(operator=self.h2_op)
|
||||
np.testing.assert_array_almost_equal(
|
||||
result.eigenvalues.real, self.h2_energy_excited, decimal=2
|
||||
)
|
||||
|
||||
@unittest.skipUnless(has_aer(), "qiskit-aer doesn't appear to be installed.")
|
||||
def test_with_aer_qasm(self):
|
||||
"""Test VQD with Aer's qasm_simulator."""
|
||||
backend = Aer.get_backend("aer_simulator")
|
||||
optimizer = COBYLA(maxiter=1000)
|
||||
wavefunction = self.ry_wavefunction
|
||||
|
||||
quantum_instance = QuantumInstance(
|
||||
backend,
|
||||
seed_simulator=algorithm_globals.random_seed,
|
||||
seed_transpiler=algorithm_globals.random_seed,
|
||||
)
|
||||
|
||||
vqd = VQD(
|
||||
k=2,
|
||||
ansatz=wavefunction,
|
||||
optimizer=optimizer,
|
||||
expectation=PauliExpectation(),
|
||||
quantum_instance=quantum_instance,
|
||||
)
|
||||
|
||||
result = vqd.compute_eigenvalues(operator=self.h2_op)
|
||||
|
||||
np.testing.assert_array_almost_equal(
|
||||
result.eigenvalues.real, self.h2_energy_excited, decimal=1
|
||||
)
|
||||
|
||||
@unittest.skipUnless(has_aer(), "qiskit-aer doesn't appear to be installed.")
|
||||
def test_with_aer_qasm_snapshot_mode(self):
|
||||
"""Test the VQD using Aer's qasm_simulator snapshot mode."""
|
||||
|
||||
backend = Aer.get_backend("aer_simulator")
|
||||
optimizer = COBYLA(maxiter=400)
|
||||
wavefunction = self.ryrz_wavefunction
|
||||
|
||||
quantum_instance = QuantumInstance(
|
||||
backend,
|
||||
shots=100,
|
||||
seed_simulator=algorithm_globals.random_seed,
|
||||
seed_transpiler=algorithm_globals.random_seed,
|
||||
)
|
||||
vqd = VQD(
|
||||
k=2,
|
||||
ansatz=wavefunction,
|
||||
optimizer=optimizer,
|
||||
expectation=AerPauliExpectation(),
|
||||
quantum_instance=quantum_instance,
|
||||
)
|
||||
|
||||
result = vqd.compute_eigenvalues(operator=self.test_op)
|
||||
np.testing.assert_array_almost_equal(result.eigenvalues.real, self.test_results, decimal=1)
|
||||
|
||||
def test_callback(self):
|
||||
"""Test the callback on VQD."""
|
||||
history = {"eval_count": [], "parameters": [], "mean": [], "std": []}
|
||||
|
||||
def store_intermediate_result(eval_count, parameters, mean, std):
|
||||
history["eval_count"].append(eval_count)
|
||||
history["parameters"].append(parameters)
|
||||
history["mean"].append(mean)
|
||||
history["std"].append(std)
|
||||
|
||||
optimizer = COBYLA(maxiter=3)
|
||||
wavefunction = self.ry_wavefunction
|
||||
|
||||
vqd = VQD(
|
||||
ansatz=wavefunction,
|
||||
optimizer=optimizer,
|
||||
callback=store_intermediate_result,
|
||||
quantum_instance=self.qasm_simulator,
|
||||
)
|
||||
vqd.compute_eigenvalues(operator=self.h2_op)
|
||||
|
||||
self.assertTrue(all(isinstance(count, int) for count in history["eval_count"]))
|
||||
self.assertTrue(all(isinstance(mean, float) for mean in history["mean"]))
|
||||
self.assertTrue(all(isinstance(std, float) for std in history["std"]))
|
||||
for params in history["parameters"]:
|
||||
self.assertTrue(all(isinstance(param, float) for param in params))
|
||||
|
||||
def test_reuse(self):
|
||||
"""Test re-using a VQD algorithm instance."""
|
||||
vqd = VQD(k=1)
|
||||
with self.subTest(msg="assert running empty raises AlgorithmError"):
|
||||
with self.assertRaises(AlgorithmError):
|
||||
_ = vqd.compute_eigenvalues(operator=self.h2_op)
|
||||
|
||||
ansatz = TwoLocal(rotation_blocks=["ry", "rz"], entanglement_blocks="cz")
|
||||
vqd.ansatz = ansatz
|
||||
with self.subTest(msg="assert missing operator raises AlgorithmError"):
|
||||
with self.assertRaises(AlgorithmError):
|
||||
_ = vqd.compute_eigenvalues(operator=self.h2_op)
|
||||
|
||||
vqd.expectation = MatrixExpectation()
|
||||
vqd.quantum_instance = self.statevector_simulator
|
||||
with self.subTest(msg="assert VQE works once all info is available"):
|
||||
result = vqd.compute_eigenvalues(operator=self.h2_op)
|
||||
np.testing.assert_array_almost_equal(result.eigenvalues.real, self.h2_energy, decimal=2)
|
||||
|
||||
operator = PrimitiveOp(np.array([[1, 0, 0, 0], [0, -1, 0, 0], [0, 0, 2, 0], [0, 0, 0, 3]]))
|
||||
|
||||
with self.subTest(msg="assert minimum eigensolver interface works"):
|
||||
result = vqd.compute_eigenvalues(operator=operator)
|
||||
self.assertAlmostEqual(result.eigenvalues.real[0], -1.0, places=5)
|
||||
|
||||
def test_vqd_optimizer(self):
|
||||
"""Test running same VQD twice to re-use optimizer, then switch optimizer"""
|
||||
vqd = VQD(
|
||||
k=2,
|
||||
optimizer=SLSQP(),
|
||||
quantum_instance=QuantumInstance(BasicAer.get_backend("statevector_simulator")),
|
||||
)
|
||||
|
||||
def run_check():
|
||||
result = vqd.compute_eigenvalues(operator=self.h2_op)
|
||||
np.testing.assert_array_almost_equal(
|
||||
result.eigenvalues.real, self.h2_energy_excited, decimal=3
|
||||
)
|
||||
|
||||
run_check()
|
||||
|
||||
with self.subTest("Optimizer re-use"):
|
||||
run_check()
|
||||
|
||||
with self.subTest("Optimizer replace"):
|
||||
vqd.optimizer = L_BFGS_B()
|
||||
run_check()
|
||||
|
||||
@data(MatrixExpectation(), None)
|
||||
def test_backend_change(self, user_expectation):
|
||||
"""Test that VQE works when backend changes."""
|
||||
vqd = VQD(
|
||||
k=1,
|
||||
ansatz=TwoLocal(rotation_blocks=["ry", "rz"], entanglement_blocks="cz"),
|
||||
optimizer=SLSQP(maxiter=2),
|
||||
expectation=user_expectation,
|
||||
quantum_instance=BasicAer.get_backend("statevector_simulator"),
|
||||
)
|
||||
result0 = vqd.compute_eigenvalues(operator=self.h2_op)
|
||||
if user_expectation is not None:
|
||||
with self.subTest("User expectation kept."):
|
||||
self.assertEqual(vqd.expectation, user_expectation)
|
||||
|
||||
vqd.quantum_instance = BasicAer.get_backend("qasm_simulator")
|
||||
|
||||
# works also if no expectation is set, since it will be determined automatically
|
||||
result1 = vqd.compute_eigenvalues(operator=self.h2_op)
|
||||
|
||||
if user_expectation is not None:
|
||||
with self.subTest("Change backend with user expectation, it is kept."):
|
||||
self.assertEqual(vqd.expectation, user_expectation)
|
||||
|
||||
with self.subTest("Check results."):
|
||||
self.assertEqual(len(result0.optimal_point), len(result1.optimal_point))
|
||||
|
||||
def test_set_ansatz_to_none(self):
|
||||
"""Tests that setting the ansatz to None results in the default behavior"""
|
||||
vqd = VQD(
|
||||
k=1,
|
||||
ansatz=self.ryrz_wavefunction,
|
||||
optimizer=L_BFGS_B(),
|
||||
quantum_instance=self.statevector_simulator,
|
||||
)
|
||||
vqd.ansatz = None
|
||||
self.assertIsInstance(vqd.ansatz, RealAmplitudes)
|
||||
|
||||
def test_set_optimizer_to_none(self):
|
||||
"""Tests that setting the optimizer to None results in the default behavior"""
|
||||
vqd = VQD(
|
||||
k=1,
|
||||
ansatz=self.ryrz_wavefunction,
|
||||
optimizer=L_BFGS_B(),
|
||||
quantum_instance=self.statevector_simulator,
|
||||
)
|
||||
vqd.optimizer = None
|
||||
self.assertIsInstance(vqd.optimizer, SLSQP)
|
||||
|
||||
def test_aux_operators_list(self):
|
||||
"""Test list-based aux_operators."""
|
||||
wavefunction = self.ry_wavefunction
|
||||
vqd = VQD(k=2, ansatz=wavefunction, quantum_instance=self.statevector_simulator)
|
||||
|
||||
# Start with an empty list
|
||||
result = vqd.compute_eigenvalues(self.h2_op, aux_operators=[])
|
||||
np.testing.assert_array_almost_equal(
|
||||
result.eigenvalues.real, self.h2_energy_excited, decimal=2
|
||||
)
|
||||
self.assertIsNone(result.aux_operator_eigenvalues)
|
||||
|
||||
# Go again with two auxiliary operators
|
||||
aux_op1 = PauliSumOp.from_list([("II", 2.0)])
|
||||
aux_op2 = PauliSumOp.from_list([("II", 0.5), ("ZZ", 0.5), ("YY", 0.5), ("XX", -0.5)])
|
||||
aux_ops = [aux_op1, aux_op2]
|
||||
result = vqd.compute_eigenvalues(self.h2_op, aux_operators=aux_ops)
|
||||
np.testing.assert_array_almost_equal(
|
||||
result.eigenvalues.real, self.h2_energy_excited, decimal=2
|
||||
)
|
||||
self.assertEqual(len(result.aux_operator_eigenvalues), 2)
|
||||
# expectation values
|
||||
self.assertAlmostEqual(result.aux_operator_eigenvalues[0][0][0], 2, places=2)
|
||||
self.assertAlmostEqual(result.aux_operator_eigenvalues[0][1][0], 0, places=2)
|
||||
# standard deviations
|
||||
self.assertAlmostEqual(result.aux_operator_eigenvalues[0][1][1], 0.0)
|
||||
self.assertAlmostEqual(result.aux_operator_eigenvalues[0][1][1], 0.0)
|
||||
|
||||
# Go again with additional None and zero operators
|
||||
extra_ops = [*aux_ops, None, 0]
|
||||
result = vqd.compute_eigenvalues(self.h2_op, aux_operators=extra_ops)
|
||||
np.testing.assert_array_almost_equal(
|
||||
result.eigenvalues.real, self.h2_energy_excited, decimal=2
|
||||
)
|
||||
self.assertEqual(len(result.aux_operator_eigenvalues), 2)
|
||||
# expectation values
|
||||
self.assertAlmostEqual(result.aux_operator_eigenvalues[0][0][0], 2, places=2)
|
||||
self.assertAlmostEqual(result.aux_operator_eigenvalues[0][1][0], 0, places=2)
|
||||
self.assertEqual(result.aux_operator_eigenvalues[0][2][0], 0.0)
|
||||
self.assertEqual(result.aux_operator_eigenvalues[0][3][0], 0.0)
|
||||
# standard deviations
|
||||
self.assertAlmostEqual(result.aux_operator_eigenvalues[0][0][1], 0.0)
|
||||
self.assertAlmostEqual(result.aux_operator_eigenvalues[0][1][1], 0.0)
|
||||
self.assertEqual(result.aux_operator_eigenvalues[0][3][1], 0.0)
|
||||
|
||||
def test_aux_operators_dict(self):
|
||||
"""Test dictionary compatibility of aux_operators"""
|
||||
wavefunction = self.ry_wavefunction
|
||||
vqd = VQD(ansatz=wavefunction, quantum_instance=self.statevector_simulator)
|
||||
|
||||
# Start with an empty dictionary
|
||||
result = vqd.compute_eigenvalues(self.h2_op, aux_operators={})
|
||||
np.testing.assert_array_almost_equal(
|
||||
result.eigenvalues.real, self.h2_energy_excited, decimal=2
|
||||
)
|
||||
self.assertIsNone(result.aux_operator_eigenvalues)
|
||||
|
||||
# Go again with two auxiliary operators
|
||||
aux_op1 = PauliSumOp.from_list([("II", 2.0)])
|
||||
aux_op2 = PauliSumOp.from_list([("II", 0.5), ("ZZ", 0.5), ("YY", 0.5), ("XX", -0.5)])
|
||||
aux_ops = {"aux_op1": aux_op1, "aux_op2": aux_op2}
|
||||
result = vqd.compute_eigenvalues(self.h2_op, aux_operators=aux_ops)
|
||||
self.assertEqual(len(result.eigenvalues), 2)
|
||||
self.assertEqual(len(result.eigenstates), 2)
|
||||
self.assertEqual(result.eigenvalues.dtype, np.complex128)
|
||||
self.assertAlmostEqual(result.eigenvalues[0], -1.85727503)
|
||||
self.assertEqual(len(result.aux_operator_eigenvalues), 2)
|
||||
self.assertEqual(len(result.aux_operator_eigenvalues[0]), 2)
|
||||
# expectation values
|
||||
self.assertAlmostEqual(result.aux_operator_eigenvalues[0]["aux_op1"][0], 2, places=6)
|
||||
self.assertAlmostEqual(result.aux_operator_eigenvalues[0]["aux_op2"][0], 0, places=1)
|
||||
# standard deviations
|
||||
self.assertAlmostEqual(result.aux_operator_eigenvalues[0]["aux_op1"][1], 0.0)
|
||||
self.assertAlmostEqual(result.aux_operator_eigenvalues[0]["aux_op2"][1], 0.0)
|
||||
|
||||
# Go again with additional None and zero operators
|
||||
extra_ops = {**aux_ops, "None_operator": None, "zero_operator": 0}
|
||||
result = vqd.compute_eigenvalues(self.h2_op, aux_operators=extra_ops)
|
||||
self.assertEqual(len(result.eigenvalues), 2)
|
||||
self.assertEqual(len(result.eigenstates), 2)
|
||||
self.assertEqual(result.eigenvalues.dtype, np.complex128)
|
||||
self.assertAlmostEqual(result.eigenvalues[0], -1.85727503)
|
||||
self.assertEqual(len(result.aux_operator_eigenvalues), 2)
|
||||
self.assertEqual(len(result.aux_operator_eigenvalues[0]), 3)
|
||||
# expectation values
|
||||
self.assertAlmostEqual(result.aux_operator_eigenvalues[0]["aux_op1"][0], 2, places=6)
|
||||
self.assertAlmostEqual(result.aux_operator_eigenvalues[0]["aux_op2"][0], 0, places=6)
|
||||
self.assertEqual(result.aux_operator_eigenvalues[0]["zero_operator"][0], 0.0)
|
||||
self.assertTrue("None_operator" not in result.aux_operator_eigenvalues[0].keys())
|
||||
# standard deviations
|
||||
self.assertAlmostEqual(result.aux_operator_eigenvalues[0]["aux_op1"][1], 0.0)
|
||||
self.assertAlmostEqual(result.aux_operator_eigenvalues[0]["aux_op2"][1], 0.0)
|
||||
self.assertAlmostEqual(result.aux_operator_eigenvalues[0]["zero_operator"][1], 0.0)
|
||||
|
||||
def test_aux_operator_std_dev_pauli(self):
|
||||
"""Test non-zero standard deviations of aux operators with PauliExpectation."""
|
||||
wavefunction = self.ry_wavefunction
|
||||
vqd = VQD(
|
||||
ansatz=wavefunction,
|
||||
expectation=PauliExpectation(),
|
||||
initial_point=[
|
||||
1.70256666,
|
||||
-5.34843975,
|
||||
-0.39542903,
|
||||
5.99477786,
|
||||
-2.74374986,
|
||||
-4.85284669,
|
||||
0.2442925,
|
||||
-1.51638917,
|
||||
],
|
||||
optimizer=COBYLA(maxiter=0),
|
||||
quantum_instance=self.qasm_simulator,
|
||||
)
|
||||
|
||||
# Go again with two auxiliary operators
|
||||
aux_op1 = PauliSumOp.from_list([("II", 2.0)])
|
||||
aux_op2 = PauliSumOp.from_list([("II", 0.5), ("ZZ", 0.5), ("YY", 0.5), ("XX", -0.5)])
|
||||
aux_ops = [aux_op1, aux_op2]
|
||||
result = vqd.compute_eigenvalues(self.h2_op, aux_operators=aux_ops)
|
||||
self.assertEqual(len(result.aux_operator_eigenvalues), 2)
|
||||
# expectation values
|
||||
self.assertAlmostEqual(result.aux_operator_eigenvalues[0][0][0], 2.0, places=1)
|
||||
self.assertAlmostEqual(
|
||||
result.aux_operator_eigenvalues[0][1][0], 0.0019531249999999445, places=1
|
||||
)
|
||||
# standard deviations
|
||||
self.assertAlmostEqual(result.aux_operator_eigenvalues[0][0][1], 0.0)
|
||||
self.assertAlmostEqual(
|
||||
result.aux_operator_eigenvalues[0][1][1], 0.015183867579396111, places=1
|
||||
)
|
||||
|
||||
# Go again with additional None and zero operators
|
||||
aux_ops = [*aux_ops, None, 0]
|
||||
result = vqd.compute_eigenvalues(self.h2_op, aux_operators=aux_ops)
|
||||
self.assertEqual(len(result.aux_operator_eigenvalues[0]), 4)
|
||||
# expectation values
|
||||
self.assertAlmostEqual(result.aux_operator_eigenvalues[0][0][0], 2.0, places=1)
|
||||
self.assertAlmostEqual(
|
||||
result.aux_operator_eigenvalues[0][1][0], 0.0019531249999999445, places=1
|
||||
)
|
||||
self.assertEqual(result.aux_operator_eigenvalues[0][2][0], 0.0)
|
||||
self.assertEqual(result.aux_operator_eigenvalues[0][3][0], 0.0)
|
||||
# # standard deviations
|
||||
self.assertAlmostEqual(result.aux_operator_eigenvalues[0][0][1], 0.0)
|
||||
self.assertAlmostEqual(
|
||||
result.aux_operator_eigenvalues[0][1][1], 0.01548658094658011, places=1
|
||||
)
|
||||
self.assertAlmostEqual(result.aux_operator_eigenvalues[0][2][1], 0.0)
|
||||
self.assertAlmostEqual(result.aux_operator_eigenvalues[0][3][1], 0.0)
|
||||
|
||||
@unittest.skipUnless(has_aer(), "qiskit-aer doesn't appear to be installed.")
|
||||
def test_aux_operator_std_dev_aer_pauli(self):
|
||||
"""Test non-zero standard deviations of aux operators with AerPauliExpectation."""
|
||||
wavefunction = self.ry_wavefunction
|
||||
vqd = VQD(
|
||||
ansatz=wavefunction,
|
||||
expectation=AerPauliExpectation(),
|
||||
optimizer=COBYLA(maxiter=0),
|
||||
quantum_instance=QuantumInstance(
|
||||
backend=Aer.get_backend("qasm_simulator"),
|
||||
shots=1,
|
||||
seed_simulator=algorithm_globals.random_seed,
|
||||
seed_transpiler=algorithm_globals.random_seed,
|
||||
),
|
||||
)
|
||||
|
||||
# Go again with two auxiliary operators
|
||||
aux_op1 = PauliSumOp.from_list([("II", 2.0)])
|
||||
aux_op2 = PauliSumOp.from_list([("II", 0.5), ("ZZ", 0.5), ("YY", 0.5), ("XX", -0.5)])
|
||||
aux_ops = [aux_op1, aux_op2]
|
||||
result = vqd.compute_eigenvalues(self.h2_op, aux_operators=aux_ops)
|
||||
self.assertEqual(len(result.aux_operator_eigenvalues), 2)
|
||||
# expectation values
|
||||
self.assertAlmostEqual(result.aux_operator_eigenvalues[0][0][0], 2.0, places=1)
|
||||
self.assertAlmostEqual(
|
||||
result.aux_operator_eigenvalues[0][1][0], 0.6698863565455391, places=1
|
||||
)
|
||||
# standard deviations
|
||||
self.assertAlmostEqual(result.aux_operator_eigenvalues[0][0][1], 0.0)
|
||||
self.assertAlmostEqual(result.aux_operator_eigenvalues[0][1][1], 0.0, places=6)
|
||||
|
||||
# Go again with additional None and zero operators
|
||||
aux_ops = [*aux_ops, None, 0]
|
||||
result = vqd.compute_eigenvalues(self.h2_op, aux_operators=aux_ops)
|
||||
self.assertEqual(len(result.aux_operator_eigenvalues[-1]), 4)
|
||||
# expectation values
|
||||
self.assertAlmostEqual(result.aux_operator_eigenvalues[0][0][0], 2.0, places=6)
|
||||
self.assertAlmostEqual(
|
||||
result.aux_operator_eigenvalues[0][1][0], 0.6036400943063891, places=6
|
||||
)
|
||||
self.assertEqual(result.aux_operator_eigenvalues[0][2][0], 0.0)
|
||||
self.assertEqual(result.aux_operator_eigenvalues[0][3][0], 0.0)
|
||||
# standard deviations
|
||||
self.assertAlmostEqual(result.aux_operator_eigenvalues[0][0][1], 0.0)
|
||||
self.assertAlmostEqual(result.aux_operator_eigenvalues[0][1][1], 0.0, places=6)
|
||||
self.assertAlmostEqual(result.aux_operator_eigenvalues[0][2][1], 0.0)
|
||||
self.assertAlmostEqual(result.aux_operator_eigenvalues[0][3][1], 0.0)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
Loading…
Reference in New Issue