Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Migrating AdaptVQE to Qiskit Terra #7930

Merged
merged 66 commits into from
Sep 28, 2022
Merged
Show file tree
Hide file tree
Changes from 65 commits
Commits
Show all changes
66 commits
Select commit Hold shift + click to select a range
192355b
added adapt_vqe
fs1132429 Apr 13, 2022
7f15dbd
implemented most of the suggested changes
fs1132429 Apr 15, 2022
609a8e1
implemented the suggested changes
fs1132429 Apr 16, 2022
3304230
added the changes
fs1132429 Apr 18, 2022
65e6107
.
fs1132429 Apr 24, 2022
673083d
made the suggested changes
fs1132429 Apr 24, 2022
4d8e22d
added the code
fs1132429 May 25, 2022
a570435
test added
fs1132429 May 25, 2022
76df1fd
added adapt_vqe
fs1132429 May 25, 2022
a926e6d
.
fs1132429 May 29, 2022
24d82b8
.
fs1132429 May 29, 2022
2b331e6
added changes
fs1132429 Jun 7, 2022
eeb888d
added changes
fs1132429 Jun 7, 2022
4016183
added changes
fs1132429 Jun 7, 2022
be79ee5
.
fs1132429 Jun 7, 2022
d6e1cbe
Merge branch 'adapt_vqe2' into adapt_vqe
fs1132429 Jun 7, 2022
8c8fd11
added the changes
fs1132429 Jun 8, 2022
4a3cfc2
.
fs1132429 Jun 8, 2022
692c5a4
added
fs1132429 Jun 8, 2022
85eaddc
formatting done
fs1132429 Jun 15, 2022
073fbb6
Update adapt_vqe.py
fs1132429 Jun 15, 2022
6aa09e6
made the suggested changes
fs1132429 Jun 29, 2022
f5e7aaf
made the changes
fs1132429 Jul 5, 2022
972b925
lint changes
fs1132429 Jul 5, 2022
b025167
removed initial point from init
fs1132429 Jul 5, 2022
45f9489
black changes
fs1132429 Jul 5, 2022
5806d95
added inline definition
fs1132429 Jul 5, 2022
7cd5a64
made the changes
fs1132429 Jul 12, 2022
4a16b84
removed sys import
fs1132429 Jul 12, 2022
9c588e7
Update qiskit/algorithms/minimum_eigen_solvers/adapt_vqe.py
fs1132429 Jul 12, 2022
f818a77
Update qiskit/algorithms/minimum_eigen_solvers/adapt_vqe.py
fs1132429 Jul 12, 2022
7eb5c6a
Update qiskit/algorithms/minimum_eigen_solvers/adapt_vqe.py
fs1132429 Jul 12, 2022
f3a3057
Update qiskit/algorithms/minimum_eigen_solvers/adapt_vqe.py
fs1132429 Jul 12, 2022
29ed4d2
Update qiskit/algorithms/minimum_eigen_solvers/adapt_vqe.py
fs1132429 Jul 12, 2022
561cb06
Update qiskit/algorithms/minimum_eigen_solvers/adapt_vqe.py
fs1132429 Jul 12, 2022
dc87bc0
Update qiskit/algorithms/minimum_eigen_solvers/adapt_vqe.py
fs1132429 Jul 12, 2022
b08fa47
Update qiskit/algorithms/minimum_eigen_solvers/adapt_vqe.py
fs1132429 Jul 12, 2022
982e239
Update qiskit/algorithms/minimum_eigen_solvers/adapt_vqe.py
fs1132429 Jul 12, 2022
031b7bd
Update qiskit/algorithms/minimum_eigen_solvers/adapt_vqe.py
fs1132429 Jul 12, 2022
c0fba12
Update qiskit/algorithms/minimum_eigen_solvers/adapt_vqe.py
fs1132429 Jul 12, 2022
3fa1e64
Update qiskit/algorithms/minimum_eigen_solvers/adapt_vqe.py
fs1132429 Jul 12, 2022
2356367
added
fs1132429 Jul 12, 2022
83a163f
added
fs1132429 Jul 12, 2022
45d06b0
added
fs1132429 Jul 12, 2022
c72cc8b
.
fs1132429 Jul 18, 2022
f0d7783
.
fs1132429 Jul 18, 2022
f57c118
made final changes
fs1132429 Jul 19, 2022
9fab0fb
fix: evaluate gradients via commutator relation
mrossinek Jul 22, 2022
69ebf6d
Merge pull request #14 from mrossinek/adapt_vqe
fs1132429 Jul 22, 2022
c9381d5
.
fs1132429 Jul 25, 2022
392c519
Merge branch 'adapt_vqe' of https://github.com/fs1132429/qiskit-terra…
fs1132429 Aug 1, 2022
885bed9
made the changes
fs1132429 Aug 2, 2022
8ad2ff2
.
fs1132429 Aug 9, 2022
cb42abc
fix: AdaptVQE gradient computation unittest
mrossinek Sep 2, 2022
c9b8557
Merge branch 'main' into adapt_vqe
mrossinek Sep 2, 2022
a09332d
Merge branch 'main' into adapt_vqe
mrossinek Sep 3, 2022
ff471aa
fix: remove unused import
mrossinek Sep 3, 2022
9974290
Merge branch 'main' into adapt_vqe
mrossinek Sep 27, 2022
1abb4c8
refactor: migrate AdaptVQE to primitive-based module
mrossinek Sep 23, 2022
d1fe000
Add a release note
mrossinek Sep 27, 2022
3fffc34
Apply suggestions from code review
mrossinek Sep 27, 2022
a3615d5
Pass parameters to estimate_observables method
mrossinek Sep 27, 2022
8b96c1a
Apply suggestions from code review
mrossinek Sep 28, 2022
8e47ab9
More changes based on code review
mrossinek Sep 28, 2022
4d254da
Avoid deep-copying the ansatz
mrossinek Sep 28, 2022
1983023
Merge branch 'main' into adapt_vqe
mergify[bot] Sep 28, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions qiskit/algorithms/minimum_eigensolvers/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,20 +25,25 @@
MinimumEigensolver
NumPyMinimumEigensolver
VQE
AdaptVQE

