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

Evolution operator #3375

Merged
merged 40 commits into from
Dec 9, 2022
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
becbe1b
add generator fn for Exp
lillian542 Nov 7, 2022
820b7b6
add parameter_frequencies fn
lillian542 Nov 7, 2022
1d13458
add docstring to generator fn
lillian542 Nov 7, 2022
439b771
add Evolution operator
lillian542 Nov 16, 2022
ac5b9e8
remove commented-out line
lillian542 Nov 16, 2022
3e462c2
tests
lillian542 Nov 16, 2022
4b1680b
update docstring
lillian542 Nov 16, 2022
7fdd0ad
Merge branch 'master' into evolution_operator
lillian542 Nov 16, 2022
4352d8e
replace parameter property with data
lillian542 Nov 23, 2022
37ef340
docstring edits
lillian542 Nov 23, 2022
38b9bb4
label
lillian542 Nov 23, 2022
0f185bf
update tests
lillian542 Nov 23, 2022
06d17b6
make data setter consistent with Exp
lillian542 Nov 23, 2022
ed62419
Update args in docstring
lillian542 Nov 24, 2022
800ef0a
Add Evolution to list of symbolic classes
lillian542 Nov 25, 2022
b839f9d
Inherit parameter_frequencies from Operation
lillian542 Nov 25, 2022
c4ca701
Overwrite inverse methods inherited from Operation
lillian542 Nov 25, 2022
fef8b48
Apply docstring suggestions from code review
lillian542 Nov 25, 2022
24dbd8c
Check if simplified op has generator
lillian542 Nov 25, 2022
3e1116c
Add test for simplifying to find generator
lillian542 Nov 25, 2022
7f9a66e
test setting inverse raises error
lillian542 Nov 28, 2022
650aa28
Merge branch 'master' into evolution_operator
Jaybsoni Dec 1, 2022
f7268cf
Merge branch 'master' into evolution_operator
Jaybsoni Dec 5, 2022
567cb67
Get parameter shift gradients with the new `Evolution` operator (#3472)
albi3ro Dec 6, 2022
03c622b
Update simplify function
lillian542 Dec 7, 2022
991d66b
Update tests
lillian542 Dec 7, 2022
4ec41c3
Add to default qubit supported operations
lillian542 Dec 8, 2022
5e39ae1
update changelog
lillian542 Dec 8, 2022
0ce322f
Apply formatting and doc suggestions from code review
lillian542 Dec 9, 2022
ce1ed34
Merge branch 'master' into evolution_operator
Jaybsoni Dec 9, 2022
ecd8f51
Update tests
lillian542 Dec 9, 2022
298b83d
Merge branch 'master' into evolution_operator
Jaybsoni Dec 9, 2022
053699e
Update generator function
lillian542 Dec 9, 2022
e039a4a
Remove test for simplifying to get generator
lillian542 Dec 9, 2022
d5adb57
Merge branch 'master' into evolution_operator
obliviateandsurrender Dec 9, 2022
a84ca4f
Update doc/releases/changelog-dev.md
lillian542 Dec 9, 2022
f87749b
Update docstring
lillian542 Dec 9, 2022
8be06e4
codecov
lillian542 Dec 9, 2022
768fa07
Merge branch 'master' into evolution_operator
Jaybsoni Dec 9, 2022
a140b1f
Merge branch 'master' into evolution_operator
lillian542 Dec 9, 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
3 changes: 2 additions & 1 deletion pennylane/ops/op_math/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
~CompositeOp
~Controlled
~ControlledOp
~Evolution
~Exp
~Pow
~Prod
Expand All @@ -54,7 +55,7 @@
from .adjoint_class import Adjoint
from .adjoint_constructor import adjoint
from .controlled_class import Controlled, ControlledOp
from .exp import exp, Exp
from .exp import exp, Exp, Evolution
lillian542 marked this conversation as resolved.
Show resolved Hide resolved

from .prod import prod, Prod

Expand Down
169 changes: 168 additions & 1 deletion pennylane/ops/op_math/exp.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,18 @@
"""
This submodule defines the symbolic operation that stands for an exponential of an operator.
"""
from warnings import warn
from warnings import warn, catch_warnings, filterwarnings
from scipy.sparse.linalg import expm as sparse_expm
import numpy as np

import pennylane as qml
from pennylane import math
from pennylane.operation import (
DecompositionUndefinedError,
expand_matrix,
OperatorPropertyUndefined,
GeneratorUndefinedError,
ParameterFrequenciesUndefinedError,
Tensor,
)
from pennylane.pauli import is_pauli_word, pauli_word_to_string
Expand Down Expand Up @@ -181,6 +184,11 @@ def has_decomposition(self):
)
return math.real(self.coeff) == 0 and is_pauli_word(self.base)

