Skip to content

Commit

Permalink
Evolution operator (#3375)
Browse files Browse the repository at this point in the history
* add generator fn for Exp

* add parameter_frequencies fn

* add docstring to generator fn

* add Evolution operator

* remove commented-out line

* tests

* update docstring

* replace parameter property with data

* docstring edits

* label

* update tests

* make data setter consistent with Exp

* Update args in docstring

Co-authored-by: Jay Soni <jbsoni@uwaterloo.ca>

* Add Evolution to list of symbolic classes

* Inherit parameter_frequencies from Operation

* Overwrite inverse methods inherited from Operation

* Apply docstring suggestions from code review

Co-authored-by: Utkarsh <utkarshazad98@gmail.com>

* Check if simplified op has generator

* Add test for simplifying to find generator

* test setting inverse raises error

* Get parameter shift gradients with the new `Evolution` operator (#3472)

* get parameter shift working with evolution operator

* Fix bug in Exp.hash

* Update tests

* Coeff to string in hash

* Update test

Co-authored-by: Lillian Frederiksen <lillian542@gmail.com>

* Update simplify function

* Update tests

* Add to default qubit supported operations

* update changelog

* Apply formatting and doc suggestions from code review

Co-authored-by: Jay Soni <jbsoni@uwaterloo.ca>

* Update tests

* Update generator function

* Remove test for simplifying to get generator

* Update doc/releases/changelog-dev.md

Co-authored-by: Utkarsh <utkarshazad98@gmail.com>

* Update docstring

* codecov

Co-authored-by: Jay Soni <jbsoni@uwaterloo.ca>
Co-authored-by: Utkarsh <utkarshazad98@gmail.com>
Co-authored-by: Christina Lee <christina@xanadu.ai>
  • Loading branch information
4 people authored Dec 9, 2022
1 parent 2162274 commit f10ad1c
Show file tree
Hide file tree
Showing 8 changed files with 458 additions and 26 deletions.
26 changes: 26 additions & 0 deletions doc/releases/changelog-dev.md
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,32 @@
>>> qml.grad(qml.qinfo.purity(circuit, wires=[0]))(param)
-0.5
```
* New operation `Evolution` defines the exponential of an operator $\hat{O}$ of the form $e^{ix\hat{O}}$, with a single
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)]

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

from .prod import prod, Prod

Expand Down
208 changes: 195 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]
@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."
)

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,136 @@ 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):
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
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**
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)
>>> 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)])
>>> t = 10e-6
>>> U = Evolution(H, -1 * t)
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]

@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

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

0 comments on commit f10ad1c

Please sign in to comment.