.. autosummary::
:toctree: ../stubs/

MinimumEigensolverResult
NumPyMinimumEigensolverResult
VQEResult
AdaptVQEResult
"""

from .adapt_vqe import AdaptVQE, AdaptVQEResult
from .minimum_eigensolver import MinimumEigensolver, MinimumEigensolverResult
from .numpy_minimum_eigensolver import NumPyMinimumEigensolver, NumPyMinimumEigensolverResult
from .vqe import VQE, VQEResult

__all__ = [
"AdaptVQE",
"AdaptVQEResult",
mrossinek marked this conversation as resolved.
Show resolved Hide resolved
"MinimumEigensolver",
"MinimumEigensolverResult",
"NumPyMinimumEigensolver",
Expand Down
311 changes: 311 additions & 0 deletions qiskit/algorithms/minimum_eigensolvers/adapt_vqe.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,311 @@
# 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.

"""An implementation of the AdaptVQE algorithm."""

from __future__ import annotations

from enum import Enum
from typing import Optional, Sequence

import re
import logging

import numpy as np

from qiskit import QiskitError
from qiskit.algorithms.list_or_dict import ListOrDict
from qiskit.quantum_info.operators.base_operator import BaseOperator
from qiskit.opflow import OperatorBase, PauliSumOp
from qiskit.circuit.library import EvolvedOperatorAnsatz
from qiskit.utils.validation import validate_min

from .vqe import VQE, VQEResult
from ..observables_evaluator import estimate_observables
from ..variational_algorithm import VariationalAlgorithm


logger = logging.getLogger(__name__)


class TerminationCriterion(Enum):
"""A class enumerating the various finishing criteria."""

CONVERGED = "Threshold converged"
CYCLICITY = "Aborted due to a cyclic selection of evolution operators"
MAXIMUM = "Maximum number of iterations reached"


class AdaptVQE(VariationalAlgorithm):
"""The Adaptive Variational Quantum Eigensolver algorithm.

