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 34 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
26 changes: 26 additions & 0 deletions doc/releases/changelog-dev.md
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,32 @@
>>> qml.grad(qml.qinfo.purity(circuit, wires=[0]))(param)
-0.5
```
* New operation `Evolution` defines an exponential of an operator in the form $e^{ix\hat{G}}$, with a single
lillian542 marked this conversation as resolved.
Show resolved Hide resolved
trainable parameter, x. Limiting to a single trainable parameter allows the use of `qml.gradient.param_shift` to
find the gradient with respect to the parameter x.
[(#3375)](https://github.com/PennyLaneAI/pennylane/pull/3375)

This example circuit uses the `Evolution` operation to define $e^{-\frac{i}{2}\phi\hat{\sigma}_x}$ and finds a
gradient using parameter shift:

```python
dev = qml.device('default.qubit', wires=2)

@qml.qnode(dev, diff_method=qml.gradients.param_shift)
def circuit(phi):
Evolution(qml.PauliX(0), -.5 * phi)
return qml.expval(qml.PauliZ(0))
```

If we run this circuit, we will get the following output

```pycon
>>> phi = np.array(1.2)
>>> circuit(phi)
tensor(0.36235775, requires_grad=True)
>>> qml.grad(circuit)(phi)
-0.9320390495504149
```

<h3>Improvements</h3>

Expand Down
1 change: 1 addition & 0 deletions pennylane/devices/default_qubit.py
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,7 @@ class DefaultQubit(QubitDevice):
"SProd",
"Prod",
"Exp",
"Evolution",
}

def __init__(
Expand Down
2 changes: 1 addition & 1 deletion pennylane/operation.py
Original file line number Diff line number Diff line change
Expand Up @@ -1335,7 +1335,7 @@ def parameter_frequencies(self):
eigvals = qml.eigvals(gen)

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

raise ParameterFrequenciesUndefinedError(
f"Operation {self.name} does not have parameter frequencies defined, "
Expand Down
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
214 changes: 201 additions & 13 deletions pennylane/ops/op_math/exp.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,18 +14,22 @@
"""
This submodule defines the symbolic operation that stands for an exponential of an operator.
"""
from copy import copy
from warnings import warn
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,
Tensor,
)
from pennylane.wires import Wires
from pennylane.operation import Operation

from .symbolicop import SymbolicOp

Expand Down Expand Up @@ -81,7 +85,7 @@ def exp(op, coeff=1, id=None):
return Exp(op, coeff, id=id)


class Exp(SymbolicOp):
class Exp(SymbolicOp, Operation):
"""A symbolic operator representating the exponential of a operator.

Args:
Expand Down Expand Up @@ -127,15 +131,13 @@ class Exp(SymbolicOp):

"""

coeff = 1
"""The numerical coefficient of the operator in the exponent."""

control_wires = Wires([])

def __init__(self, base=None, coeff=1, do_queue=True, id=None):
self.coeff = coeff
super().__init__(base, do_queue=do_queue, id=id)
self._name = "Exp"
self._data = [[coeff], self.base.data]
self.grad_recipe = [None]

def __repr__(self):
return (
Expand All @@ -144,14 +146,35 @@ def __repr__(self):
else f"Exp({self.coeff} {self.base.name})"
)

# pylint: disable=attribute-defined-outside-init
def __copy__(self):
# this method needs to be overwritten because the base must be copied too.
copied_op = object.__new__(type(self))
# copied_op must maintain inheritance structure of self
# Relevant for symbolic ops that mix in operation-specific components.

for attr, value in vars(self).items():
if attr not in {"_hyperparameters"}:
setattr(copied_op, attr, value)

copied_op._hyperparameters = copy(self.hyperparameters)
copied_op.hyperparameters["base"] = copy(self.base)
copied_op._data = copy(self._data)

return copied_op

@property
def hash(self):
return hash((str(self.name), self.base.hash, str(self.coeff)))

@property
def data(self):
return [[self.coeff], self.base.data]
return self._data

@data.setter
def data(self, new_data):
self.coeff = new_data[0][0]
self.base.data = new_data[1]
lillian542 marked this conversation as resolved.
Show resolved Hide resolved
@property
def coeff(self):
"""The numerical coefficient of the operator in the exponent."""
return self.data[0][0]

@property
def num_params(self):
Expand Down Expand Up @@ -180,6 +203,35 @@ def has_decomposition(self):
)
return math.real(self.coeff) == 0 and qml.pauli.is_pauli_word(self.base)

@property
def inverse(self):
"""Setting inverse is not defined for Exp, so the inverse is always False"""
return False

@inverse.setter
def inverse(self, boolean):
raise NotImplementedError(
f"Setting the inverse of {type(self)} is not implemented. "
f"Use qml.adjoint or qml.pow instead."
)

def inv(self):
"""Inverts the operator.

This method concatenates a string to the name of the operation,
to indicate that the inverse will be used for computations.

Any subsequent call of this method will toggle between the original
operation and the inverse of the operation.

Returns:
:class:`Operator`: operation to be inverted
"""
raise NotImplementedError(
f"Setting the inverse of {type(self)} is not implemented. "
f"Use qml.adjoint or qml.pow instead."
)
Jaybsoni 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 @@ -276,6 +328,142 @@ def pow(self, z):
return Exp(self.base, self.coeff * z)

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)
new_base = self.base.simplify()
if isinstance(new_base, qml.ops.op_math.SProd): # pylint: disable=no-member
return Exp(new_base.base, self.coeff * new_base.scalar)
return Exp(new_base, 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 self.base.is_hermitian and not np.real(self.coeff):
return self.base

raise GeneratorUndefinedError(
f"Exponential with coefficient {self.coeff} and base operator {self.base} does not appear to have a "
f"generator. Consider using op.simplify() to simplify before finding the generator, or define the operator "
f"in the form exp(ixG) through the Evolution class."
)


class Evolution(Exp):
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.
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 :math:`e^{ix\hat{G}}`,
where x is real.

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

lillian542 marked this conversation as resolved.
Show resolved Hide resolved
In contrast to the general :class:`~.Exp` class, the Evolution operator :math:`e^{ix\hat{G}}` is constrained to have a single trainable
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 :class:`~.Exp` class will be incompatible with a variety of PennyLane functions that require only a single
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._data = [param]

@property
def param(self):
"""A real coefficient with ``1j`` factored out."""
return self.data[0]

@property
def coeff(self):
return 1j * self.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)})"

def simplify(self):
new_base = self.base.simplify()
if isinstance(new_base, qml.ops.op_math.SProd): # pylint: disable=no-member
return Evolution(new_base.base, self.param * new_base.scalar)
return Evolution(new_base, self.param)

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 not self.base.is_hermitian:
warn(f"The base {self.base} may not be hermitian.")
if np.real(self.coeff):
raise GeneratorUndefinedError(
f"The operator coefficient {self.coeff} is not imaginary; the expected format is exp(ixG)."
f"The generator is not defined."
)
return self.base
1 change: 1 addition & 0 deletions pennylane/ops/qubit/hamiltonian.py
Original file line number Diff line number Diff line change
Expand Up @@ -393,6 +393,7 @@ def simplify(self):
self._wires = qml.wires.Wires.all_wires([op.wires for op in self.ops], sort=True)
# reset grouping, since the indices refer to the old observables and coefficients
self._grouping_indices = None
return self
lillian542 marked this conversation as resolved.
Show resolved Hide resolved

def __str__(self):
def wires_print(ob: Observable):
Expand Down
Loading