Repository: PennyLaneAI/pennylane

Issue:
Decomposition of multi-controlled RZ not supported[BUG]
### Expected behavior

Should be able to decompose a multi-controlled RZ gate:

```python
import pennylane as qml
dev = qml.device("default.qubit", wires=[0,1,2])
decomposition = qml.ctrl(qml.RZ(0, wires=[0]), control=[1,2]).decomposition()
@qml.qnode(dev)
def my_circuit():
    for op in decomposition:
        qml.apply(op)
    return qml.state()
qml.draw(my_circuit)()
```

this should be akin to how one can decompose a multi-controlled PauliX, eg:
```python
dev = qml.device("default.qubit", wires=[0,1,2])
decomposition = qml.ctrl(qml.PauliX(wires=[0]), control=[1,2]).decomposition()
@qml.qnode(dev)
def my_circuit():
    for op in decomposition:
        qml.apply(op)
    return qml.state()
qml.draw(my_circuit)()
```


### Actual behavior

A `DecompositionUndefinedError` is raised

### Additional information

This causes errors with various plug-in devices, eg the ionq or qiskit simulators, see:
```python
@qml.qnode(qml.device('ionq.simulator', wires=3,shots=10))
def my_circuit():
    qml.ctrl(qml.RZ(0,wires=[0]),control=[1,2])
    return qml.expval(qml.PauliZ(wires=0))
my_circuit()
```

Seems that this backs up to the base operation not having a decomposition in `_decompose_no_control_values` of `qml.ops.op_math.controlled.py` (though commented out lines 591-600 seem to get near to this). Should be able to add something like the following below line 586 of `controlled.py`:
```python
if isinstance(op.base, qml.RZ):
    return [qml.RZ(phi=op.data[0]/2,wires=op.target_wires), qml.MultiControlledX(wires=op.active_wires, work_wires=op.work_wires),qml.RZ(phi=-1*op.data[0]/2,wires=op.target_wires), qml.MultiControlledX(wires=op.active_wires, work_wires=op.work_wires)]
```
Can do something similar for a mulitcontrolled-RY and from there, via a ZYZ decomposition, should be able to implement multicontrolled versions of all 1 qubit unitaries

### Source code

_No response_

### Tracebacks

```shell
DecompositionUndefinedError               Traceback (most recent call last)
Cell In[54], line 3
      1 dev = qml.device("default.qubit", wires=[0,1,2])
----> 3 decomposition = qml.ctrl(qml.RZ(0, wires=[0]), control=[1,2]).decomposition()
      5 @qml.qnode(dev)
      6 def my_circuit():
      7     for op in decomposition:

File /opt/homebrew/Caskroom/miniconda/base/envs/penny/lib/python3.8/site-packages/pennylane/ops/op_math/controlled.py:497, in Controlled.decomposition(self)
    495     decomp = _decompose_no_control_values(self)
    496     if decomp is None:
--> 497         raise qml.operation.DecompositionUndefinedError
    498     return decomp
    500 # We need to add paulis to flip some control wires

DecompositionUndefinedError:
```


### System information

```shell
Name: PennyLane
Version: 0.31.0
Summary: PennyLane is a Python quantum machine learning library by Xanadu Inc.
Home-page: https://github.com/PennyLaneAI/pennylane
Author: 
Author-email: 
License: Apache License 2.0
Location: /opt/homebrew/Caskroom/miniconda/base/envs/penny/lib/python3.8/site-packages
Requires: appdirs, autograd, autoray, cachetools, networkx, numpy, pennylane-lightning, requests, rustworkx, scipy, semantic-version, toml
Required-by: PennyLane-IonQ, PennyLane-Lightning, PennyLane-qiskit

Platform info:           macOS-13.3.1-arm64-arm-64bit
Python version:          3.8.17
Numpy version:           1.23.5
Scipy version:           1.10.0
Installed devices:
- default.gaussian (PennyLane-0.31.0)
- default.mixed (PennyLane-0.31.0)
- default.qubit (PennyLane-0.31.0)
- default.qubit.autograd (PennyLane-0.31.0)
- default.qubit.jax (PennyLane-0.31.0)
- default.qubit.tf (PennyLane-0.31.0)
- default.qubit.torch (PennyLane-0.31.0)
- default.qutrit (PennyLane-0.31.0)
- null.qubit (PennyLane-0.31.0)
- ionq.qpu (PennyLane-IonQ-0.28.0)
- ionq.simulator (PennyLane-IonQ-0.28.0)
- lightning.qubit (PennyLane-Lightning-0.31.0)
- qiskit.aer (PennyLane-qiskit-0.31.0)
- qiskit.basicaer (PennyLane-qiskit-0.31.0)
- qiskit.ibmq (PennyLane-qiskit-0.31.0)
- qiskit.ibmq.circuit_runner (PennyLane-qiskit-0.31.0)
- qiskit.ibmq.sampler (PennyLane-qiskit-0.31.0)
- qiskit.remote (PennyLane-qiskit-0.31.0)
```