`AdaptVQE <https://arxiv.org/abs/1812.11173>`__ is a quantum algorithm which creates a compact
ansatz from a set of evolution operators. It iteratively extends the ansatz circuit, by
selecting the building block that leads to the largest gradient from a set of candidates. In
chemistry, this is usually a list of orbital excitations. Thus, a common choice of ansatz to be
used with this algorithm is the Unitary Coupled Cluster ansatz implemented in Qiskit Nature.
This results in a wavefunction ansatz which is uniquely adapted to the operator whose minimum
eigenvalue is being determined. This class relies on a supplied instance of :class:`~.VQE` to
find the minimum eigenvalue. The performance of AdaptVQE significantly depends on the
minimization routine.

.. code-block:: python

from qiskit.algorithms.minimum_eigensolvers import AdaptVQE, VQE
from qiskit.algorithms.optimizers import SLSQP
from qiskit.primitives import Estimator
from qiskit.circuit.library import EvolvedOperatorAnsatz

# get your Hamiltonian
hamiltonian = ...

# construct your ansatz
ansatz = EvolvedOperatorAnsatz(...)

vqe = VQE(Estimator(), ansatz, SLSQP())

adapt_vqe = AdaptVQE(vqe)

eigenvalue, _ = adapt_vqe.compute_minimum_eigenvalue(hamiltonian)

The following attributes can be set via the initializer but can also be read and updated once
the AdaptVQE object has been constructed.

mrossinek marked this conversation as resolved.
Show resolved Hide resolved
Attributes:
solver: a :class:`~.VQE` instance used internally to compute the minimum eigenvalues.
It is a requirement that the :attr:`~.VQE.ansatz` of this solver is of type
:class:`qiskit.circuit.library.EvolvedOperatorAnsatz`.
threshold: the convergence threshold for the algorithm. Once all gradients have an absolute
value smaller than this threshold, the algorithm terminates.
max_iterations: the maximum number of iterations for the adaptive loop. If ``None``, the
algorithm is not bound in its number of iterations.
"""

def __init__(
self,
solver: VQE,
mrossinek marked this conversation as resolved.
Show resolved Hide resolved
*,
threshold: float = 1e-5,
max_iterations: int | None = None,
) -> None:
"""
Args:
solver: a :class:`~.VQE` instance used internally to compute the minimum eigenvalues.
It is a requirement that the :attr:`~.VQE.ansatz` of this solver is of type
:class:`qiskit.circuit.library.EvolvedOperatorAnsatz`.
threshold: the convergence threshold for the algorithm. Once all gradients have an
absolute value smaller than this threshold, the algorithm terminates.
max_iterations: the maximum number of iterations for the adaptive loop. If ``None``, the
algorithm is not bound in its number of iterations.
mrossinek marked this conversation as resolved.
Show resolved Hide resolved
"""
validate_min("threshold", threshold, 1e-15)

self.solver = solver
self.threshold = threshold
self.max_iterations = max_iterations
self._tmp_ansatz: EvolvedOperatorAnsatz | None = None
self._excitation_pool: list[OperatorBase] = []
self._excitation_list: list[OperatorBase] = []

@property
def initial_point(self) -> Sequence[float] | None:
mrossinek marked this conversation as resolved.
Show resolved Hide resolved
"""Returns the initial point of the internal :class:`~.VQE` solver."""
return self.solver.initial_point

@initial_point.setter
def initial_point(self, value: Sequence[float] | None) -> None:
"""Sets the initial point of the internal :class:`~.VQE` solver."""
self.solver.initial_point = value

def _compute_gradients(
self,
theta: list[float],
operator: OperatorBase,
) -> list[tuple[complex, complex]]:
"""
Computes the gradients for all available excitation operators.

