Repository: PennyLaneAI/pennylane

Issue:
[BUG] Incomplete Operator Name Mapping for Controlled RX Gate
### Expected behavior

Great question. The way operators get translated from PL to Qiskit is via a dictionary lookup given the operator’s name in PL (see [here](https://github.com/PennyLaneAI/pennylane-qiskit/blob/d6964c723278272196c9fdea57bd2da4db9344e9/pennylane_qiskit/qiskit_device.py#L36) for the source code). The controlled RX gate has a different name in PennyLane depending on how you define it:

>>> op = qml.CRX(0.1, wires=[0, 1])
>>> op.name
'CRX'
>>> op = qml.ctrl(qml.RX(0.1, 0), control=[1])
>>> op.name
'C(RX)'


### Actual behavior

There is a mapping for when the name is CRX, but not for C(RX)

### Additional information

Info here:
[https://discuss.pennylane.ai/t/pennylane-ibmq-conversion-error/3498](url)

### Source code

```shell
import pennylane as qml
from pennylane import numpy as np

property_prices = [4, 8, 6, 3, 12, 15] # total 48 
variables_wires = [0, 1, 2, 3, 4, 5]

aux_oracle_wires = [6, 7, 8 ,9 ,10, 11]

def oracle(variables_wires, aux_oracle_wires):
    
    def add_k_fourier(k, wires):
        for j in range(len(wires)):
            qml.RZ(k * np.pi / (2**j), wires=wires[j])
            
    def value_second_sibling():
        
        qml.QFT(wires = aux_oracle_wires)
        
        for wire in variables_wires:
            qml.ctrl(add_k_fourier, control = wire)(property_prices[wire], wires = aux_oracle_wires)
            
        qml.adjoint(qml.QFT)(wires = aux_oracle_wires)
        
    value_second_sibling()
    qml.FlipSign(sum(property_prices) // 2, wires = aux_oracle_wires)
    qml.adjoint(value_second_sibling)()

dev = qml.device('qiskit.ibmq', wires=12, backend='ibmq_qasm_simulator')

@qml.qnode(dev)
def circuit():
    
    # step 1
    for wire in variables_wires:
        qml.Hadamard(wires = wire)
       
    # step 2
    oracle(variables_wires, aux_oracle_wires)
    
    # step 3
    qml.GroverOperator(wires = variables_wires)
    
    return qml.probs(wires = variables_wires)


import matplotlib.pyplot as plt

values = circuit()
plt.bar(range(len(values)), values)
```


### Tracebacks

```shell
# Put full error message here

---------------------------------------------------------------------------
KeyError                                  Traceback (most recent call last)
Cell In[5], line 3
      1 import matplotlib.pyplot as plt
----> 3 values = circuit()
      4 plt.bar(range(len(values)), values)

File ~\anaconda3\Lib\site-packages\pennylane\qnode.py:989, in QNode.__call__(self, *args, **kwargs)
    986     self.execute_kwargs.pop("mode")
    988 # pylint: disable=unexpected-keyword-arg
--> 989 res = qml.execute(
    990     (self._tape,),
    991     device=self.device,
    992     gradient_fn=self.gradient_fn,
    993     interface=self.interface,
    994     transform_program=self.transform_program,
    995     gradient_kwargs=self.gradient_kwargs,
    996     override_shots=override_shots,
    997     **self.execute_kwargs,
    998 )
   1000 res = res[0]
   1002 # convert result to the interface in case the qfunc has no parameters

File ~\anaconda3\Lib\site-packages\pennylane\interfaces\execution.py:757, in execute(tapes, device, gradient_fn, interface, transform_program, grad_on_execution, gradient_kwargs, cache, cachesize, max_diff, override_shots, expand_fn, max_expansion, device_batch_transform)
    754     raise ValueError("Gradient transforms cannot be used with grad_on_execution=True")
    756 ml_boundary_execute = _get_ml_boundary_execute(interface, _grad_on_execution)
--> 757 results = ml_boundary_execute(
    758     tapes, device, execute_fn, gradient_fn, gradient_kwargs, _n=1, max_diff=max_diff
    759 )
    761 results = batch_fn(results)
    762 return program_post_processing(results)

File ~\anaconda3\Lib\site-packages\pennylane\interfaces\autograd.py:317, in execute(tapes, device, execute_fn, gradient_fn, gradient_kwargs, _n, max_diff)
    312 # pylint misidentifies autograd.builtins as a dict
    313 # pylint: disable=no-member
    314 parameters = autograd.builtins.tuple(
    315     [autograd.builtins.list(t.get_parameters()) for t in tapes]
    316 )
--> 317 return _execute(
    318     parameters,
    319     tapes=tapes,
    320     device=device,
    321     execute_fn=execute_fn,
    322     gradient_fn=gradient_fn,
    323     gradient_kwargs=gradient_kwargs,
    324     _n=_n,
    325     max_diff=max_diff,
    326 )[0]

File ~\anaconda3\Lib\site-packages\autograd\tracer.py:48, in primitive.<locals>.f_wrapped(*args, **kwargs)
     46     return new_box(ans, trace, node)
     47 else:
---> 48     return f_raw(*args, **kwargs)

File ~\anaconda3\Lib\site-packages\pennylane\interfaces\autograd.py:378, in _execute(parameters, tapes, device, execute_fn, gradient_fn, gradient_kwargs, _n, max_diff)
    360 if logger.isEnabledFor(logging.DEBUG):
    361     logger.debug(
    362         "Entry with args=(parameters=%s, tapes=%s, device=%s, execute_fn=%s, gradient_fn=%s, gradient_kwargs=%s, _n=%s, max_diff=%s) called by=%s",
    363         parameters,
   (...)
    375         "::L".join(str(i) for i in inspect.getouterframes(inspect.currentframe(), 2)[1][1:3]),
    376     )
--> 378 res, jacs = execute_fn(tapes, **gradient_kwargs)
    380 return res, jacs

File ~\anaconda3\Lib\site-packages\pennylane\interfaces\execution.py:623, in execute.<locals>.inner_execute_with_empty_jac(tapes, **_)
    622 def inner_execute_with_empty_jac(tapes, **_):
--> 623     return (inner_execute(tapes), [])

File ~\anaconda3\Lib\site-packages\pennylane\interfaces\execution.py:255, in _make_inner_execute.<locals>.inner_execute(tapes, **_)
    253 if numpy_only:
    254     tapes = tuple(qml.transforms.convert_to_numpy_parameters(t) for t in tapes)
--> 255 return cached_device_execution(tapes)

File ~\anaconda3\Lib\site-packages\pennylane\interfaces\execution.py:377, in cache_execute.<locals>.wrapper(tapes, **kwargs)
    372         return (res, []) if return_tuple else res
    374 else:
    375     # execute all unique tapes that do not exist in the cache
    376     # convert to list as new device interface returns a tuple
--> 377     res = list(fn(tuple(execution_tapes.values()), **kwargs))
    379 final_res = []
    381 for i, tape in enumerate(tapes):

File ~\anaconda3\Lib\contextlib.py:81, in ContextDecorator.__call__.<locals>.inner(*args, **kwds)
     78 @wraps(func)
     79 def inner(*args, **kwds):
     80     with self._recreate_cm():
---> 81         return func(*args, **kwds)

File ~\anaconda3\Lib\site-packages\pennylane_qiskit\ibmq.py:91, in IBMQDevice.batch_execute(self, circuits)
     90 def batch_execute(self, circuits):  # pragma: no cover, pylint:disable=arguments-differ
---> 91     res = super().batch_execute(circuits, timeout=self.timeout_secs)
     92     if self.tracker.active:
     93         self._track_run()

File ~\anaconda3\Lib\site-packages\pennylane_qiskit\qiskit_device.py:473, in QiskitDevice.batch_execute(self, circuits, timeout)
    470 def batch_execute(self, circuits, timeout: int = None):
    471     # pylint: disable=missing-function-docstring
--> 473     compiled_circuits = self.compile_circuits(circuits)
    475     # Send the batch of circuit objects using backend.run
    476     self._current_job = self.backend.run(compiled_circuits, shots=self.shots, **self.run_args)

File ~\anaconda3\Lib\site-packages\pennylane_qiskit\qiskit_device.py:462, in QiskitDevice.compile_circuits(self, circuits)
    458 for circuit in circuits:
    459     # We need to reset the device here, else it will
    460     # not start the next computation in the zero state
    461     self.reset()
--> 462     self.create_circuit_object(circuit.operations, rotations=circuit.diagonalizing_gates)
    464     compiled_circ = self.compile()
    465     compiled_circ.name = f"circ{len(compiled_circuits)}"

File ~\anaconda3\Lib\site-packages\pennylane_qiskit\qiskit_device.py:273, in QiskitDevice.create_circuit_object(self, operations, **kwargs)
    261 """Builds the circuit objects based on the operations and measurements
    262 specified to apply.
    263 
   (...)
    269         pre-measurement into the eigenbasis of the observables.
    270 """
    271 rotations = kwargs.get("rotations", [])
--> 273 applied_operations = self.apply_operations(operations)
    275 # Rotating the state for measurement in the computational basis
    276 rotation_circuits = self.apply_operations(rotations)

File ~\anaconda3\Lib\site-packages\pennylane_qiskit\qiskit_device.py:322, in QiskitDevice.apply_operations(self, operations)
    318         par[idx] = p.tolist()
    320 operation = operation.name
--> 322 mapped_operation = self._operation_map[operation]
    324 self.qubit_state_vector_check(operation)
    326 qregs = [self._reg[i] for i in device_wires.labels]

KeyError: 'C(RZ)'
```


### System information

```shell
Name: PennyLane
Version: 0.32.0
Summary: PennyLane is a Python quantum machine learning library by Xanadu Inc.
Home-page: GitHub - PennyLaneAI/pennylane: PennyLane is a cross-platform Python library for differentiable programming of quantum computers. Train a quantum computer the same way as a neural network.
Author:
Author-email:
License: Apache License 2.0
Location: C:\Users\Carter\anaconda3\Lib\site-packages
Requires: appdirs, autograd, autoray, cachetools, networkx, numpy, pennylane-lightning, requests, rustworkx, scipy, semantic-version, toml, typing-extensions
Required-by: PennyLane-Lightning, PennyLane-qiskit

Platform info: Windows-10-10.0.22621-SP0
Python version: 3.11.5
Numpy version: 1.23.5
Scipy version: 1.11.1
Installed devices:

default.gaussian (PennyLane-0.32.0)
default.mixed (PennyLane-0.32.0)
default.qubit (PennyLane-0.32.0)
default.qubit.autograd (PennyLane-0.32.0)
default.qubit.jax (PennyLane-0.32.0)
default.qubit.tf (PennyLane-0.32.0)
default.qubit.torch (PennyLane-0.32.0)
default.qutrit (PennyLane-0.32.0)
null.qubit (PennyLane-0.32.0)
lightning.qubit (PennyLane-Lightning-0.32.0)
qiskit.aer (PennyLane-qiskit-0.32.0)
qiskit.basicaer (PennyLane-qiskit-0.32.0)
qiskit.ibmq (PennyLane-qiskit-0.32.0)
qiskit.ibmq.circuit_runner (PennyLane-qiskit-0.32.0)
qiskit.ibmq.sampler (PennyLane-qiskit-0.32.0)
qiskit.remote (PennyLane-qiskit-0.32.0)
```


### Existing GitHub issues

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


Discussion:
Hey @c-3543! Thanks for making the bug report. Just for the paper trail, this came up as an issue in the PL-qiskit plugin because `QISKIT_OPERATION_MAP_SELF_ADJOINT` doesn't have a `'C(RZ)'` key but does have a `'CRZ'` one. 

https://github.com/PennyLaneAI/pennylane-qiskit/blob/d6964c723278272196c9fdea57bd2da4db9344e9/pennylane_qiskit/qiskit_device.py#L36

Resolving patch:
diff --git a/pennylane/ops/op_math/controlled.py b/pennylane/ops/op_math/controlled.py
--- a/pennylane/ops/op_math/controlled.py
+++ b/pennylane/ops/op_math/controlled.py
@@ -30,6 +30,7 @@
 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 @@ def has_decomposition(self):
             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 @@ def simplify(self) -> "Controlled":
         )
 
 
+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
--- a/tests/ops/op_math/test_controlled.py
+++ b/tests/ops/op_math/test_controlled.py
@@ -263,6 +263,12 @@ def test_has_decomposition_true_via_pauli_x(self):
         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 @@ def test_None_default(self):
         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: