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

Implemented PVQD algorithm with primitives. #8705

Merged
merged 61 commits into from
Sep 22, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
61 commits
Select commit Hold shift + click to select a range
e3233b7
Implemented observables_evaluator.py with primitives.
Aug 30, 2022
d71f50b
Added evolvers problems and interfaces to time_evolvers package.
Aug 30, 2022
7a7c82b
Mostly updated trotter_qrte.py to use primitives.
Aug 30, 2022
54afe25
Added observables_evaluator.py that uses primitives.
Sep 1, 2022
d2b67d9
Added observables_evaluator.py that uses primitives.
Sep 1, 2022
c1276d2
Updated trotter_qrte.py to use primitives.
Sep 1, 2022
e06c6c2
Updated imports
Sep 1, 2022
02def38
Added estimator to pvqd.py (draft)
Sep 1, 2022
5a9bebe
Updated typehints and limited use of opflow.
Sep 5, 2022
356deae
Updated typehints and limited use of opflow.
Sep 5, 2022
14ba35d
Removed files out of scope for this PR.
Sep 5, 2022
4cf6f61
Added annotations import.
Sep 5, 2022
39c9735
Added observables_evaluator.py with primitives.
Sep 5, 2022
99adde7
Added ListOrDict support to observables_evaluator.py.
Sep 5, 2022
aace33e
Merge branch 'main' into pvqd-primitives
Sep 5, 2022
da2cdfa
Drafted pvqd.py with the fidelity primitive; type hints updated.
Sep 5, 2022
e7def3d
Accepting fidelity primitive.
Sep 6, 2022
6462bb0
Included CR suggestions.
Sep 6, 2022
604944c
Applied some CR comments.
Sep 6, 2022
d287c2f
Applied some CR comments.
Sep 6, 2022
23821e0
Added reno.
Sep 7, 2022
80a2e2b
Added reno.
Sep 7, 2022
a2960b2
Accepting Statevector.
Sep 7, 2022
9b5f50a
Added attributes docs.
Sep 7, 2022
4c40e00
Support for 0 operator.
Sep 7, 2022
cfe4447
Implemented pvqd unit tests with primitives; code refactoring.
Sep 8, 2022
cb03674
Added reno.
Sep 8, 2022
6b5fcfb
Updated algorithms init.
Sep 8, 2022
fe90188
Code refactoring.
Sep 8, 2022
12e79af
Removed old pvqd algorithm and related files.
Sep 8, 2022
4d69d6b
Merge branch 'observable-eval-primitives' into pvqd-primitives
Sep 8, 2022
61e6b1f
Merge branch 'evolution-framework-primitives' into pvqd-primitives
Sep 8, 2022
1ba639e
Merge branch 'main' into pvqd-primitives
Sep 8, 2022
107dc2b
Merge branch 'main' into evolution-framework-primitives
manoelmarques Sep 8, 2022
387ce78
Add pending deprecation for evolvers
manoelmarques Sep 8, 2022
a7e2f60
Renamed classes and linked to algorithms init.
Sep 8, 2022
34c2deb
Merge remote-tracking branch 'origin/evolution-framework-primitives' …
Sep 8, 2022
5c87255
fix docstring
manoelmarques Sep 8, 2022
d03bddf
Improved reno.
Sep 9, 2022
a656db0
Merge branch 'main' into evolution-framework-primitives
Sep 9, 2022
11e14d8
Merge branch 'evolution-framework-primitives' into pvqd-primitives
Sep 9, 2022
4a0bdd0
Updated classes names.
Sep 9, 2022
7680019
Code refactoring.
Sep 9, 2022
2d8b532
Applied CR comments.
Sep 9, 2022
d73c7cf
Removed unnecessary files.
Sep 9, 2022
ba0f2a2
Applied CR comments.
Sep 9, 2022
1dce473
Black fix.
Sep 9, 2022
d3c601e
Applied CR comments.
Sep 11, 2022
48291ec
Merge branch 'evolution-framework-primitives' into pvqd-primitives
Sep 11, 2022
17b9dfa
Init updates.
Sep 13, 2022
6191628
Merge branch 'main' into pvqd-primitives
Sep 15, 2022
1428086
Reduced number of cyclic import.
Sep 15, 2022
e40e175
Fix for cyclic import
woodsp-ibm Sep 20, 2022
a30dcb0
Doc fix
woodsp-ibm Sep 20, 2022
a821eb3
Merge branch 'main' into pvqd-primitives
Sep 21, 2022
88d3e5c
Updated init.
Sep 21, 2022
c297048
Added error raising and unit test.
Sep 21, 2022
33f0588
Apply suggestions from code review
Sep 22, 2022
2f8cb7f
Merge branch 'main' into pvqd-primitives
Sep 22, 2022
c730c56
Fixed code example.
Sep 22, 2022
7566772
Merge branch 'main' into pvqd-primitives
mergify[bot] Sep 22, 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
6 changes: 3 additions & 3 deletions qiskit/algorithms/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -113,8 +113,6 @@
RealEvolver
ImaginaryEvolver
TrotterQRTE
PVQD
PVQDResult
EvolutionResult
EvolutionProblem

