Repository: PennyLaneAI/pennylane
Pull request #5198 (2024-02-13 20:42:38)

Bugfix: ctrl_decomp_zyz - isclose => allclose
### Before submitting

Please complete the following checklist when submitting a PR:

- [x] All new features must include a unit test.
      If you've fixed a bug or added code that should be tested, add a test to the
      test directory!

- [x] All new functions and code must be clearly commented and documented.
      If you do make documentation changes, make sure that the docs build and
      render correctly by running `make docs`.

- [x] Ensure that the test suite passes, by running `make test`.

- [x] Add a new entry to the `doc/releases/changelog-dev.md` file, summarizing the
      change, and including a link back to the PR.

- [x] The PennyLane source code conforms to
      [PEP8 standards](https://www.python.org/dev/peps/pep-0008/).
      We check all of our code against [Pylint](https://www.pylint.org/).
      To lint modified files, simply `pip install pylint`, and then
      run `pylint pennylane/path/to/file.py`.

When all the above are checked, delete everything above the dashed
line and fill in the pull request template.

------------------------------------------------------------------------------------------------------------

**Context:**
While introducing `C(Rot)` in Lightning, the test below failed in `ctrl_decomp_zyz` because all the `qml.math.isclose` branches raise errors with gradient tracers. 

```
import pennylane as qml
import pennylane.numpy as np

np.random.seed(1337)

dev = qml.device("default.qubit", wires=3)
target_wires = [2]
control_wires = [0, 1]
control_values = [True, True]
def circuit(p):
    qml.ctrl(
        qml.Rot(*p, wires=target_wires),
        control_wires,
        control_values=control_values,
    )
    return np.array([qml.expval(qml.PauliY(i)) for i in range(3)])

circ_ad = qml.QNode(circuit, dev, diff_method="adjoint")
par = np.array([0.1234, -np.pi / 7, 0.5678])
jac_ad = np.array(qml.jacobian(circ_ad)(par))
```

**Description of the Change:**
Change `isclose` by `allclose` as suggested by @albi3ro .

**Benefits:**

**Possible Drawbacks:**

**Related GitHub Issues:**


codecov[bot]: ## [Codecov](https://app.codecov.io/gh/PennyLaneAI/pennylane/pull/5198?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 [(`d376a7f`)](https://app.codecov.io/gh/PennyLaneAI/pennylane/commit/d376a7f74e0c6924c5c3246e057fc3d83fae35cb?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) 99.68% compared to head [(`08122e1`)](https://app.codecov.io/gh/PennyLaneAI/pennylane/pull/5198?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) 99.68%.


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


```diff
@@            Coverage Diff             @@
##           master    #5198      +/-   ##
==========================================
- Coverage   99.68%   99.68%   -0.01%     
==========================================
  Files         396      396              
  Lines       36463    36191     -272     
==========================================
- Hits        36349    36076     -273     
- Misses        114      115       +1     
```



</details>

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

vincentmr: Rot decomposition isclose allclose [sc-56743]

vincentmr: > Mind adding some tests?

No, some functional test like the example above would be good? 
vincentmr: @albi3ro I limited the changes to `ctrl_decomp_zyz` since the other functions are said to be non-autodifferentiable.

Patch:
diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md
index b5370480d..940d879fe 100644
--- a/doc/releases/changelog-dev.md
+++ b/doc/releases/changelog-dev.md
@@ -512,8 +512,11 @@
 
 <h3>Bug fixes 🐛</h3>
 
+* `ctrl_decomp_zyz` is now differentiable.
+  [(#5198)](https://github.com/PennyLaneAI/pennylane/pull/5198)
+
 * `qml.ops.Pow.matrix()` is now differentiable with TensorFlow with integer exponents.
-[(#5178)](https://github.com/PennyLaneAI/pennylane/pull/5178)
+  [(#5178)](https://github.com/PennyLaneAI/pennylane/pull/5178)
 
 * The `qml.MottonenStatePreparation` template is updated to include a global phase operation.
   [(#5166)](https://github.com/PennyLaneAI/pennylane/pull/5166)
diff --git a/pennylane/ops/op_math/controlled_decompositions.py b/pennylane/ops/op_math/controlled_decompositions.py
index 26c4c5a1a..59f3da1f1 100644
--- a/pennylane/ops/op_math/controlled_decompositions.py
+++ b/pennylane/ops/op_math/controlled_decompositions.py
@@ -193,9 +193,9 @@ def ctrl_decomp_zyz(target_operation: Operator, control_wires: Wires):
 
     decomp = []
 
-    if not qml.math.isclose(0.0, phi, atol=1e-8, rtol=0):
+    if not qml.math.allclose(0.0, phi, atol=1e-8, rtol=0):
         decomp.append(qml.RZ(phi, wires=target_wire))
-    if not qml.math.isclose(0.0, theta / 2, atol=1e-8, rtol=0):
+    if not qml.math.allclose(0.0, theta / 2, atol=1e-8, rtol=0):
         decomp.extend(
             [
                 qml.RY(theta / 2, wires=target_wire),
@@ -205,10 +205,10 @@ def ctrl_decomp_zyz(target_operation: Operator, control_wires: Wires):
         )
     else:
         decomp.append(qml.ctrl(qml.PauliX(wires=target_wire), control=control_wires))
-    if not qml.math.isclose(0.0, -(phi + omega) / 2, atol=1e-6, rtol=0):
+    if not qml.math.allclose(0.0, -(phi + omega) / 2, atol=1e-6, rtol=0):
         decomp.append(qml.RZ(-(phi + omega) / 2, wires=target_wire))
     decomp.append(qml.ctrl(qml.PauliX(wires=target_wire), control=control_wires))
-    if not qml.math.isclose(0.0, (omega - phi) / 2, atol=1e-8, rtol=0):
+    if not qml.math.allclose(0.0, (omega - phi) / 2, atol=1e-8, rtol=0):
         decomp.append(qml.RZ((omega - phi) / 2, wires=target_wire))
 
     return decomp
diff --git a/pennylane/ops/op_math/controlled_ops.py b/pennylane/ops/op_math/controlled_ops.py
index df862bc42..0c271d281 100644
--- a/pennylane/ops/op_math/controlled_ops.py
+++ b/pennylane/ops/op_math/controlled_ops.py
@@ -939,7 +939,6 @@ class Toffoli(ControlledOp):
 
 
 def _check_and_convert_control_values(control_values, control_wires):
-
     if isinstance(control_values, str):
         # Make sure all values are either 0 or 1
         if not set(control_values).issubset({"1", "0"}):


Test patch:
diff --git a/tests/ops/op_math/test_controlled_decompositions.py b/tests/ops/op_math/test_controlled_decompositions.py
index b51dd44ad..46626d395 100644
--- a/tests/ops/op_math/test_controlled_decompositions.py
+++ b/tests/ops/op_math/test_controlled_decompositions.py
@@ -133,6 +133,41 @@ class TestControlledDecompositionZYZ:
 
         assert np.allclose(res, expected, atol=tol, rtol=0)
 
+    @pytest.mark.parametrize("control_wires", ([1], [1, 2], [1, 2, 3]))
+    def test_decomposition_circuit_gradient(self, control_wires, tol):
+        """Tests that the controlled decomposition of a single-qubit operation
+        behaves as expected in a quantum circuit"""
+        n_qubits = 4
+        np.random.seed(1337)
+
+        dev = qml.device("default.qubit", wires=n_qubits)
+        init_state = np.random.rand(2**n_qubits) + 1.0j * np.random.rand(2**n_qubits)
+        init_state /= np.sqrt(np.dot(np.conj(init_state), init_state))
+        init_state = np.array(init_state)
+        target_wires = [0]
+        control_values = [True] * len(control_wires)
+
+        def circuit(p):
+            qml.StatePrep(init_state, wires=range(n_qubits))
+            qml.ctrl(
+                qml.Rot(*p, wires=target_wires),
+                control_wires,
+                control_values=control_values,
+            )
+            return qml.probs(wires=target_wires)
+
+        circ_ad = qml.QNode(circuit, dev, diff_method="adjoint")
+        circ_bp = qml.QNode(circuit, dev, diff_method="backprop")
+        par = qml.numpy.array([0.1234, 0.235, 0.5678])
+        jac_ad = qml.jacobian(circ_ad)(par)
+        jac_bp = qml.jacobian(circ_bp)(par)
+
+        # different methods must agree
+        assert jac_ad.size == 2 * 3
+        assert np.allclose(jac_ad.shape, [2, 3])
+        assert np.allclose(jac_ad.shape, jac_bp.shape)
+        assert np.allclose(jac_ad, jac_bp, atol=tol, rtol=0)
+
     @pytest.mark.parametrize("op", su2_ops)
     @pytest.mark.parametrize("control_wires", ([1], [1, 2], [1, 2, 3]))
     def test_decomposition_matrix(self, op, control_wires, tol):