### Existing GitHub issues

- [X] I have searched existing GitHub issues to make sure the issue does not already exist.

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

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


```diff
@@           Coverage Diff            @@
##           master    #4697    +/-   ##
========================================
  Coverage   99.64%   99.64%            
========================================
  Files         377      377            
  Lines       33846    33993   +147     
========================================
+ Hits        33725    33872   +147     
  Misses        121      121            
```


| [Files](https://app.codecov.io/gh/PennyLaneAI/pennylane/pull/4697?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) | Coverage Δ | |
|---|---|---|
| [pennylane/\_\_init\_\_.py](https://app.codecov.io/gh/PennyLaneAI/pennylane/pull/4697?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI#diff-cGVubnlsYW5lL19faW5pdF9fLnB5) | `100.00% <100.00%> (ø)` | |
| [pennylane/devices/preprocess.py](https://app.codecov.io/gh/PennyLaneAI/pennylane/pull/4697?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI#diff-cGVubnlsYW5lL2RldmljZXMvcHJlcHJvY2Vzcy5weQ==) | `100.00% <100.00%> (ø)` | |
| [pennylane/drawer/draw.py](https://app.codecov.io/gh/PennyLaneAI/pennylane/pull/4697?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI#diff-cGVubnlsYW5lL2RyYXdlci9kcmF3LnB5) | `100.00% <100.00%> (ø)` | |
| [pennylane/ops/op\_math/controlled.py](https://app.codecov.io/gh/PennyLaneAI/pennylane/pull/4697?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI#diff-cGVubnlsYW5lL29wcy9vcF9tYXRoL2NvbnRyb2xsZWQucHk=) | `99.63% <100.00%> (+0.01%)` | :arrow_up: |
| [pennylane/transforms/tape\_expand.py](https://app.codecov.io/gh/PennyLaneAI/pennylane/pull/4697?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI#diff-cGVubnlsYW5lL3RyYW5zZm9ybXMvdGFwZV9leHBhbmQucHk=) | `100.00% <100.00%> (ø)` | |

... and [4 files with indirect coverage changes](https://app.codecov.io/gh/PennyLaneAI/pennylane/pull/4697/indirect-changes?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI)


</details>

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


Patch:
diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md
index 0220304d1..cd0d2c9a3 100644
--- a/doc/releases/changelog-dev.md
+++ b/doc/releases/changelog-dev.md
@@ -198,6 +198,9 @@
 
 <h3>Improvements 🛠</h3>
 
+* Multi-controlled operations with a single qubit special unitary target can now automatically decompose.
+  [(#4697)](https://github.com/PennyLaneAI/pennylane/pull/4697)
+
 * `pennylane.devices.preprocess` now offers the transforms `decompose`, `validate_observables`, `validate_measurements`,
   `validate_device_wires`, `validate_multiprocessing_workers`, `warn_about_trainable_observables`,
   and `no_sampling` to assist in the construction of devices under the new `devices.Device` API.
diff --git a/pennylane/ops/op_math/controlled.py b/pennylane/ops/op_math/controlled.py
index bfadc3fbc..a1aac04db 100644
--- a/pennylane/ops/op_math/controlled.py
+++ b/pennylane/ops/op_math/controlled.py
@@ -30,6 +30,7 @@ from pennylane.operation import Operator
 from pennylane.wires import Wires
 
 from .symbolicop import SymbolicOp
+from .controlled_decompositions import ctrl_decomp_bisect, ctrl_decomp_zyz
 
 
 def ctrl(op, control, control_values=None, work_wires=None):
@@ -489,8 +490,8 @@ class Controlled(SymbolicOp):
             return True
         if isinstance(self.base, qml.PauliX):
             return True
-        # if len(self.base.wires) == 1 and getattr(self.base, "has_matrix", False):
-        #    return True
+        if _is_single_qubit_special_unitary(self.base):
+            return True
         if self.base.has_decomposition:
             return True
 
@@ -569,6 +570,14 @@ class Controlled(SymbolicOp):
         )
 
 
+def _is_single_qubit_special_unitary(op):
+    if not op.has_matrix or len(op.wires) != 1:
+        return False
+    mat = op.matrix()
+    det = mat[0, 0] * mat[1, 1] - mat[0, 1] * mat[1, 0]
+    return qmlmath.allclose(det, 1)
+
+
 # pylint: disable=protected-access
 def _decompose_no_control_values(op: "operation.Operator") -> List["operation.Operator"]:
     """Provides a decomposition without considering control values. Returns None if
@@ -584,16 +593,10 @@ def _decompose_no_control_values(op: "operation.Operator") -> List["operation.Op
     if isinstance(op.base, qml.PauliX):
         # has some special case handling of its own for further decomposition
         return [qml.MultiControlledX(wires=op.active_wires, work_wires=op.work_wires)]
-    # if (
-    #    len(op.base.wires) == 1
-    #    and len(op.control_wires) >= 2
-    #    and getattr(op.base, "has_matrix", False)
-    #    and qmlmath.get_interface(*op.data) == "numpy"  # as implemented, not differentiable
-    # ):
-    # Bisect algorithms use CNOTs and single qubit unitary
-    #    return ctrl_decomp_bisect(op.base, op.control_wires)
-    # if len(op.base.wires) == 1 and getattr(op.base, "has_matrix", False):
-    #    return ctrl_decomp_zyz(op.base, op.control_wires)
+    if _is_single_qubit_special_unitary(op.base):
+        if len(op.control_wires) >= 2 and qmlmath.get_interface(*op.data) == "numpy":
+            return ctrl_decomp_bisect(op.base, op.control_wires)
+        return ctrl_decomp_zyz(op.base, op.control_wires)
 
     if not op.base.has_decomposition:
         return None


Test patch:
diff --git a/tests/ops/op_math/test_controlled.py b/tests/ops/op_math/test_controlled.py
index 9bf6069ea..aec60025b 100644
--- a/tests/ops/op_math/test_controlled.py
+++ b/tests/ops/op_math/test_controlled.py
@@ -263,6 +263,12 @@ class TestProperties:
         op = Controlled(qml.PauliX(3), [0, 4])
         assert op.has_decomposition is True
 
+    def test_has_decomposition_multicontrolled_special_unitary(self):
+        """Test that a one qubit special unitary with any number of control
+        wires has a decomposition."""
+        op = Controlled(qml.RX(1.234, wires=0), (1, 2, 3, 4, 5))
+        assert op.has_decomposition
+
     def test_has_decomposition_true_via_base_has_decomp(self):
         """Test that Controlled claims `has_decomposition` to be true if
         the base has a decomposition and indicates this via `has_decomposition`."""
@@ -813,6 +819,38 @@ class TestHelperMethod:
         op = Controlled(TempOperator(0), (1, 2))
         assert _decompose_no_control_values(op) is None
 
+    def test_non_differentiable_one_qubit_special_unitary(self):
+        """Assert that a non differentiable on qubit special unitary uses the bisect decomposition."""
+        op = qml.ctrl(qml.RZ(1.2, wires=0), (1, 2, 3, 4))
+        decomp = op.decomposition()
+
+        assert qml.equal(decomp[0], qml.MultiControlledX(wires=(1, 2, 0), work_wires=(3, 4)))
+        assert isinstance(decomp[1], qml.QubitUnitary)
+        assert qml.equal(decomp[2], qml.MultiControlledX(wires=(3, 4, 0), work_wires=(1, 2)))
+        assert isinstance(decomp[3].base, qml.QubitUnitary)
+        assert qml.equal(decomp[4], qml.MultiControlledX(wires=(1, 2, 0), work_wires=(3, 4)))
+        assert isinstance(decomp[5], qml.QubitUnitary)
+        assert qml.equal(decomp[6], qml.MultiControlledX(wires=(3, 4, 0), work_wires=(1, 2)))
+        assert isinstance(decomp[7].base, qml.QubitUnitary)
+
+        decomp_mat = qml.matrix(op.decomposition, wire_order=op.wires)()
+        assert qml.math.allclose(op.matrix(), decomp_mat)
+
+    def test_differentiable_one_qubit_special_unitary(self):
+        """Assert that a differentiable qubit speical unitary uses the zyz decomposition."""
+
+        op = qml.ctrl(qml.RZ(qml.numpy.array(1.2), 0), (1, 2, 3, 4))
+        decomp = op.decomposition()
+
+        assert qml.equal(decomp[0], qml.RZ(qml.numpy.array(1.2), 0))
+        assert qml.equal(decomp[1], qml.MultiControlledX(wires=(1, 2, 3, 4, 0)))
+        assert qml.equal(decomp[2], qml.RZ(qml.numpy.array(-0.6), wires=0))
+        assert qml.equal(decomp[3], qml.MultiControlledX(wires=(1, 2, 3, 4, 0)))
+        assert qml.equal(decomp[4], qml.RZ(qml.numpy.array(-0.6), wires=0))
+
+        decomp_mat = qml.matrix(op.decomposition, wire_order=op.wires)()
+        assert qml.math.allclose(op.matrix(), decomp_mat)
+
 
 @pytest.mark.parametrize("test_expand", (False, True))
 class TestDecomposition: