Skip to content

Commit

Permalink
Support for time-dependent Hamiltonians and observable evaluation in …
Browse files Browse the repository at this point in the history
…``TrotterQRTE`` (#9565)

* added support for t-dep Hamiltonian to algorithms.time_evolvers.trotterization.TrotterQRTE with synthesis LieTrotter

* added support for t-dep Hamiltonians to TrotterQRTE, leaving ProductFormula-classes unchanged wrt main. Also added evaluation of auxiliary_operators at every time-step of TrotterQRTE

* added support for t-dep Hamiltonians to TrotterQRTE, leaving ProductFormula-classes unchanged wrt main. Also added evaluation of auxiliary_operators at every time-step of TrotterQRTE

* removed note that t-dep Hamiltonians are not supported

* modified and added tests for t-dependent Hamiltonians in TrotterQRTE

* added documentation

* adapted code according to PR comments, refined tests to include manual calculation of reference values

* rename new test

* Update qiskit/algorithms/time_evolvers/trotterization/trotter_qrte.py

Co-authored-by: Julien Gacon <[email protected]>

* Update qiskit/algorithms/time_evolvers/trotterization/trotter_qrte.py

Co-authored-by: Julien Gacon <[email protected]>

* Update qiskit/algorithms/time_evolvers/trotterization/trotter_qrte.py

Co-authored-by: Julien Gacon <[email protected]>

* Update qiskit/algorithms/time_evolvers/trotterization/trotter_qrte.py

Co-authored-by: Julien Gacon <[email protected]>

* added inplace argument to assign_parameters._assign_parameters. If False, copies parameter array and returns new bound array leaving the passed array untouched

* fixing ValueErrors in TimeEvolutionProblme and formatting

* added release note

* fix lint suggestions

---------

Co-authored-by: Julien Gacon <[email protected]>
  • Loading branch information
Durd3nT and Cryoris authored Feb 13, 2023
1 parent 659063e commit 41e0c3a
Show file tree
Hide file tree
Showing 10 changed files with 215 additions and 69 deletions.
7 changes: 1 addition & 6 deletions qiskit/algorithms/time_evolvers/time_evolution_problem.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# This code is part of Qiskit.
#
# (C) Copyright IBM 2022.
# (C) Copyright IBM 2022, 2023.
#
# 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
Expand Down Expand Up @@ -97,12 +97,7 @@ def time(self) -> float:
def time(self, time: float) -> None:
"""
Sets time and validates it.
Raises:
ValueError: If time is not positive.
"""
if time <= 0:
raise ValueError(f"Evolution time must be > 0 but was {time}.")
self._time = time

def validate_params(self) -> None:
Expand Down
5 changes: 3 additions & 2 deletions qiskit/algorithms/time_evolvers/time_evolution_result.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,9 @@ def __init__(
aux_ops_evaluated: Optional list of observables for which expected values on an evolved
state are calculated. These values are in fact tuples formatted as (mean, standard
deviation).
observables: Optional list of observables for which expected values for each timestep.
These values are in fact tuples formatted as (mean array, standard deviation array).
observables: Optional list of observables for which expected values are calculated for
each timestep. These values are in fact tuples formatted as (mean array, standard
deviation array).
times: Optional list of times at which each observable has been evaluated.
"""

Expand Down
122 changes: 93 additions & 29 deletions qiskit/algorithms/time_evolvers/trotterization/trotter_qrte.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# This code is part of Qiskit.
#
# (C) Copyright IBM 2021, 2022.
# (C) Copyright IBM 2021, 2023.
#
# 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
Expand All @@ -21,10 +21,13 @@
from qiskit.algorithms.observables_evaluator import estimate_observables
from qiskit.opflow import PauliSumOp
from qiskit.circuit.library import PauliEvolutionGate
from qiskit.circuit.parametertable import ParameterView
from qiskit.primitives import BaseEstimator
from qiskit.quantum_info import Pauli
from qiskit.quantum_info import Pauli, SparsePauliOp
from qiskit.synthesis import ProductFormula, LieTrotter

from qiskit.algorithms.utils.assign_params import _assign_parameters, _get_parameters


class TrotterQRTE(RealTimeEvolver):
"""Quantum Real Time Evolution using Trotterization.
Expand Down Expand Up @@ -55,16 +58,23 @@ def __init__(
self,
product_formula: ProductFormula | None = None,
estimator: BaseEstimator | None = None,
num_timesteps: int = 1,
) -> None:
"""
Args:
product_formula: A Lie-Trotter-Suzuki product formula. If ``None`` provided, the
Lie-Trotter first order product formula with a single repetition is used.
Lie-Trotter first order product formula with a single repetition is used. ``reps``
should be 1 to obtain a number of time-steps equal to ``num_timesteps`` and an
evaluation of :attr:`.TimeEvolutionProblem.aux_operators` at every time-step. If ``reps``
is larger than 1, the true number of time-steps will be ``num_timesteps * reps``.
num_timesteps: The number of time-steps the full evolution time is devided into
(repetitions of ``product_formula``)
estimator: An estimator primitive used for calculating expectation values of
``TimeEvolutionProblem.aux_operators``.
"""

self.product_formula = product_formula
self.num_timesteps = num_timesteps
self.estimator = estimator

@property
Expand Down Expand Up @@ -94,6 +104,25 @@ def estimator(self, estimator: BaseEstimator) -> None:
"""
self._estimator = estimator

@property
def num_timesteps(self) -> int:
"""Returns the number of timesteps."""
return self._num_timesteps

@num_timesteps.setter
def num_timesteps(self, num_timesteps: int) -> None:
"""
Sets the number of time-steps.
Raises:
ValueError: If num_timesteps is not positive.
"""
if num_timesteps <= 0:
raise ValueError(
f"Number of time steps must be positive integer, {num_timesteps} provided"
)
self._num_timesteps = num_timesteps

@classmethod
def supports_aux_operators(cls) -> bool:
"""
Expand All @@ -110,10 +139,8 @@ def evolve(self, evolution_problem: TimeEvolutionProblem) -> TimeEvolutionResult
Evolves a quantum state for a given time using the Trotterization method
based on a product formula provided. The result is provided in the form of a quantum
circuit. If auxiliary operators are included in the ``evolution_problem``, they are
evaluated on an evolved state using an estimator primitive provided.
.. note::
Time-dependent Hamiltonians are not supported.
evaluated on the ``init_state`` and on the evolved state at every step (``num_timesteps``
times) using an estimator primitive provided.
Args:
evolution_problem: Instance defining evolution problem. For the included Hamiltonian,
Expand All @@ -132,44 +159,81 @@ def evolve(self, evolution_problem: TimeEvolutionProblem) -> TimeEvolutionResult
ValueError: If an unsupported Hamiltonian type is provided.
"""
evolution_problem.validate_params()
if evolution_problem.t_param is not None:
raise ValueError(
"TrotterQRTE does not accept a time dependent Hamiltonian,"
"``t_param`` from the ``TimeEvolutionProblem`` should be set to ``None``."
)

if evolution_problem.aux_operators is not None and self.estimator is None:
raise ValueError(
"The time evolution problem contained ``aux_operators`` but no estimator was "
"provided. The algorithm continues without calculating these quantities. "
)
hamiltonian = evolution_problem.hamiltonian
if not isinstance(hamiltonian, (Pauli, PauliSumOp)):
if not isinstance(hamiltonian, (Pauli, PauliSumOp, SparsePauliOp)):
raise ValueError(
f"TrotterQRTE only accepts Pauli | PauliSumOp, {type(hamiltonian)} provided."
)
# the evolution gate
evolution_gate = PauliEvolutionGate(
hamiltonian, evolution_problem.time, synthesis=self.product_formula
)
t_param = evolution_problem.t_param
if t_param is not None and _get_parameters(hamiltonian.coeffs) != ParameterView([t_param]):
raise ValueError(
"Hamiltonian time parameter does not match evolution_problem.t_param "
"or contains multiple parameters"
)

# make sure PauliEvolutionGate does not implement more than one Trotter step
dt = evolution_problem.time / self.num_timesteps

if evolution_problem.initial_state is not None:
initial_state = evolution_problem.initial_state
evolved_state = QuantumCircuit(initial_state.num_qubits)
evolved_state.append(initial_state, evolved_state.qubits)
evolved_state.append(evolution_gate, evolved_state.qubits)

else:
raise ValueError("``initial_state`` must be provided in the ``TimeEvolutionProblem``.")

evaluated_aux_ops = None
evolved_state = QuantumCircuit(initial_state.num_qubits)
evolved_state.append(initial_state, evolved_state.qubits)

if evolution_problem.aux_operators is not None:
evaluated_aux_ops = estimate_observables(
self.estimator,
evolved_state,
evolution_problem.aux_operators,
None,
evolution_problem.truncation_threshold,
observables = []
observables.append(
estimate_observables(
self.estimator,
evolved_state,
evolution_problem.aux_operators,
None,
evolution_problem.truncation_threshold,
)
)
else:
observables = None

if t_param is None:
# the evolution gate
single_step_evolution_gate = PauliEvolutionGate(
hamiltonian, dt, synthesis=self.product_formula
)

for n in range(self.num_timesteps):
# if hamiltonian is time-dependent, bind new time-value at every step to construct
# evolution for next step
if t_param is not None:
time_value = (n + 1) * dt
bound_coeffs = _assign_parameters(hamiltonian.coeffs, [time_value])
single_step_evolution_gate = PauliEvolutionGate(
SparsePauliOp(hamiltonian.paulis, bound_coeffs),
dt,
synthesis=self.product_formula,
)
evolved_state.append(single_step_evolution_gate, evolved_state.qubits)

if evolution_problem.aux_operators is not None:
observables.append(
estimate_observables(
self.estimator,
evolved_state,
evolution_problem.aux_operators,
None,
evolution_problem.truncation_threshold,
)
)

evaluated_aux_ops = None
if evolution_problem.aux_operators is not None:
evaluated_aux_ops = observables[-1]

return TimeEvolutionResult(evolved_state, evaluated_aux_ops)
return TimeEvolutionResult(evolved_state, evaluated_aux_ops, observables)
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@
"""Class for solving linear equations for Quantum Time Evolution."""
from __future__ import annotations

import copy
from collections.abc import Mapping, Sequence
from typing import Callable

Expand All @@ -24,7 +23,7 @@
from qiskit.quantum_info import SparsePauliOp
from qiskit.quantum_info.operators.base_operator import BaseOperator

from .ode.assign_params import _assign_parameters
from qiskit.algorithms.utils.assign_params import _assign_parameters

from ..variational_principles import VariationalPrinciple

Expand Down Expand Up @@ -93,7 +92,7 @@ def solve_lse(
self,
param_dict: Mapping[Parameter, float],
time_value: float | None = None,
) -> (np.ndarray, np.ndarray, np.ndarray):
) -> tuple(np.ndarray, np.ndarray, np.ndarray):
"""
Solve the system of linear equations underlying McLachlan's variational principle for the
calculation without error bounds.
Expand All @@ -116,8 +115,7 @@ def solve_lse(

if self._time_param is not None:
if time_value is not None:
parametrized_coeffs = copy.deepcopy(self._hamiltonian.coeffs)
bound_params_array = _assign_parameters(parametrized_coeffs, [time_value])
bound_params_array = _assign_parameters(self._hamiltonian.coeffs, [time_value])
hamiltonian = SparsePauliOp(self._hamiltonian.paulis, bound_params_array)
else:
raise ValueError(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@

from collections.abc import Sequence

import copy
import numpy as np

from qiskit.circuit import ParameterExpression
Expand All @@ -30,14 +31,32 @@ def _get_parameters(array: np.ndarray) -> ParameterView:
return ParameterView(ret)


def _assign_parameters(array: np.ndarray, parameter_values: Sequence[float]) -> np.ndarray:
"""Binds ``ParameterExpression``s in a numpy array to provided values."""
parameter_dict = dict(zip(_get_parameters(array), parameter_values))
for i, a in enumerate(array):
def _assign_parameters(
array: np.ndarray, parameter_values: Sequence[float], inplace: bool = False
) -> np.ndarray:
"""Binds ``ParameterExpression``s in a numpy array to provided values.
Args:
array: array of ``ParameterExpression``
parameter_values: array of values to bind to parameters
inplace: If ``False``, a copy of the array with the bound parameters is returned.
If True the array itself is modified.
Returns:
A copy of the array with bound parameters, if ``inplace`` is False, otherwise None.
"""
if inplace:
bound_array = array
else:
bound_array = copy.deepcopy(array)

parameter_dict = dict(zip(_get_parameters(bound_array), parameter_values))
for i, a in enumerate(bound_array):
if isinstance(a, ParameterExpression):
for key in a.parameters & parameter_dict.keys():
a = a.assign(key, parameter_dict[key])
if not a.parameters:
a = complex(a)
array[i] = a
return array
bound_array[i] = a

return None if inplace else bound_array
7 changes: 5 additions & 2 deletions qiskit/circuit/library/pauli_evolution.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# This code is part of Qiskit.
#
# (C) Copyright IBM 2021.
# (C) Copyright IBM 2021, 2023.
#
# 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
Expand All @@ -12,6 +12,8 @@

"""A gate to implement time-evolution of operators."""

from __future__ import annotations

from typing import Union, Optional
import numpy as np

Expand Down Expand Up @@ -109,7 +111,6 @@ class docstring for an example.

num_qubits = operator[0].num_qubits if isinstance(operator, list) else operator.num_qubits
super().__init__(name="PauliEvolution", num_qubits=num_qubits, params=[time], label=label)

self.operator = operator
self.synthesis = synthesis

Expand Down Expand Up @@ -170,6 +171,8 @@ def _to_sparse_pauli_op(operator):

if any(np.iscomplex(sparse_pauli.coeffs)):
raise ValueError("Operator contains complex coefficients, which are not supported.")
if any(isinstance(coeff, ParameterExpression) for coeff in sparse_pauli.coeffs):
raise ValueError("Operator contains ParameterExpression, which are not supported.")

return sparse_pauli

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
features:
- |
Add support for time-dependent Hamiltonians in
:class:`~.time_evolvers.TrotterQRTE`.
- |
Add support for observable evaluations at every time-step in
:class:`~.time_evolvers.TrotterQRTE`.
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
"""Test evolver problem class."""
import unittest
from test.python.algorithms import QiskitAlgorithmsTestCase
from ddt import data, ddt, unpack
from ddt import data, ddt
from numpy.testing import assert_raises
from qiskit import QuantumCircuit
from qiskit.algorithms import TimeEvolutionProblem
Expand Down Expand Up @@ -80,13 +80,6 @@ def test_init_all(self, initial_state):
self.assertEqual(evo_problem.t_param, expected_t_param)
self.assertEqual(evo_problem.param_value_map, expected_param_value_dict)

@data([Y, -1, One], [Y, -1.2, One], [Y, 0, One])
@unpack
def test_init_errors(self, hamiltonian, time, initial_state):
"""Tests expected errors are thrown on invalid time argument."""
with assert_raises(ValueError):
_ = TimeEvolutionProblem(hamiltonian, time, initial_state)

def test_validate_params(self):
"""Tests expected errors are thrown on parameters mismatch."""
param_x = Parameter("x")
Expand Down
Loading

0 comments on commit 41e0c3a

Please sign in to comment.