@property
def inverse(self):
"""Setting inverse not defined for Exp, so inverse is always False"""
lillian542 marked this conversation as resolved.
Show resolved Hide resolved
return False
lillian542 marked this conversation as resolved.
Show resolved Hide resolved

def decomposition(self):
r"""Representation of the operator as a product of other operators. Decomposes into
:class:`~.PauliRot` if the coefficient is imaginary and the base is a Pauli Word.
Expand Down Expand Up @@ -280,3 +288,162 @@ def simplify(self):
if isinstance(self.base, qml.ops.op_math.SProd): # pylint: disable=no-member
return Exp(self.base.base.simplify(), self.coeff * self.base.scalar)
return Exp(self.base.simplify(), self.coeff)

def generator(self):
r"""Generator of an operator that is in single-parameter-form.

For example, for operator

.. math::

U(\phi) = e^{i\phi (0.5 Y + Z\otimes X)}

we get the generator

>>> U.generator()
(0.5) [Y0]
+ (1.0) [Z0 X1]

"""
if np.real(self.coeff) != 0 or not self.base.is_hermitian:
raise GeneratorUndefinedError(
f"Exponential with coefficient {self.coeff} and base operator {self.base} does not have a generator."
)
lillian542 marked this conversation as resolved.
Show resolved Hide resolved
return self.base

@property
lillian542 marked this conversation as resolved.
Show resolved Hide resolved
def parameter_frequencies(self):
r"""Returns the frequencies for each operator parameter with respect
to an expectation value of the form
:math:`\langle \psi | U(\mathbf{p})^\dagger \hat{O} U(\mathbf{p})|\psi\rangle`.

These frequencies encode the behaviour of the operator :math:`U(\mathbf{p})`
lillian542 marked this conversation as resolved.
Show resolved Hide resolved
on the value of the expectation value as the parameters are modified.
For more details, please see the :mod:`.pennylane.fourier` module.

Returns:
list[tuple[int or float]]: Tuple of frequencies for each parameter.
Note that only non-negative frequency values are returned.

**Example**

>>> op = qml.PauliX(0)
>>> U = qml.exp(op, 1j)
>>> U.parameter_frequencies
[(2,)]

For operators that define a generator, the parameter frequencies are directly
related to the eigenvalues of the generator:

>>> gen = qml.generator(U, format="observable")
>>> gen
PauliX(wires=[0])
>>> gen_eigvals = qml.eigvals(gen)
>>> qml.gradients.eigvals_to_frequencies(tuple(gen_eigvals))
(2,)

For more details on this relationship, see :func:`.eigvals_to_frequencies`.
"""
if self.num_params == 1:
lillian542 marked this conversation as resolved.
Show resolved Hide resolved
# if the operator has a single parameter, we can query the
# generator, and if defined, use its eigenvalues.
try:
gen = qml.generator(self, format="observable")
except GeneratorUndefinedError as e:
raise ParameterFrequenciesUndefinedError(
f"Operation {self.name} does not have parameter frequencies defined."
lillian542 marked this conversation as resolved.
Show resolved Hide resolved
) from e

with catch_warnings():
filterwarnings(
action="ignore", message=r".+ eigenvalues will be computed numerically\."
)
eigvals = qml.eigvals(gen)
lillian542 marked this conversation as resolved.
Show resolved Hide resolved

eigvals = tuple(np.round(eigvals, 8))
lillian542 marked this conversation as resolved.
Show resolved Hide resolved
return [qml.gradients.eigvals_to_frequencies(eigvals)]

raise ParameterFrequenciesUndefinedError(
f"Operation {self.name} does not have parameter frequencies defined, "
"and parameter frequencies can not be computed as no generator is defined."
)


class Evolution(Exp):
lillian542 marked this conversation as resolved.
Show resolved Hide resolved

lillian542 marked this conversation as resolved.
Show resolved Hide resolved
r"""Create an exponential operator that defines a generator, of the form :math:`e^{ix\hat{G}}`