Expand All @@ -132,6 +130,8 @@

RealTimeEvolver
ImaginaryTimeEvolver
PVQD
PVQDResult
TimeEvolutionResult
TimeEvolutionProblem

Expand Down Expand Up @@ -310,7 +310,7 @@
from .observables_evaluator import estimate_observables
from .evolvers.trotterization import TrotterQRTE

from .evolvers.pvqd import PVQD, PVQDResult
from .time_evolvers.pvqd import PVQD, PVQDResult

__all__ = [
"AlgorithmJob",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
import numpy as np

from qiskit import QuantumCircuit
from qiskit.algorithms import AlgorithmJob
from qiskit.algorithms.algorithm_job import AlgorithmJob
from qiskit.circuit import ParameterVector
from .state_fidelity_result import StateFidelityResult

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,33 +11,32 @@
# that they have been altered from the originals.

"""The projected Variational Quantum Dynamics Algorithm."""

from typing import Optional, Union, List, Tuple, Callable
from __future__ import annotations

import logging
from typing import Callable

import numpy as np

from qiskit import QiskitError
from qiskit.algorithms.optimizers import Optimizer, Minimizer
from qiskit.circuit import QuantumCircuit, ParameterVector
from qiskit.circuit import QuantumCircuit, ParameterVector, Parameter
from qiskit.circuit.library import PauliEvolutionGate
from qiskit.extensions import HamiltonianGate
from qiskit.providers import Backend
from qiskit.opflow import OperatorBase, CircuitSampler, ExpectationBase, StateFn, MatrixOp
from qiskit.opflow import PauliSumOp
from qiskit.primitives import BaseEstimator
from qiskit.quantum_info.operators.base_operator import BaseOperator
from qiskit.synthesis import EvolutionSynthesis, LieTrotter
from qiskit.utils import QuantumInstance

from ...exceptions import AlgorithmError, QiskitError
from .pvqd_result import PVQDResult
from .utils import _get_observable_evaluator, _is_gradient_supported

from ..evolution_problem import EvolutionProblem
from ..evolution_result import EvolutionResult
from ..real_evolver import RealEvolver
from ..time_evolution_problem import TimeEvolutionProblem
from ..time_evolution_result import TimeEvolutionResult
from ..real_time_evolver import RealTimeEvolver
from ...state_fidelities.base_state_fidelity import BaseStateFidelity
from ...optimizers import Optimizer, Minimizer

logger = logging.getLogger(__name__)


class PVQD(RealEvolver):
class PVQD(RealTimeEvolver):
"""The projected Variational Quantum Dynamics (p-VQD) Algorithm.

In each timestep, this algorithm computes the next state with a Trotter formula
Expand All @@ -52,7 +51,6 @@ class PVQD(RealEvolver):

ansatz (QuantumCircuit): The parameterized circuit representing the time-evolved state.
initial_parameters (np.ndarray): The parameters of the ansatz at time 0.
expectation (ExpectationBase): The method to compute expectation values.
optimizer (Optional[Union[Optimizer, Minimizer]]): The classical optimization routine
used to maximize the fidelity of the Trotter step and ansatz.
num_timesteps (Optional[int]): The number of timesteps to take. If None, it is automatically
Expand All @@ -73,14 +71,19 @@ class PVQD(RealEvolver):

import numpy as np

from qiskit.algorithms.state_fidelities import ComputeUncompute
from qiskit.algorithms.time_evolvers.pvqd import PVQD
from qiskit.primitives import Estimator
from qiskit import BasicAer
from qiskit.circuit.library import EfficientSU2
from qiskit.opflow import X, Z, I, MatrixExpectation

backend = BasicAer.get_backend("statevector_simulator")
expectation = MatrixExpectation()
hamiltonian = 0.1 * (Z ^ Z) + (I ^ X) + (X ^ I)
observable = Z ^ Z
from qiskit.quantum_info import Pauli, SparsePauliOp
from qiskit.algorithms.optimizers import L_BFGS_B

sampler = Sampler()
fidelity = ComputeUncompute(sampler)
estimator = Estimator()
hamiltonian = 0.1 * SparsePauliOp([Pauli("ZZ"), Pauli("IX"), Pauli("XI")])
observable = Pauli("ZZ")
ansatz = EfficientSU2(2, reps=1)
initial_parameters = np.zeros(ansatz.num_parameters)

Expand All @@ -89,12 +92,12 @@ class PVQD(RealEvolver):

# setup the algorithm
pvqd = PVQD(
fidelity,
ansatz,
estimator,
initial_parameters,
num_timesteps=100,
optimizer=optimizer,
quantum_instance=backend,
expectation=expectation
)

# specify the evolution problem
Expand All @@ -114,30 +117,32 @@ class PVQD(RealEvolver):

def __init__(
self,
fidelity: BaseStateFidelity,
ansatz: QuantumCircuit,
initial_parameters: np.ndarray,
expectation: ExpectationBase,
optimizer: Optional[Union[Optimizer, Minimizer]] = None,
num_timesteps: Optional[int] = None,
evolution: Optional[EvolutionSynthesis] = None,
estimator: BaseEstimator | None = None,
optimizer: Optimizer | Minimizer | None = None,
num_timesteps: int | None = None,
evolution: EvolutionSynthesis | None = None,
use_parameter_shift: bool = True,
initial_guess: Optional[np.ndarray] = None,
quantum_instance: Optional[Union[Backend, QuantumInstance]] = None,
initial_guess: np.ndarray | None = None,
) -> None:
"""
Args:
fidelity: A fidelity primitive used by the algorithm.
ansatz: A parameterized circuit preparing the variational ansatz to model the
time evolved quantum state.
initial_parameters: The initial parameters for the ansatz. Together with the ansatz,
these define the initial state of the time evolution.
expectation: The expectation converter to evaluate expectation values.
estimator: An estimator primitive used for calculating expected values of auxiliary
operators (if provided via the problem).
optimizer: The classical optimizers used to minimize the overlap between
Trotterization and ansatz. Can be either a :class:`.Optimizer` or a callable
using the :class:`.Minimizer` protocol. This argument is optional since it is
not required for :meth:`get_loss`, but it has to be set before :meth:`evolve`
is called.
num_timestep: The number of time steps. If ``None`` it will be set such that the timestep
is close to 0.01.
num_timesteps: The number of time steps. If ``None`` it will be set such that the
timestep is close to 0.01.
evolution: The evolution synthesis to use for the construction of the Trotter step.
Defaults to first-order Lie-Trotter decomposition, see also
:mod:`~qiskit.synthesis.evolution` for different options.
Expand All @@ -147,7 +152,6 @@ def __init__(
initial_guess: The initial guess for the first VQE optimization. Afterwards the
previous iteration result is used as initial guess. If None, this is set to
a random vector with elements in the interval :math:`[-0.01, 0.01]`.
quantum_instance: The backend or quantum instance used to evaluate the circuits.
"""
super().__init__()
if evolution is None:
Expand All @@ -158,36 +162,19 @@ def __init__(
self.num_timesteps = num_timesteps
self.optimizer = optimizer
self.initial_guess = initial_guess
self.expectation = expectation
self.estimator = estimator
self.fidelity_primitive = fidelity
self.evolution = evolution
self.use_parameter_shift = use_parameter_shift

self._sampler = None
self.quantum_instance = quantum_instance

@property
def quantum_instance(self) -> Optional[QuantumInstance]:
"""Return the current quantum instance."""
return self._quantum_instance

@quantum_instance.setter
def quantum_instance(self, quantum_instance: Optional[Union[Backend, QuantumInstance]]) -> None:
"""Set the quantum instance and circuit sampler."""
if quantum_instance is not None:
if not isinstance(quantum_instance, QuantumInstance):
quantum_instance = QuantumInstance(quantum_instance)
self._sampler = CircuitSampler(quantum_instance)

self._quantum_instance = quantum_instance

def step(
self,
hamiltonian: OperatorBase,
hamiltonian: BaseOperator | PauliSumOp,
ansatz: QuantumCircuit,
theta: np.ndarray,
dt: float,
initial_guess: np.ndarray,
) -> Tuple[np.ndarray, float]:
) -> tuple[np.ndarray, float]:
"""Perform a single time step.

Args:
Expand Down Expand Up @@ -223,11 +210,11 @@ def step(

def get_loss(
self,
hamiltonian: OperatorBase,
hamiltonian: BaseOperator | PauliSumOp,
ansatz: QuantumCircuit,
dt: float,
current_parameters: np.ndarray,
) -> Tuple[Callable[[np.ndarray], float], Optional[Callable[[np.ndarray], np.ndarray]]]:
) -> tuple[Callable[[np.ndarray], float], Callable[[np.ndarray], np.ndarray]] | None:

"""Get a function to evaluate the infidelity between Trotter step and ansatz.

Expand All @@ -247,42 +234,50 @@ def get_loss(
# use Trotterization to evolve the current state
trotterized = ansatz.bind_parameters(current_parameters)

if isinstance(hamiltonian, MatrixOp):
evolution_gate = HamiltonianGate(hamiltonian.primitive, time=dt)
else:
evolution_gate = PauliEvolutionGate(hamiltonian, time=dt, synthesis=self.evolution)
evolution_gate = PauliEvolutionGate(hamiltonian, time=dt, synthesis=self.evolution)

trotterized.append(evolution_gate, ansatz.qubits)

# define the overlap of the Trotterized state and the ansatz
x = ParameterVector("w", ansatz.num_parameters)
shifted = ansatz.assign_parameters(current_parameters + x)
overlap = StateFn(trotterized).adjoint() @ StateFn(shifted)

converted = self.expectation.convert(overlap)

def evaluate_loss(
displacement: Union[np.ndarray, List[np.ndarray]]
) -> Union[float, List[float]]:
def evaluate_loss(displacement: np.ndarray | list[np.ndarray]) -> float | list[float]:
"""Evaluate the overlap of the ansatz with the Trotterized evolution.

Args:
displacement: The parameters for the ansatz.

Returns:
The fidelity of the ansatz with parameters ``theta`` and the Trotterized evolution.

Raises:
AlgorithmError: If a primitive job fails.
"""
if isinstance(displacement, list):
displacement = np.asarray(displacement)
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
param_dicts = self._transpose_param_dicts(value_dict)
num_of_param_sets = len(param_dicts)
states1 = [trotterized] * num_of_param_sets
states2 = [shifted] * num_of_param_sets
param_dicts2 = [list(param_dict.values()) for param_dict in param_dicts]
# the first state does not have free parameters so values_1 will be None by default
try:
job = self.fidelity_primitive.run(states1, states2, values_2=param_dicts2)
fidelities = job.result().fidelities
except Exception as exc:
raise AlgorithmError("The primitive job failed!") from exc

if len(fidelities) == 1:
fidelities = fidelities[0]

# in principle, we could add different loss functions here, but we're currently
# not aware of a use-case for a different one than in the paper
return 1 - np.abs(sampled.eval()) ** 2
return 1 - np.abs(fidelities) ** 2

if _is_gradient_supported(ansatz) and self.use_parameter_shift:

Expand Down Expand Up @@ -314,8 +309,25 @@ def evaluate_gradient(displacement: np.ndarray) -> np.ndarray:

return evaluate_loss, evaluate_gradient

def evolve(self, evolution_problem: EvolutionProblem) -> EvolutionResult:
"""
def _transpose_param_dicts(self, params: dict) -> list[dict[Parameter, float]]:
p_0 = list(params.values())[0]
if isinstance(p_0, (list, np.ndarray)):
num_parameterizations = len(p_0)
param_bindings = [
{param: value_list[i] for param, value_list in params.items()} # type: ignore
for i in range(num_parameterizations)
]
else:
param_bindings = [params]

return param_bindings

def evolve(self, evolution_problem: TimeEvolutionProblem) -> TimeEvolutionResult:
r"""Perform real time evolution :math:`\exp(-i t H)|\Psi\rangle`.

Evolves an initial state :math:`|\Psi\rangle` for a time :math:`t`
under a Hamiltonian :math:`H`, as provided in the ``evolution_problem``.

Args:
evolution_problem: The evolution problem containing the hamiltonian, total evolution
time and observables to evaluate.
Expand All @@ -324,7 +336,8 @@ def evolve(self, evolution_problem: EvolutionProblem) -> EvolutionResult:
A result object containing the evolution information and evaluated observables.

Raises:
ValueError: If the evolution time is not positive or the timestep is too small.
ValueError: If ``aux_operators`` provided in the time evolution problem but no estimator
provided to the algorithm.
NotImplementedError: If the evolution problem contains an initial state.
"""
self._validate_setup()
Expand All @@ -346,8 +359,12 @@ def evolve(self, evolution_problem: EvolutionProblem) -> EvolutionResult:

# get the function to evaluate the observables for a given set of ansatz parameters
if observables is not None:
if self.estimator is None:
raise ValueError(
"The evolution problem contained aux_operators but no estimator was provided. "
)
evaluate_observables = _get_observable_evaluator(
self.ansatz, observables, self.expectation, self._sampler
self.ansatz, observables, self.estimator
)
observable_values = [evaluate_observables(self.initial_parameters)]

Expand Down Expand Up @@ -392,7 +409,7 @@ def _validate_setup(self, skip=None):
if skip is None:
skip = {}

required_attributes = {"quantum_instance", "optimizer"}.difference(skip)
required_attributes = {"optimizer"}.difference(skip)

for attr in required_attributes:
if getattr(self, attr, None) is None:
Expand Down
Loading