Repository: PennyLaneAI/pennylane
Pull request #5073 (2024-01-17 01:34:48)

Fix TrotterProduct fail for a Hamiltonian with identical coefficients
**Context:**

Fixes this [bug](https://github.com/PennyLaneAI/pennylane/issues/5066). The input hamiltonian in `TrotterProduct` is internally converted to a `Sum`. But a Hamiltonian with identical coefficients (=! 1) created internally with `qml.dot(coeffs, ops)` has type `SProd` and gives an error. 

**Description of the Change:**
The following addition to `TrotterProduct` fixes the issue: 
```
if isinstance(hamiltonian, SProd):
    hamiltonian = hamiltonian.simplify()
```

**Benefits:**

**Possible Drawbacks:**

**Related GitHub Issues:**


codecov[bot]: ## [Codecov](https://app.codecov.io/gh/PennyLaneAI/pennylane/pull/5073?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) Report
Attention: `3 lines` in your changes are missing coverage. Please review.
> Comparison is base [(`c01cb4b`)](https://app.codecov.io/gh/PennyLaneAI/pennylane/commit/c01cb4b96f758a9f1a0c4e422115829b80e113cb?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) 99.67% compared to head [(`325157e`)](https://app.codecov.io/gh/PennyLaneAI/pennylane/pull/5073?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) 99.65%.
> Report is 20 commits behind head on master.

| [Files](https://app.codecov.io/gh/PennyLaneAI/pennylane/pull/5073?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) | Patch % | Lines |
|---|---|---|
| [pennylane/workflow/jacobian\_products.py](https://app.codecov.io/gh/PennyLaneAI/pennylane/pull/5073?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI#diff-cGVubnlsYW5lL3dvcmtmbG93L2phY29iaWFuX3Byb2R1Y3RzLnB5) | 91.30% | [2 Missing :warning: ](https://app.codecov.io/gh/PennyLaneAI/pennylane/pull/5073?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) |
| [pennylane/templates/subroutines/trotter.py](https://app.codecov.io/gh/PennyLaneAI/pennylane/pull/5073?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI#diff-cGVubnlsYW5lL3RlbXBsYXRlcy9zdWJyb3V0aW5lcy90cm90dGVyLnB5) | 75.00% | [1 Missing :warning: ](https://app.codecov.io/gh/PennyLaneAI/pennylane/pull/5073?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) |

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##           master    #5073      +/-   ##
==========================================
- Coverage   99.67%   99.65%   -0.02%     
==========================================
  Files         394      394              
  Lines       35670    35464     -206     
==========================================
- Hits        35554    35343     -211     
- Misses        116      121       +5     
```



</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/PennyLaneAI/pennylane/pull/5073?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI).

soranjh: [sc-54033]

Patch:
diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md
index 6c588b906..63b707b09 100644
--- a/doc/releases/changelog-dev.md
+++ b/doc/releases/changelog-dev.md
@@ -302,6 +302,9 @@
 
 <h3>Bug fixes 🐛</h3>
 
+* The `qml.TrotterProduct` template is updated to accept `SProd` as input Hamiltonian.
+  [(#5073)](https://github.com/PennyLaneAI/pennylane/pull/5073)
+
 * Fixed a bug where caching together with JIT compilation and broadcasted tapes yielded wrong results
   `Operator.hash` now depends on the memory location, `id`, of a Jax tracer instead of its string representation.
   [(#3917)](https://github.com/PennyLaneAI/pennylane/pull/3917)
diff --git a/pennylane/templates/subroutines/trotter.py b/pennylane/templates/subroutines/trotter.py
index a1244a581..6609361f0 100644
--- a/pennylane/templates/subroutines/trotter.py
+++ b/pennylane/templates/subroutines/trotter.py
@@ -17,6 +17,7 @@ Contains templates for Suzuki-Trotter approximation based subroutines.
 import pennylane as qml
 from pennylane.operation import Operation
 from pennylane.ops import Sum
+from pennylane.ops.op_math import SProd
 
 
 def _scalar(order):
@@ -165,18 +166,25 @@ class TrotterProduct(Operation):
                 f"The order of a TrotterProduct must be 1 or a positive even integer, got {order}."
             )
 
-        if isinstance(hamiltonian, qml.Hamiltonian):
+        if isinstance(hamiltonian, (qml.Hamiltonian)):
             coeffs, ops = hamiltonian.terms()
             if len(coeffs) < 2:
                 raise ValueError(
-                    "There should be atleast 2 terms in the Hamiltonian. Otherwise use `qml.exp`"
+                    "There should be at least 2 terms in the Hamiltonian. Otherwise use `qml.exp`"
                 )
 
             hamiltonian = qml.dot(coeffs, ops)
 
+        if isinstance(hamiltonian, SProd):
+            hamiltonian = hamiltonian.simplify()
+            if len(hamiltonian.terms()[0]) < 2:
+                raise ValueError(
+                    "There should be at least 2 terms in the Hamiltonian. Otherwise use `qml.exp`"
+                )
+
         if not isinstance(hamiltonian, Sum):
             raise TypeError(
-                f"The given operator must be a PennyLane ~.Hamiltonian or ~.Sum got {hamiltonian}"
+                f"The given operator must be a PennyLane ~.Hamiltonian, ~.Sum or ~.SProd, got {hamiltonian}"
             )
 
         if check_hermitian:


Test patch:
diff --git a/tests/templates/test_subroutines/test_trotter.py b/tests/templates/test_subroutines/test_trotter.py
index f46d94e85..fd49f7b01 100644
--- a/tests/templates/test_subroutines/test_trotter.py
+++ b/tests/templates/test_subroutines/test_trotter.py
@@ -33,6 +33,7 @@ test_hamiltonians = (
     qml.dot(
         [1, -0.5, 0.5], [qml.Identity(wires=[0, 1]), qml.PauliZ(0), qml.PauliZ(0)]
     ),  # H = Identity
+    qml.dot([2, 2, 2], [qml.PauliX(0), qml.PauliY(0), qml.PauliZ(1)]),
 )
 
 p_4 = (4 - 4 ** (1 / 3)) ** -1
@@ -161,6 +162,51 @@ test_decompositions = (
             qml.exp(qml.PauliZ(0), p_4 * -0.5 * 4.2j / 2),
             qml.exp(qml.Identity(wires=[0, 1]), p_4 * 4.2j / 2),
         ],
+        (3, 1): [
+            qml.exp(qml.PauliX(0), 8.4j),
+            qml.exp(qml.PauliY(0), 8.4j),
+            qml.exp(qml.PauliZ(1), 8.4j),
+        ],
+        (3, 2): [
+            qml.exp(qml.PauliX(0), 8.4j / 2),
+            qml.exp(qml.PauliY(0), 8.4j / 2),
+            qml.exp(qml.PauliZ(1), 8.4j / 2),
+            qml.exp(qml.PauliZ(1), 8.4j / 2),
+            qml.exp(qml.PauliY(0), 8.4j / 2),
+            qml.exp(qml.PauliX(0), 8.4j / 2),
+        ],
+        (3, 4): [
+            qml.exp(qml.PauliX(0), p_4 * 8.4j / 2),
+            qml.exp(qml.PauliY(0), p_4 * 8.4j / 2),
+            qml.exp(qml.PauliZ(1), p_4 * 8.4j / 2),
+            qml.exp(qml.PauliZ(1), p_4 * 8.4j / 2),
+            qml.exp(qml.PauliY(0), p_4 * 8.4j / 2),
+            qml.exp(qml.PauliX(0), p_4 * 8.4j / 2),
+            qml.exp(qml.PauliX(0), p_4 * 8.4j / 2),
+            qml.exp(qml.PauliY(0), p_4 * 8.4j / 2),
+            qml.exp(qml.PauliZ(1), p_4 * 8.4j / 2),
+            qml.exp(qml.PauliZ(1), p_4 * 8.4j / 2),
+            qml.exp(qml.PauliY(0), p_4 * 8.4j / 2),
+            qml.exp(qml.PauliX(0), p_4 * 8.4j / 2),  # S_2(p * t) ^ 2
+            qml.exp(qml.PauliX(0), (1 - 4 * p_4) * 8.4j / 2),
+            qml.exp(qml.PauliY(0), (1 - 4 * p_4) * 8.4j / 2),
+            qml.exp(qml.PauliZ(1), (1 - 4 * p_4) * 8.4j / 2),
+            qml.exp(qml.PauliZ(1), (1 - 4 * p_4) * 8.4j / 2),
+            qml.exp(qml.PauliY(0), (1 - 4 * p_4) * 8.4j / 2),
+            qml.exp(qml.PauliX(0), (1 - 4 * p_4) * 8.4j / 2),  # S_2((1 - 4p) * t)
+            qml.exp(qml.PauliX(0), p_4 * 8.4j / 2),
+            qml.exp(qml.PauliY(0), p_4 * 8.4j / 2),
+            qml.exp(qml.PauliZ(1), p_4 * 8.4j / 2),
+            qml.exp(qml.PauliZ(1), p_4 * 8.4j / 2),
+            qml.exp(qml.PauliY(0), p_4 * 8.4j / 2),
+            qml.exp(qml.PauliX(0), p_4 * 8.4j / 2),
+            qml.exp(qml.PauliX(0), p_4 * 8.4j / 2),
+            qml.exp(qml.PauliY(0), p_4 * 8.4j / 2),
+            qml.exp(qml.PauliZ(1), p_4 * 8.4j / 2),
+            qml.exp(qml.PauliZ(1), p_4 * 8.4j / 2),
+            qml.exp(qml.PauliY(0), p_4 * 8.4j / 2),
+            qml.exp(qml.PauliX(0), p_4 * 8.4j / 2),  # S_2(p * t) ^ 2
+        ],
     }
 )
 
@@ -218,7 +264,8 @@ class TestInitialization:
         """Test an error is raised of an incorrect type is passed"""
         if raise_error:
             with pytest.raises(
-                TypeError, match="The given operator must be a PennyLane ~.Hamiltonian or ~.Sum"
+                TypeError,
+                match="The given operator must be a PennyLane ~.Hamiltonian, ~.Sum or ~.SProd",
             ):
                 qml.TrotterProduct(hamiltonian, time=1.23)
 
@@ -250,10 +297,19 @@ class TestInitialization:
         except ValueError:
             assert False  # No error should be raised if the check_hermitian flag is disabled
 
-    def test_error_hamiltonian(self):
-        """Test that an error is raised if the input hamultonian has only 1 term."""
-        with pytest.raises(ValueError, match="There should be atleast 2 terms in the Hamiltonian."):
-            qml.TrotterProduct(qml.Hamiltonian([1.0], [qml.PauliX(0)]), 1.23, n=2, order=4)
+    @pytest.mark.parametrize(
+        "hamiltonian",
+        (
+            qml.Hamiltonian([1.0], [qml.PauliX(0)]),
+            qml.dot([2.0], [qml.PauliY(0)]),
+        ),
+    )
+    def test_error_hamiltonian(self, hamiltonian):
+        """Test that an error is raised if the input Hamiltonian has only 1 term."""
+        with pytest.raises(
+            ValueError, match="There should be at least 2 terms in the Hamiltonian."
+        ):
+            qml.TrotterProduct(hamiltonian, 1.23, n=2, order=4)
 
     @pytest.mark.parametrize("order", (-1, 0, 0.5, 3, 7.0))
     def test_error_order(self, order):
@@ -272,6 +328,9 @@ class TestInitialization:
         time, n, order = (4.2, 10, 4)
         op = qml.TrotterProduct(hamiltonian, time, n=n, order=order, check_hermitian=False)
 
+        if isinstance(hamiltonian, qml.ops.op_math.SProd):
+            hamiltonian = hamiltonian.simplify()
+
         assert op.wires == hamiltonian.wires
         assert op.parameters == [time]
         assert op.data == (time,)
@@ -303,6 +362,9 @@ class TestInitialization:
         time, n, order = (4.2, 10, 4)
         op = qml.TrotterProduct(hamiltonian, time, n=n, order=order)
 
+        if isinstance(hamiltonian, qml.ops.op_math.SProd):
+            hamiltonian = hamiltonian.simplify()
+
         data, metadata = op._flatten()
         assert qml.equal(data[0], hamiltonian)
         assert data[1] == time