Args:
base (~.operation.Operator): The Operator to be used as a Generator, G.
lillian542 marked this conversation as resolved.
Show resolved Hide resolved
param (float): The evolution parameter, x. This parameter is not expected to have
lillian542 marked this conversation as resolved.
Show resolved Hide resolved
any complex component.

Returns:
:class:`Evolution`: A :class`~.operation.Operator` representing an operator exponential of the form exp(ixG),
lillian542 marked this conversation as resolved.
Show resolved Hide resolved
where x is real.

**Use Details**
lillian542 marked this conversation as resolved.
Show resolved Hide resolved

In contrast to the general Exp class, the Evolution operator is constrained to a single trainable
lillian542 marked this conversation as resolved.
Show resolved Hide resolved
parameter, x. Any parameters contained in the base operator are not trainable. This allows the operator
to be differentiated with regard to the evolution parameter. Defining a mathematically identical operator
using the Exp class will be incompatible with a variety of PennyLane functions that require only a single
lillian542 marked this conversation as resolved.
Show resolved Hide resolved
trainable parameter.

**Example**
This symbolic operator can be used to make general rotation operators:
>>> theta = np.array(1.23)
lillian542 marked this conversation as resolved.
Show resolved Hide resolved
>>> op = Evolution(qml.PauliX(0), -0.5 * theta)
>>> qml.math.allclose(op.matrix(), qml.RX(theta, wires=0).matrix())
True

Or to define a time evolution operator for a time-independent Hamiltonian:
>>> H = qml.Hamiltonian([1, 1], [qml.PauliY(0), qml.PauliX(1)])
lillian542 marked this conversation as resolved.
Show resolved Hide resolved
>>> t = 10e-6
>>> U = Evolution(H, -1 * t)

Even for more complicated generators, this operator is defined to have a single parameter,
allowing it to be differentiated with respect to that parameter (base operator parameters are
treated as constants):
>>> base_op = 0.5 * qml.PauliY(0) + qml.PauliZ(0) @ qml.PauliX(1)
lillian542 marked this conversation as resolved.
Show resolved Hide resolved
>>> op = Evolution(base_op, 1.23)
>>> op.num_params
1

If the base operator is Hermitian, then the gate can be used in a circuit,
though it may not be supported by the device and may not be differentiable.

>>> @qml.qnode(qml.device('default.qubit', wires=1))
... def circuit(x):
... Evolution(qml.PauliX(0), -0.5 * x)
... return qml.expval(qml.PauliZ(0))
>>> print(qml.draw(circuit)(1.23))
0: ──Exp(-0.61j X)─┤ <Z>

