Repository: PennyLaneAI/pennylane
Pull request #5242 (2024-02-21 15:59:55)

Support controlled ZYZ decomposition of composite operators
**Context:**

Attempting to run the preliminary example from the [QSVT in Practice](https://pennylane.ai/qml/demos/tutorial_apply_qsvt/#preliminaries) demo with the Lightning Qubit device

```python
import pennylane as qml
from pennylane import numpy as pnp

dev = qml.device("lightning.qubit", wires=[0, 1])

A = pnp.array([[0.1, 0.2], [0.3, 0.4]])
phase_angles = pnp.array([0.0, 1.0, 2.0, 3.0])


@qml.qnode(dev)
def my_circuit(phase_angles):
    qml.qsvt(A, phase_angles, wires=[0, 1])
    return qml.state()

print(my_circuit(phase_angles))
```

yields the error

```
  File "/Users/.../PennyLane-0.35.0.dev0-py3.11.egg/pennylane/ops/op_math/controlled_decompositions.py", line 188, in ctrl_decomp_zyz
    phi, theta, omega = target_operation.single_qubit_rot_angles()
                        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
AttributeError: 'Prod' object has no attribute 'single_qubit_rot_angles'
```

**Description of the Change:**

* Updated `ctrl_decomp_zyz()` to support `Operators` that are not `Operations`.
* Trimmed trailing whitespace from the changelog (done automatically by VS Code).

**Benefits:**

* The QSVT in Practice demo can be executed on the Lightning Qubit device.

**Possible Drawbacks:**

None.

**Related GitHub Issues:**

None.

codecov[bot]: ## [Codecov](https://app.codecov.io/gh/PennyLaneAI/pennylane/pull/5242?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:
> Comparison is base [(`f7bba2b`)](https://app.codecov.io/gh/PennyLaneAI/pennylane/commit/f7bba2b8a21319309b21250073492573f5ee967a?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) 99.68% compared to head [(`3b80855`)](https://app.codecov.io/gh/PennyLaneAI/pennylane/pull/5242?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) 99.68%.
> Report is 3 commits behind head on master.


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


```diff
@@            Coverage Diff             @@
##           master    #5242      +/-   ##
==========================================
- Coverage   99.68%   99.68%   -0.01%     
==========================================
  Files         396      399       +3     
  Lines       36660    36503     -157     
==========================================
- Hits        36545    36387     -158     
- Misses        115      116       +1     
```



</details>

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

Mandrenkov: Thanks for taking a look, @timmysilv!

> since we were already doing the try-except thing, do you know if it hurts to instead just update the `except` to catch `(NotImplementedError, AttributeError)`?

That also works! The main difference is that that doing so would also catch `AttributeErrors` inside the implementation of `single_qubit_rot_angles()` which might not be preferable. I opted for the `if-else` approach because:
1. It explicitly documents the assumption that the `Operator` is an `Operation` (in that branch).
2. In my opinion,`AttributeError` is a bit _too_ broad of an exception to catch. A lot of silly mistakes can get caught here which are better left exposed (and subsequently fixed).
Mandrenkov: Thanks for the review, @albi3ro!

Patch:
diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md
index 61d1b7f0b..3e9e5835a 100644
--- a/doc/releases/changelog-dev.md
+++ b/doc/releases/changelog-dev.md
@@ -271,6 +271,9 @@
     + 2j * X(0) @ Z(1)
     ```
 
+* Controlled composite operations can be decomposed using ZYZ rotations.
+  [(#5242)](https://github.com/PennyLaneAI/pennylane/pull/5242)
+
 * Composite operations (e.g., those made with `qml.prod` and `qml.sum`) and `SProd` operations
   convert `Hamiltonian` and `Tensor` operands to `Sum` and `Prod` types, respectively. This helps
   avoid the mixing of incompatible operator types.
@@ -630,6 +633,7 @@
 This release contains contributions from (in alphabetical order):
 
 Abhishek Abhishek,
+Mikhail Andrenkov,
 Utkarsh Azad,
 Trenten Babcock,
 Gabriel Bottrill,
diff --git a/pennylane/ops/op_math/controlled_decompositions.py b/pennylane/ops/op_math/controlled_decompositions.py
index e4be40297..e9b80e3fc 100644
--- a/pennylane/ops/op_math/controlled_decompositions.py
+++ b/pennylane/ops/op_math/controlled_decompositions.py
@@ -16,10 +16,11 @@ This submodule defines functions to decompose controlled operations
 """
 
 from copy import copy
+from typing import Tuple
 import numpy as np
 import numpy.linalg as npl
 import pennylane as qml
-from pennylane.operation import Operator
+from pennylane.operation import Operation, Operator
 from pennylane.wires import Wires
 from pennylane import math
 
@@ -184,12 +185,25 @@ def ctrl_decomp_zyz(target_operation: Operator, control_wires: Wires):
 
     target_wire = target_operation.wires
 
-    try:
-        phi, theta, omega = target_operation.single_qubit_rot_angles()
-    except NotImplementedError:
+    def get_single_qubit_rot_angles_via_matrix() -> Tuple[float, float, float]:
+        """Returns a triplet of angles representing the single-qubit decomposition
+        of the matrix of the target operation using ZYZ rotations.
+        """
         with qml.QueuingManager.stop_recording():
-            zyz_decomp = qml.ops.one_qubit_decomposition(qml.matrix(target_operation), target_wire)
-        phi, theta, omega = [gate.parameters[0] for gate in zyz_decomp]
+            zyz_decomp = qml.ops.one_qubit_decomposition(
+                qml.matrix(target_operation),
+                wire=target_wire,
+                rotations="ZYZ",
+            )
+        return tuple(gate.parameters[0] for gate in zyz_decomp)  # type: ignore
+
+    if isinstance(target_operation, Operation):
+        try:
+            phi, theta, omega = target_operation.single_qubit_rot_angles()
+        except NotImplementedError:
+            phi, theta, omega = get_single_qubit_rot_angles_via_matrix()
+    else:
+        phi, theta, omega = get_single_qubit_rot_angles_via_matrix()
 
     decomp = []
 


Test patch:
diff --git a/tests/ops/op_math/test_controlled_decompositions.py b/tests/ops/op_math/test_controlled_decompositions.py
index 46626d395..30381e8f8 100644
--- a/tests/ops/op_math/test_controlled_decompositions.py
+++ b/tests/ops/op_math/test_controlled_decompositions.py
@@ -241,6 +241,50 @@ class TestControlledDecompositionZYZ:
         assert len(decomp) == 5
         assert decomp == expected
 
+    @pytest.mark.parametrize(
+        "composite_op, want_decomp",
+        [
+            (
+                qml.ops.Prod(qml.PauliX(0), qml.PauliX(0)),  # type: ignore
+                [
+                    qml.CNOT(wires=[1, 0]),
+                    qml.CNOT(wires=[1, 0]),
+                ],
+            ),
+            (
+                qml.s_prod(1j, qml.PauliX(0)),
+                [
+                    qml.RZ(7 * np.pi / 2, wires=0),
+                    qml.RY(np.pi / 2, wires=0),
+                    qml.CNOT(wires=[1, 0]),
+                    qml.RY(-np.pi / 2, wires=0),
+                    qml.RZ(-2 * np.pi, wires=0),
+                    qml.CNOT(wires=[1, 0]),
+                    qml.RZ(-3 * np.pi / 2, wires=0),
+                ],
+            ),
+            (
+                (
+                    qml.s_prod(1 / np.sqrt(2), qml.PauliX(0))
+                    + qml.s_prod(1 / np.sqrt(2), qml.PauliX(0))
+                ),
+                [
+                    qml.RZ(np.pi / 2, wires=0),
+                    qml.RY(np.pi / 2, wires=0),
+                    qml.CNOT(wires=[1, 0]),
+                    qml.RY(-np.pi / 2, wires=0),
+                    qml.RZ(-2 * np.pi, wires=0),
+                    qml.CNOT(wires=[1, 0]),
+                    qml.RZ(3 * np.pi / 2, wires=0),
+                ],
+            ),
+        ],
+    )
+    def test_composite_ops(self, composite_op, want_decomp):
+        """Test that ZYZ decomposition is used for composite operators."""
+        have_decomp = ctrl_decomp_zyz(composite_op, 1)
+        assert have_decomp == want_decomp
+
     @pytest.mark.parametrize("test_expand", [False, True])
     def test_zyz_decomp_no_control_values(self, test_expand):
         """Test that the ZYZ decomposition is used for single qubit target operations