Args:
theta: List of (up to now) optimal parameters.
operator: operator whose gradient needs to be computed.
Returns:
List of pairs consisting of the computed gradient and excitation operator.
"""
# The excitations operators are applied later as exp(i*theta*excitation).
# For this commutator, we need to explicitly pull in the imaginary phase.
commutators = [1j * (operator @ exc - exc @ operator) for exc in self._excitation_pool]
res = estimate_observables(self.solver.estimator, self.solver.ansatz, commutators, theta)
return res

@staticmethod
def _check_cyclicity(indices: list[int]) -> bool:
"""
Auxiliary function to check for cycles in the indices of the selected excitations.

Args:
indices: The list of chosen gradient indices.

Returns:
Whether repeating sequences of indices have been detected.
"""
cycle_regex = re.compile(r"(\b.+ .+\b)( \b\1\b)+")
# reg-ex explanation:
# 1. (\b.+ .+\b) will match at least two numbers and try to match as many as possible. The
# word boundaries in the beginning and end ensure that now numbers are split into digits.
# 2. the match of this part is placed into capture group 1
# 3. ( \b\1\b)+ will match a space followed by the contents of capture group 1 (again
# delimited by word boundaries to avoid separation into digits).
# -> this results in any sequence of at least two numbers being detected
match = cycle_regex.search(" ".join(map(str, indices)))
logger.debug("Cycle detected: %s", match)
logger.info("Alternating sequence found. Finishing.")
# Additionally we also need to check whether the last two numbers are identical, because the
# reg-ex above will only find cycles of at least two consecutive numbers.
# It is sufficient to assert that the last two numbers are different due to the iterative
# nature of the algorithm.
return match is not None or (len(indices) > 1 and indices[-2] == indices[-1])

def compute_minimum_eigenvalue(
self,
operator: BaseOperator | PauliSumOp,
aux_operators: ListOrDict[BaseOperator | PauliSumOp] | None = None,
) -> AdaptVQEResult:
"""Computes the minimum eigenvalue.

Args:
operator: Operator whose minimum eigenvalue we want to find.
aux_operators: Additional auxiliary operators to evaluate.

Raises:
TypeError: If an ansatz other than :class:`~.EvolvedOperatorAnsatz` is provided.
QiskitError: If all evaluated gradients lie below the convergence threshold in the first
iteration of the algorithm.