"""

def __init__(self, generator, param, do_queue=True, id=None):
super().__init__(generator, coeff=1j * param, do_queue=do_queue, id=id)
self._name = "Evolution"
self.param = param

@property
def data(self):
return [self.param]

@data.setter
def data(self, new_data):
self.coeff = 1j * new_data[0]
self.param = new_data[0]

lillian542 marked this conversation as resolved.
Show resolved Hide resolved
@property
def num_params(self):
return 1

def label(self, decimals=None, base_label=None, cache=None):
param = (
self.data[0]
if decimals is None
else format(math.toarray(self.data[0]), f".{decimals}f")
)
return base_label or f"Exp({param}j {self.base.label(decimals=decimals, cache=cache)})"
128 changes: 126 additions & 2 deletions tests/ops/op_math/test_exp.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,12 @@

import pennylane as qml
from pennylane import numpy as np
from pennylane.operation import DecompositionUndefinedError
from pennylane.ops.op_math import Exp
from pennylane.operation import (
DecompositionUndefinedError,
GeneratorUndefinedError,
ParameterFrequenciesUndefinedError,
)
from pennylane.ops.op_math import Exp, Evolution


@pytest.mark.parametrize("constructor", (qml.exp, Exp))
Expand Down Expand Up @@ -577,3 +581,123 @@ def test_draw_integration(self):
qml.drawer.tape_text(tape)

assert "0: ──Exp─┤ "


class TestDifferentiation:
Jaybsoni marked this conversation as resolved.
Show resolved Hide resolved
"""Test generator and parameter_frequency for differentiation"""

def test_base_not_hermitian_generator_undefined(self):
"""That that imaginary coefficient but non-Hermitian base operator raises GeneratorUndefinedError"""
op = Exp(qml.RX(1.23, 0), 1j)
with pytest.raises(GeneratorUndefinedError):
op.generator()

def test_real_component_coefficient_generator_undefined(self):
"""Test that Hermitian base operator but real coefficient raises GeneratorUndefinedError"""
op = Exp(qml.PauliX(0), 1)
with pytest.raises(GeneratorUndefinedError):
op.generator()

def test_generator_is_base_operator(self):
"""Test that generator is base operator"""
base_op = qml.PauliX(0)
op = Exp(base_op, 1j)
assert op.base == op.generator()

def test_parameter_frequencies(self):
lillian542 marked this conversation as resolved.
Show resolved Hide resolved
"""Test parameter_frequencies property"""
op = Exp(qml.PauliZ(1), 1j)
assert op.parameter_frequencies == [(2,)]

def test_parameter_frequencies_raises_error(self):
"""Test that parameter_frequencies raises an error if the op.generator() is undefined"""
op = Exp(qml.PauliX(0), 1)
with pytest.raises(GeneratorUndefinedError):
op.generator()
with pytest.raises(ParameterFrequenciesUndefinedError):
op.parameter_frequencies

def test_parameter_frequency_with_parameters_in_base_operator(self):
"""Test that parameter_frequency raises an error for the Exp class, but not the
Evolution class, if there are additional parameters in the base operator"""

base_op = 2 * qml.PauliX(0)
op1 = Exp(base_op, 1j)
op2 = Evolution(base_op, 1)

with pytest.raises(ParameterFrequenciesUndefinedError):
op1.parameter_frequencies()

assert op2.parameter_frequencies == [(4.0,)]


class TestEvolution:
"""Test Evolution(Exp) class that takes a parameter x and a generator G and defines an evolution exp(ixG)"""

def test_initialization(self):
"""Test initialization with a provided coefficient and a Tensor base."""
base = qml.PauliZ("b") @ qml.PauliZ("c")
param = 1.23

op = Evolution(base, param)

assert op.base is base
assert op.coeff == 1j * param
assert op.name == "Evolution"
assert isinstance(op, Exp)

assert op.num_params == 1
assert op.parameters == [param]
assert op.data == [param]

assert op.wires == qml.wires.Wires(("b", "c"))

def test_evolution_matches_corresponding_exp(self):
base_op = 2 * qml.PauliX(0)
op1 = Exp(base_op, 1j)
op2 = Evolution(base_op, 1)

assert np.all(op1.matrix() == op2.matrix())

def test_generator(self):
U = Evolution(qml.PauliX(0), 3)
assert U.base == U.generator()

def test_num_params_for_parametric_base(self):
base_op = 0.5 * qml.PauliY(0) + qml.PauliZ(0) @ qml.PauliX(1)
op = Evolution(base_op, 1.23)

assert base_op.num_params == 2
assert op.num_params == 1

def test_data(self):
"""Test accessing and setting the data property."""

param = np.array(1.234)

base = qml.PauliX(0)
op = Evolution(base, param)

assert op.data == [param]

new_data = [2.345]
op.data = new_data

assert op.data == new_data
assert op.coeff == 1j * op.data[0]
assert op.param == op.data[0]

@pytest.mark.parametrize(
"op,decimals,expected",
[
(Evolution(qml.PauliZ(0), 2), None, "Exp(2j Z)"),
(Evolution(qml.PauliZ(0), 2), 2, "Exp(2.00j Z)"),
(Evolution(qml.prod(qml.PauliZ(0), qml.PauliY(1)), 2), None, "Exp(2j Z@Y)"),
(Evolution(qml.prod(qml.PauliZ(0), qml.PauliY(1)), 2), 2, "Exp(2.00j Z@Y)"),
(Evolution(qml.RZ(1.234, wires=[0]), 5.678), None, "Exp(5.678j RZ)"),
(Evolution(qml.RZ(1.234, wires=[0]), 5.678), 2, "Exp(5.68j RZ\n(1.23))"),
],
)
def test_label(self, op, decimals, expected):
"""Test that the label is informative and uses decimals."""
assert op.label(decimals=decimals) == expected