Repository: PennyLaneAI/pennylane
Pull request #3777 (2023-02-15 16:48:21)

Prioritize trotter decomposition when `num_steps` is not None.

codecov[bot]: # [Codecov](https://codecov.io/gh/PennyLaneAI/pennylane/pull/3777?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) Report
> Merging [#3777](https://codecov.io/gh/PennyLaneAI/pennylane/pull/3777?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) (0a0663b) into [master](https://codecov.io/gh/PennyLaneAI/pennylane/commit/d1aa8d74da777bf8feb4819a42d2169e00f369c2?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) (d1aa8d7) will **decrease** coverage by `0.01%`.
> The diff coverage is `100.00%`.

```diff
@@            Coverage Diff             @@
##           master    #3777      +/-   ##
==========================================
- Coverage   99.80%   99.80%   -0.01%     
==========================================
  Files         331      331              
  Lines       29102    29098       -4     
==========================================
- Hits        29045    29041       -4     
  Misses         57       57              
```


| [Impacted Files](https://codecov.io/gh/PennyLaneAI/pennylane/pull/3777?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/3777?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI#diff-cGVubnlsYW5lL29wcy9vcF9tYXRoL2V4cC5weQ==) | `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 1d3796af8..e3c2048c3 100644
--- a/doc/releases/changelog-dev.md
+++ b/doc/releases/changelog-dev.md
@@ -426,6 +426,7 @@
 * `Exp` operator now detects if the base is a generator, and decomposes to its corresponding operator.
   If not, it decomposes to a `PauliRot`, or it uses the Suzuki-Trotter decomposition when necessary.
   [(#3691)](https://github.com/PennyLaneAI/pennylane/pull/3691)
+  [(#3777)](https://github.com/PennyLaneAI/pennylane/pull/3777)
 
   If the `base` operator is a generator of another operator, `Exp` returns this operator during decomposition:
 
diff --git a/pennylane/ops/op_math/exp.py b/pennylane/ops/op_math/exp.py
index e989cb50a..f6bed6b13 100644
--- a/pennylane/ops/op_math/exp.py
+++ b/pennylane/ops/op_math/exp.py
@@ -224,9 +224,7 @@ class Exp(ScalarSymbolicOp, Operation):
 
     @property
     def _queue_category(self):
-        if self.base.is_hermitian and math.allequal(math.real(self.coeff), 0):
-            return "_ops"
-        return None
+        return "_ops"
 
     # pylint: disable=invalid-overridden-method, arguments-renamed
     @property
@@ -293,6 +291,13 @@ class Exp(ScalarSymbolicOp, Operation):
         if isinstance(base, SProd):
             return self._recursive_decomposition(base.base, base.scalar * coeff)
 
+        if self.num_steps is not None and isinstance(base, (Hamiltonian, Sum)):
+            # Apply trotter decomposition
+            coeffs = base.coeffs if isinstance(base, Hamiltonian) else [1] * len(base)
+            coeffs = [c * coeff for c in coeffs]
+            ops = base.ops if isinstance(base, Hamiltonian) else base.operands
+            return self._trotter_decomposition(ops, coeffs)
+
         for op_class in has_unitary_generator_types:
             # Check if the exponentiated operator is a generator of another operator
             if op_class.num_wires in {base.num_wires, AnyWires} and op_class is not qml.PauliRot:
@@ -308,14 +313,11 @@ class Exp(ScalarSymbolicOp, Operation):
             # Check if the exponential can be decomposed into a PauliRot gate
             return self._pauli_rot_decomposition(base, coeff)
 
-        if isinstance(base, (Hamiltonian, Sum)):
-            # Apply trotter decomposition
-            coeffs = base.coeffs if isinstance(base, Hamiltonian) else [1] * len(base)
-            coeffs = [c * coeff for c in coeffs]
-            ops = base.ops if isinstance(base, Hamiltonian) else base.operands
-            return self._trotter_decomposition(ops, coeffs)
-
-        raise DecompositionUndefinedError
+        raise DecompositionUndefinedError(
+            f"The decomposition of the {self} operator is not defined. "
+            "Please set a value to ``num_steps`` when instantiating the ``Exp`` operator "
+            "if a Suzuki-Trotter decomposition is required."
+        )
 
     @staticmethod
     def _pauli_rot_decomposition(base: Operator, coeff: complex):
@@ -350,13 +352,6 @@ class Exp(ScalarSymbolicOp, Operation):
         Returns:
             List[Operator]: a list of operators containing the decomposition
         """
-        if self.num_steps is None:
-            raise DecompositionUndefinedError(
-                "The number of steps is required to calculate the Suzuki-Trotter "
-                "decomposition of the exponential operator. Please specify a value "
-                "for ``num_steps`` when instantiating the operator."
-            )
-
         op_list = []
         for c, op in zip(coeffs, ops):
             c /= self.num_steps  # divide by trotter number


Test patch:
diff --git a/tests/ops/op_math/test_exp.py b/tests/ops/op_math/test_exp.py
index bb8a98125..01261cfdf 100644
--- a/tests/ops/op_math/test_exp.py
+++ b/tests/ops/op_math/test_exp.py
@@ -118,9 +118,9 @@ class TestProperties:
         """Test the _queue_category property."""
         assert Exp(qml.PauliX(0), -1.234j)._queue_category == "_ops"
 
-        assert Exp(qml.PauliX(0), 1 + 2j)._queue_category is None
+        assert Exp(qml.PauliX(0), 1 + 2j)._queue_category == "_ops"
 
-        assert Exp(qml.RX(1.2, 0), -1.2j)._queue_category is None
+        assert Exp(qml.RX(1.2, 0), -1.2j)._queue_category == "_ops"
 
     def test_is_hermitian(self):
         """Test that the op is hermitian if the base is hermitian and the coeff is real."""
@@ -448,6 +448,24 @@ class TestDecomposition:
         assert len(dec) == 1
         assert qml.equal(op, dec[0])
 
+    def test_trotter_is_used_if_num_steps_is_defined(self):
+        """Test that the Suzuki-Trotter decomposition is used when ``num_steps`` is defined."""
+        phi = 1.23
+        op = qml.IsingXY(phi, wires=[0, 1])
+        exp = qml.evolve(op.generator(), coeff=-phi, num_steps=3)
+        dec = exp.decomposition()
+        expected_decomp = [
+            qml.IsingXX(phi / 3, wires=[0, 1]),
+            qml.IsingYY(phi / 3, wires=[0, 1]),
+            qml.IsingXX(phi / 3, wires=[0, 1]),
+            qml.IsingYY(phi / 3, wires=[0, 1]),
+            qml.IsingXX(phi / 3, wires=[0, 1]),
+            qml.IsingYY(phi / 3, wires=[0, 1]),
+        ]
+        assert len(dec) == len(expected_decomp)
+        for op1, op2 in zip(dec, expected_decomp):
+            qml.equal(op1, op2)
+
     @pytest.mark.parametrize(
         ("time", "hamiltonian", "steps", "expected_queue"),
         [
@@ -505,7 +523,7 @@ class TestDecomposition:
         op = qml.evolve(qml.sum(qml.PauliX(0), qml.PauliY(1)))
         with pytest.raises(
             DecompositionUndefinedError,
-            match="The number of steps is required to calculate the Suzuki-Trotter",
+            match="Please set a value to ``num_steps`` when instantiating the ``Exp`` operator",
         ):
             op.decomposition()