Returns:
An :class:`~.AdaptVQEResult` which is a :class:`~.VQEResult` but also but also
includes runtime information about the AdaptVQE algorithm like the number of iterations,
termination criterion, and the final maximum gradient.
"""
if not isinstance(self.solver.ansatz, EvolvedOperatorAnsatz):
raise TypeError("The AdaptVQE ansatz must be of the EvolvedOperatorAnsatz type.")

# Overwrite the solver's ansatz with the initial state
self._tmp_ansatz = self.solver.ansatz
self._excitation_pool = self._tmp_ansatz.operators
self.solver.ansatz = self._tmp_ansatz.initial_state
mrossinek marked this conversation as resolved.
Show resolved Hide resolved

prev_op_indices: list[int] = []
theta: list[float] = []
max_grad: tuple[float, Optional[PauliSumOp]] = (0.0, None)
self._excitation_list = []
iteration = 0
while self.max_iterations is None or iteration < self.max_iterations:
iteration += 1
logger.info("--- Iteration #%s ---", str(iteration))
# compute gradients
cur_grads = self._compute_gradients(theta, operator)
# pick maximum gradient
max_grad_index, max_grad = max(
enumerate(cur_grads), key=lambda item: np.abs(item[1][0])
)
# store maximum gradient's index for cycle detection
prev_op_indices.append(max_grad_index)
# log gradients
if np.abs(max_grad[0]) < self.threshold:
if iteration == 1:
raise QiskitError(
"All gradients have been evaluated to lie below the convergence threshold "
"during the first iteration of the algorithm. Try to either tighten the "
"convergence threshold or pick a different ansatz."
)
logger.info(
"AdaptVQE terminated successfully with a final maximum gradient: %s",
str(np.abs(max_grad[0])),
)
termination_criterion = TerminationCriterion.CONVERGED
break
# check indices of picked gradients for cycles
if self._check_cyclicity(prev_op_indices):
logger.info("Final maximum gradient: %s", str(np.abs(max_grad[0])))
termination_criterion = TerminationCriterion.CYCLICITY
break
# add new excitation to self._ansatz
self._excitation_list.append(self._excitation_pool[max_grad_index])
theta.append(0.0)
# run VQE on current Ansatz
self._tmp_ansatz.operators = self._excitation_list
self.solver.ansatz = self._tmp_ansatz
self.solver.initial_point = theta
raw_vqe_result = self.solver.compute_minimum_eigenvalue(operator)
theta = raw_vqe_result.optimal_point.tolist()
else:
# reached maximum number of iterations
termination_criterion = TerminationCriterion.MAXIMUM
logger.info("Maximum number of iterations reached. Finishing.")
logger.info("Final maximum gradient: %s", str(np.abs(max_grad[0])))

result = AdaptVQEResult()
result.combine(raw_vqe_result)
result.num_iterations = iteration
result.final_max_gradient = max_grad[0]
result.termination_criterion = termination_criterion

# once finished evaluate auxiliary operators if any
if aux_operators is not None:
aux_values = estimate_observables(
self.solver.estimator, self.solver.ansatz, aux_operators, result.optimal_point
)
result.aux_operators_evaluated = aux_values

logger.info("The final energy is: %s", str(result.eigenvalue))
self.solver.ansatz.operators = self._excitation_pool
return result


class AdaptVQEResult(VQEResult):
"""AdaptVQE Result."""

def __init__(self) -> None:
super().__init__()
self._num_iterations: int = None
self._final_max_gradient: float = None
self._termination_criterion: str = ""

@property
def num_iterations(self) -> int:
"""Returns number of iterations"""
return self._num_iterations

@num_iterations.setter
def num_iterations(self, value: int) -> None:
"""Sets number of iterations"""
self._num_iterations = value

@property
def final_max_gradient(self) -> float:
"""Returns final maximum gradient"""
return self._final_max_gradient

@final_max_gradient.setter
def final_max_gradient(self, value: float) -> None:
"""Sets final maximum gradient"""
self._final_max_gradient = value

@property
def termination_criterion(self) -> str:
"""Returns termination criterion"""
return self._termination_criterion

@termination_criterion.setter
def termination_criterion(self, value: str) -> None:
"""Sets termination criterion"""
self._termination_criterion = value
27 changes: 27 additions & 0 deletions releasenotes/notes/adapt-vqe-0f71234cb6ec92f8.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
---
features:
- |
Implements the :class:`qiskit.algorithms.minimum_eigensolvers.AdaptVQE`
algorithm. This algorithm uses a
:class:`qiskit.algorithms.minimum_eigensolvers.VQE` in combination with a
pool of operators from which to build out an
:class:`qiskit.circuit.library.EvolvedOperatorAnsatz` adaptively.
woodsp-ibm marked this conversation as resolved.
Show resolved Hide resolved

.. code-block:: python

from qiskit.algorithms.minimum_eigensolvers import AdaptVQE, VQE
from qiskit.algorithms.optimizers import SLSQP
from qiskit.primitives import Estimator
from qiskit.circuit.library import EvolvedOperatorAnsatz

# get your Hamiltonian
hamiltonian = ...

# construct your ansatz
ansatz = EvolvedOperatorAnsatz(...)

vqe = VQE(Estimator(), ansatz, SLSQP())

adapt_vqe = AdaptVQE(vqe)

result = adapt_vqe.compute_minimum_eigenvalue(hamiltonian)
Loading