Repository: PennyLaneAI/pennylane
Pull request #4995 (2024-01-04 21:54:18)

Update ControlledSequence decomposition
Recreation of PR #4955 , because the merge conflicts with the RC branch were a mess

**Context:**
The current decomposition of ControlledSequence stops at Power operations (with non-trainable powers). These are rejected by many ML interfaces.

**Description of the Change:**

- Added `lazy=False` when creating the power operators.
- Also added a `control_wires` property, because it was returning an empty wires object, and that's confusing. It does mean we have both `control` and `control_wires` returning the same thing. If we don't want both, we could have control_wires raise an error saying it doesn't exist, but it's strange for it to exist and return empty Wires.

**Benefits:**

- `ControlledSequence` can be used with the ML interfaces.
- The people who come after me will not be confused, as I was, when they use `control_wires` and everything runs but weird things happen.

github-actions[bot]: Hello. You may have forgotten to update the changelog!
Please edit [`doc/releases/changelog-dev.md`](/PennyLaneAI/pennylane/blob/master/doc/releases/changelog-dev.md) with:
* A one-to-two sentence description of the change. You may include a small working example for new features.
* A link back to this PR.
* Your name (or GitHub username) in the contributors section.
codecov[bot]: ## [Codecov](https://app.codecov.io/gh/PennyLaneAI/pennylane/pull/4995?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) Report
All modified and coverable lines are covered by tests :white_check_mark:
> :exclamation: No coverage uploaded for pull request base (`v0.34.0-rc0@573be94`). [Click here to learn what that means](https://docs.codecov.io/docs/error-reference?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI#section-missing-base-commit).


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


```diff
@@              Coverage Diff               @@
##             v0.34.0-rc0    #4995   +/-   ##
==============================================
  Coverage               ?   99.66%           
==============================================
  Files                  ?      392           
  Lines                  ?    35271           
  Branches               ?        0           
==============================================
  Hits                   ?    35154           
  Misses                 ?      117           
  Partials               ?        0           
```



</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/PennyLaneAI/pennylane/pull/4995?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).

lillian542: [sc-49877]

