Repository: PennyLaneAI/pennylane
Pull request #3375 (2022-11-16 21:51:57)

Evolution operator
**Context:**

- `Exp` class does not have `generator` method or `parameter_frequency` property defined, even though these could be defined for some instances of `Exp`.
- For instances of `Exp` where the base operator includes parameters (e.g. `0.5*qml.PauliX(0)`, or any instance of `qml.Hamiltonian`), the operator has `num_params > 1`, and standard functions for differentiation via parameter shift, such as `qml.generator(op)`, don't work.


**Description of the Change:**

- Add `generator` and `parameter_frequency` to `Exp` class in cases where it is defined (when the coefficient has no real component and the base operator is Hermitian)
- Add `Evolution` operator defined to be of the form exp(ixG) with a single, trainable parameter x. This allows differentiation _with regards to the parameter x_, even in instances where the base operator G contains additional parameters.


codecov[bot]: # [Codecov](https://codecov.io/gh/PennyLaneAI/pennylane/pull/3375?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) Report
> Merging [#3375](https://codecov.io/gh/PennyLaneAI/pennylane/pull/3375?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) (7fdd0ad) into [master](https://codecov.io/gh/PennyLaneAI/pennylane/commit/22b1d55276e5b8b1d5a3987371a38fc6fb15ac1f?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) (22b1d55) will **decrease** coverage by `0.00%`.
> The diff coverage is `97.05%`.

```diff
@@            Coverage Diff             @@
##           master    #3375      +/-   ##
==========================================
- Coverage   99.70%   99.69%   -0.01%     
==========================================
  Files         299      299              
  Lines       26185    26217      +32     
==========================================
+ Hits        26107    26138      +31     
- Misses         78       79       +1     
```


| [Impacted Files](https://codecov.io/gh/PennyLaneAI/pennylane/pull/3375?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) | Coverage Δ | |
|---|---|---|
| [pennylane/ops/op\_math/exp.py](https://codecov.io/gh/PennyLaneAI/pennylane/pull/3375/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI#diff-cGVubnlsYW5lL29wcy9vcF9tYXRoL2V4cC5weQ==) | `99.19% <96.96%> (-0.81%)` | :arrow_down: |
| [pennylane/ops/op\_math/\_\_init\_\_.py](https://codecov.io/gh/PennyLaneAI/pennylane/pull/3375/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI#diff-cGVubnlsYW5lL29wcy9vcF9tYXRoL19faW5pdF9fLnB5) | `100.00% <100.00%> (ø)` | |

Help us with your feedback. Take ten seconds to tell us [how you rate us](https://about.codecov.io/nps?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI). Have a feature suggestion? [Share it here.](https://app.codecov.io/gh/feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI)


Patch:
diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md
index 1320ba6cd..af65dfed8 100644
--- a/doc/releases/changelog-dev.md
+++ b/doc/releases/changelog-dev.md
@@ -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>
 
diff --git a/pennylane/devices/default_qubit.py b/pennylane/devices/default_qubit.py
index 619d9007e..42a7617dc 100644
--- a/pennylane/devices/default_qubit.py
+++ b/pennylane/devices/default_qubit.py
@@ -168,6 +168,7 @@ class DefaultQubit(QubitDevice):
         "SProd",
         "Prod",
         "Exp",
+        "Evolution",
     }
 
     def __init__(
diff --git a/pennylane/operation.py b/pennylane/operation.py
index acdcf7b07..e09279636 100644
--- a/pennylane/operation.py
+++ b/pennylane/operation.py
@@ -1335,7 +1335,7 @@ class Operation(Operator):
                 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, "
diff --git a/pennylane/ops/op_math/__init__.py b/pennylane/ops/op_math/__init__.py
index 4868704ec..595a2c34f 100644
--- a/pennylane/ops/op_math/__init__.py
+++ b/pennylane/ops/op_math/__init__.py
@@ -42,6 +42,7 @@ Symbolic Classes
     ~CompositeOp
     ~Controlled
     ~ControlledOp
+    ~Evolution
     ~Exp
     ~Pow
     ~Prod
@@ -54,7 +55,7 @@ Symbolic Classes
 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
 
diff --git a/pennylane/ops/op_math/exp.py b/pennylane/ops/op_math/exp.py
index 5ba333cfe..cd0cce765 100644
--- a/pennylane/ops/op_math/exp.py
+++ b/pennylane/ops/op_math/exp.py
@@ -14,8 +14,10 @@
 """
 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
@@ -23,9 +25,11 @@ from pennylane.operation import (
     DecompositionUndefinedError,
     expand_matrix,
     OperatorPropertyUndefined,
+    GeneratorUndefinedError,
     Tensor,
 )
 from pennylane.wires import Wires
+from pennylane.operation import Operation
 
 from .symbolicop import SymbolicOp
 
@@ -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:
@@ -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 (
@@ -144,14 +146,35 @@ class Exp(SymbolicOp):
             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):
@@ -180,6 +203,35 @@ class Exp(SymbolicOp):
                 )
         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.
@@ -276,6 +328,136 @@ class Exp(SymbolicOp):
         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
diff --git a/pennylane/ops/qubit/hamiltonian.py b/pennylane/ops/qubit/hamiltonian.py
index cf666e88b..c95a6544f 100644
--- a/pennylane/ops/qubit/hamiltonian.py
+++ b/pennylane/ops/qubit/hamiltonian.py
@@ -393,6 +393,7 @@ class Hamiltonian(Observable):
         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):


Test patch:
diff --git a/tests/ops/op_math/test_exp.py b/tests/ops/op_math/test_exp.py
index d321df8a4..8a8f17a31 100644
--- a/tests/ops/op_math/test_exp.py
+++ b/tests/ops/op_math/test_exp.py
@@ -17,8 +17,12 @@ from copy import copy
 
 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))
@@ -95,7 +99,7 @@ class TestProperties:
     """Test of the properties of the Exp class."""
 
     def test_data(self):
-        """Test accessing and setting the data property."""
+        """Test intializaing and accessing the data property."""
 
         phi = np.array(1.234)
         coeff = np.array(2.345)
@@ -105,13 +109,6 @@ class TestProperties:
 
         assert op.data == [[coeff], [phi]]
 
-        new_data = [[-2.1], [-3.4]]
-        op.data = new_data
-
-        assert op.data == new_data
-        assert op.coeff == -2.1
-        assert base.data == [-3.4]
-
     def test_queue_category_ops(self):
         """Test the _queue_category property."""
         assert Exp(qml.PauliX(0), -1.234j)._queue_category == "_ops"
@@ -128,6 +125,15 @@ class TestProperties:
 
         assert not Exp(qml.RX(1.2, wires=0)).is_hermitian
 
+    def test_setting_inverse_raises_error(self):
+        op = Exp(qml.PauliX(0), -1.234j)
+
+        with pytest.raises(NotImplementedError):
+            op.inverse = True
+
+        with pytest.raises(NotImplementedError):
+            op.inv()
+
 
 class TestMatrix:
     """Test the matrix method."""
@@ -399,6 +405,34 @@ class TestMiscMethods:
         assert qml.equal(new_op.base, qml.PauliX(0))
         assert new_op.coeff == 0.2
 
+    def test_simplify_s_prod(self):
+        """Tests that when simplification of the base results in an SProd,
+        the scalar is included in the coeff rather than the base"""
+        base = qml.s_prod(2, qml.op_sum(qml.PauliX(0), qml.PauliX(0)))
+        op = Exp(base, 3)
+        new_op = op.simplify()
+
+        assert qml.equal(new_op.base, qml.PauliX(0))
+        assert new_op.coeff == 12
+        assert new_op is not op
+
+    def test_copy(self):
+        """Tests making a copy."""
+        op = Exp(qml.CNOT([0, 1]), 2)
+        copied_op = op.__copy__()
+
+        assert qml.equal(op.base, copied_op.base)
+        assert op.data == copied_op.data
+        assert op.hyperparameters.keys() == copied_op.hyperparameters.keys()
+
+        for attr, value in vars(copied_op).items():
+            if (
+                not attr == "_hyperparameters"
+            ):  # hyperparameters contains base, which can't be compared via ==
+                assert vars(op)[attr] == value
+
+        assert len(vars(op).items()) == len(vars(copied_op).items())
+
 
 class TestIntegration:
     """Test Exp with gradients in qnodes."""
@@ -482,6 +516,24 @@ class TestIntegration:
         grad = qml.grad(circuit)(phi)
         assert qml.math.allclose(grad, -qml.numpy.sin(phi))
 
+    @pytest.mark.autograd
+    def test_autograd_param_shift_qnode(self):
+        """Test execution and gradient with pennylane numpy array."""
+        phi = qml.numpy.array(1.2)
+
+        dev = qml.device("default.qubit", wires=1)
+
+        @qml.qnode(dev, gradient_fn=qml.gradients.param_shift)
+        def circuit(phi):
+            Exp(qml.PauliX(0), -0.5j * phi)
+            return qml.expval(qml.PauliZ(0))
+
+        res = circuit(phi)
+        assert qml.math.allclose(res, qml.numpy.cos(phi))
+
+        grad = qml.grad(circuit)(phi)
+        assert qml.math.allclose(grad, -qml.numpy.sin(phi))
+
     @pytest.mark.autograd
     def test_autograd_measurement(self):
         """Test exp in a measurement with gradient and autograd."""
@@ -578,3 +630,172 @@ class TestIntegration:
         qml.drawer.tape_text(tape)
 
         assert "0: ──Exp─┤  "
+
+
+class TestDifferentiation:
+    """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):
+        """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 initializing and accessing the data property."""
+
+        param = np.array(1.234)
+
+        base = qml.PauliX(0)
+        op = Evolution(base, param)
+
+        assert op.data == [param]
+        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
+
+    def test_simplify(self):
+        """Test that the simplify method simplifies the base."""
+        orig_base = qml.adjoint(qml.adjoint(qml.PauliX(0)))
+
+        op = Exp(orig_base, coeff=0.2)
+        new_op = op.simplify()
+        assert qml.equal(new_op.base, qml.PauliX(0))
+        assert new_op.coeff == 0.2
+
+    def test_simplify_s_prod(self):
+        """Tests that when simplification of the base results in an SProd,
+        the scalar is included in the coeff rather than the base"""
+        base = qml.s_prod(2, qml.op_sum(qml.PauliX(0), qml.PauliX(0)))
+        op = Evolution(base, 3)
+        new_op = op.simplify()
+
+        assert qml.equal(new_op.base, qml.PauliX(0))
+        assert new_op.coeff == 12j
+
+    @pytest.mark.jax
+    def test_parameter_shift_gradient_matches_jax(self):
+        import jax
+
+        dev = qml.device("default.qubit", wires=2)
+        base = qml.PauliX(0)
+        x = np.array(1.234)
+
+        @qml.qnode(dev, diff_method=qml.gradients.param_shift)
+        def circ_param_shift(x):
+            Evolution(base, -0.5 * x)
+            return qml.expval(qml.PauliZ(0))
+
+        @qml.qnode(qml.device("default.qubit.jax", wires=1), interface="jax")
+        def circ(x):
+            Evolution(qml.PauliX(0), -0.5 * x)
+            return qml.expval(qml.PauliZ(0))
+
+        grad_param_shift = qml.grad(circ_param_shift)(x)
+        grad = jax.grad(circ)(x)
+
+        assert qml.math.allclose(grad, grad_param_shift)
+
+    def test_generator_warns_if_not_hermitian(self):
+        base = qml.s_prod(1j, qml.Identity(0))
+        op = Evolution(base, 2)
+        with pytest.warns(UserWarning, match="may not be hermitian"):
+            op.generator()
+
+    def test_simplifying_Evolution_operator(self):
+        base = qml.PauliX(0) + qml.PauliX(1) + qml.PauliX(0)
+        op = Evolution(base, 2)
+
+        assert qml.equal(op.simplify(), Evolution(base.simplify(), 2))
diff --git a/tests/test_operation.py b/tests/test_operation.py
index 9e8190f50..a3d56c4a9 100644
--- a/tests/test_operation.py
+++ b/tests/test_operation.py
@@ -625,7 +625,7 @@ class TestOperationConstruction:
 
         x = 0.654
         op = DummyOp(x, wires=0)
-        assert op.parameter_frequencies == (0.4,)
+        assert op.parameter_frequencies == [(0.4,)]
 
     def test_frequencies_default_multi_param(self):
         """Test that an operation with default parameter frequencies and multiple