Patch:
diff --git a/doc/releases/changelog-0.34.0.md b/doc/releases/changelog-0.34.0.md
index a899b4fee..21f3af8b3 100644
--- a/doc/releases/changelog-0.34.0.md
+++ b/doc/releases/changelog-0.34.0.md
@@ -514,6 +514,10 @@
   (with potentially negative eigenvalues) onto the closest valid density matrix.
   [(#4959)](https://github.com/PennyLaneAI/pennylane/pull/4959)
 
+* The `ControlledSequence.compute_decomposition` default now decomposes the `Pow` operators, 
+  improving compatibility with machine learning interfaces. 
+  [(#4995)](https://github.com/PennyLaneAI/pennylane/pull/4995)
+
 <h3>Breaking changes 💔</h3>
 
 * The functions `qml.transforms.one_qubit_decomposition`, `qml.transforms.two_qubit_decomposition`, and
diff --git a/pennylane/templates/subroutines/controlled_sequence.py b/pennylane/templates/subroutines/controlled_sequence.py
index 27d6651d0..fcc7d8e63 100644
--- a/pennylane/templates/subroutines/controlled_sequence.py
+++ b/pennylane/templates/subroutines/controlled_sequence.py
@@ -104,6 +104,11 @@ class ControlledSequence(SymbolicOp, Operation):
         """The control wires for the sequence"""
         return self.hyperparameters["control_wires"]
 
+    @property
+    def control_wires(self):
+        """The control wires for the sequence"""
+        return self.hyperparameters["control_wires"]
+
     @property
     def wires(self):
         return self.control + self.base.wires
@@ -126,7 +131,7 @@ class ControlledSequence(SymbolicOp, Operation):
 
     # pylint:disable=arguments-differ
     @staticmethod
-    def compute_decomposition(*_, base=None, control_wires=None, **__):
+    def compute_decomposition(*_, base=None, control_wires=None, lazy=False, **__):
         r"""Representation of the operator as a product of other operators.
 
         .. math:: O = O_1 O_2 \dots O_n.
@@ -152,6 +157,24 @@ class ControlledSequence(SymbolicOp, Operation):
                 op.decomposition()
                 return qml.state()
 
+        >>> print(qml.draw(circuit, wire_order=[0,1,2,3])())
+        0: ─╭●─────────────────────────────────────┤  State
+        1: ─│────────────╭●────────────────────────┤  State
+        2: ─│────────────│────────────╭●───────────┤  State
+        3: ─╰(RX(1.00))──╰(RX(0.50))──╰(RX(0.25))──┤  State
+
+        To display the operators as powers of the base operator without further simplifcation,
+        the `compute_decompostion` method can be used with `lazy=True`.
+
+        .. code-block:: python
+
+            dev = qml.device("default.qubit")
+            op = qml.ControlledSequence(qml.RX(0.25, wires = 3), control = [0, 1, 2])
+
+            @qml.qnode(dev)
+            def circuit():
+                op.compute_decomposition(base=op.base, control_wires=op.control, lazy=True)
+                return qml.state()
 
         >>> print(qml.draw(circuit, wire_order=[0,1,2,3])())
         0: ─╭●─────────────────────────────────────┤  State
@@ -165,6 +188,6 @@ class ControlledSequence(SymbolicOp, Operation):
         ops = []
 
         for z, ctrl_wire in zip(powers_of_two[::-1], control_wires):
-            ops.append(qml.pow(qml.ctrl(base, control=ctrl_wire), z=z))
+            ops.append(qml.pow(qml.ctrl(base, control=ctrl_wire), z=z, lazy=lazy))
 
         return ops


Test patch:
diff --git a/tests/ops/functions/test_iterative_qpe.py b/tests/ops/functions/test_iterative_qpe.py
index 92b458852..a11fad161 100644
--- a/tests/ops/functions/test_iterative_qpe.py
+++ b/tests/ops/functions/test_iterative_qpe.py
@@ -120,7 +120,7 @@ class TestIQPE:
         phi = torch.tensor(1.0, requires_grad=True)
         assert torch.isclose(torch.func.grad(circuit)(phi), torch.func.grad(manual_circuit)(phi))
 
-    @pytest.mark.xfail(reason="See https://github.com/PennyLaneAI/pennylane/issues/4825")
+    @pytest.mark.xfail(reason="See https://github.com/PennyLaneAI/pennylane/issues/5002")
     @pytest.mark.tf
     def test_check_gradients_tf(self):
         """Test to check that the gradients are correct comparing with the expanded circuit using TensorFlow"""
diff --git a/tests/templates/test_subroutines/test_controlled_sequence.py b/tests/templates/test_subroutines/test_controlled_sequence.py
index 4355d85ba..1f556fde9 100644
--- a/tests/templates/test_subroutines/test_controlled_sequence.py
+++ b/tests/templates/test_subroutines/test_controlled_sequence.py
@@ -16,6 +16,8 @@ Unit tests for the ControlledSequence subroutine.
 """
 import pytest
 import numpy as np
+
+from pennylane import numpy as pnp
 import pennylane as qml
 
 from pennylane.wires import Wires
@@ -23,7 +25,6 @@ from pennylane.wires import Wires
 # pylint: disable=unidiomatic-typecheck, cell-var-from-loop
 
 
-@pytest.mark.xfail  # to be fixed by shortcut #49175
 def test_standard_validity():
     """Check the operation using the assert_valid function."""
     op = qml.ControlledSequence(qml.RX(0.25, wires=3), control=[0, 1, 2])
@@ -140,13 +141,13 @@ class TestMethods:
         assert new_op.base.wires == Wires(["a", "b"])
         assert new_op.control == Wires(["c", "d"])
 
-    def test_compute_decomposition(self):
-        """Test that the decomposition is as expected"""
+    def test_compute_decomposition_lazy(self):
+        """Test compute_decomposition with lazy=True"""
         base = qml.RZ(4.3, 1)
         control_wires = [0, 2, 3]
 
         decomp = qml.ControlledSequence.compute_decomposition(
-            base=base, control_wires=control_wires
+            base=base, control_wires=control_wires, lazy=True
         )
 
         assert len(decomp) == len(control_wires)
@@ -158,14 +159,28 @@ class TestMethods:
         for op, w in zip(decomp, control_wires):
             assert op.base.control_wires == Wires(w)
 
+    def test_compute_decomposition_not_lazy(self):
+        """Test compute_decomposition with lazy=False"""
+        op = qml.ControlledSequence(qml.RX(0.25, wires=3), control=["a", 1, "blue"])
+
+        decomp = op.compute_decomposition(base=op.base, control_wires=op.control, lazy=False)
+        expected_decomp = [
+            qml.ops.op_math.Controlled(qml.RX(0.25 * 4, wires=3), control_wires="a"),
+            qml.ops.op_math.Controlled(qml.RX(0.25 * 2, wires=3), control_wires=1),
+            qml.ops.op_math.Controlled(qml.RX(0.25 * 1, wires=3), control_wires="blue"),
+        ]
+
+        for op1, op2 in zip(decomp, expected_decomp):
+            assert op1 == op2
+
     def test_decomposition(self):
         op = qml.ControlledSequence(qml.RX(0.25, wires=3), control=["a", 1, "blue"])
 
         decomp = op.decomposition()
         expected_decomp = [
-            qml.ops.op_math.Controlled(qml.RX(0.25, wires=3), control_wires="a") ** 4,
-            qml.ops.op_math.Controlled(qml.RX(0.25, wires=3), control_wires=1) ** 2,
-            qml.ops.op_math.Controlled(qml.RX(0.25, wires=3), control_wires="blue") ** 1,
+            qml.ops.op_math.Controlled(qml.RX(0.25 * 4, wires=3), control_wires="a"),
+            qml.ops.op_math.Controlled(qml.RX(0.25 * 2, wires=3), control_wires=1),
+            qml.ops.op_math.Controlled(qml.RX(0.25 * 1, wires=3), control_wires="blue"),
         ]
 
         for op1, op2 in zip(decomp, expected_decomp):
@@ -311,7 +326,6 @@ class TestIntegration:
 
         _ = circuit()
 
-    @pytest.mark.xfail(reason="not working yet")
     def test_gradient_with_composite_op_base(self):
         """Test executing and getting the gradient of a circuit with a
         ControlledSequence based on a CompositeOp"""
@@ -325,10 +339,10 @@ class TestIntegration:
 
         @qml.qnode(dev)
         def circuit(thetas):
-            qml.ControlledSequence(U(thetas, wires=[0, 1]), control=[2, 3])
-            return qml.state()
+            qml.ControlledSequence(U(thetas), control=[2, 3])
+            return qml.expval(qml.PauliZ(0))
 
-        thetas = np.array([1.0, 1.0], requires_grad=True)
+        thetas = pnp.array([1.0, 1.0], requires_grad=True)
         _ = circuit(thetas)
         _ = qml.grad(circuit)(thetas)