Repository: PennyLaneAI/pennylane

Issue:
[BUG] Sub-devices for `default.qubit` are not used with variations of the interface name
### Expected behavior

Since we accept variations on the interface name when initializing a QNode, using any accepted variation should delegate to the corresponding sub-device of `default.qubit` the same way as when the "canonical" name of the interface is used.

The list of legal values for the `interface` keyword argument to a QNode is shown below:

https://github.com/PennyLaneAI/pennylane/blob/d9810446e1809c19cc4abcbffe545ca56d8308c4/pennylane/interfaces/execution.py#L34-L40

For example, `'tf'` and `'tensorflow'` are both accepted.


### Actual behavior

Using an interface name variation is accepted by the QNode but does not delegate to the proper sub-device. As seen on these lines, only the "canonical" version will properly return the sub-device:

https://github.com/PennyLaneAI/pennylane/blob/d9810446e1809c19cc4abcbffe545ca56d8308c4/pennylane/devices/default_qubit.py#L569-L574

This leads to either silently fallback to parameter-shift (or finite-diff) when no backprop device is found:

```py
dev = qml.device('default.qubit', wires=1)

@qml.qnode(dev, interface='tensorflow')  # no error, but does not use backprop
def circuit():
    return
```

https://github.com/PennyLaneAI/pennylane/blob/d9810446e1809c19cc4abcbffe545ca56d8308c4/pennylane/qnode.py#L358-L364

or to an error when backprop is specified explicitly:

```py
dev = qml.device('default.qubit', wires=1)

@qml.qnode(dev, interface='tensorflow', diff_method='backprop')  # error
def circuit():
    return
```

```
QuantumFunctionError: Device default.qubit only supports diff_method='backprop' when using the ['tf', 'torch', 'autograd', 'jax'] interfaces.
```

### Additional information

One solution is to add all the variations (such as 'tensorflow') to the sub-device dictionary:

https://github.com/PennyLaneAI/pennylane/blob/d9810446e1809c19cc4abcbffe545ca56d8308c4/pennylane/devices/default_qubit.py#L569-L574

Another might be to convert all accepted variations to the "canonical" one. However, some variations seem to carry actual meaning, for example 'jax-jit' is distinct from 'jax'.

A third is to remove variations completely and only allow the "canonical" version (e.g. 'tf').

In general, it seems that interface names are used rather inconsistently throughout the code base, sometimes checking against 'tf', sometimes against 'tensorflow', and sometimes both. 

### Source code

_No response_

### Tracebacks

_No response_

### System information

```shell
-
```


### Existing GitHub issues

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


Discussion:

Resolving patch:
diff --git a/pennylane/interfaces/__init__.py b/pennylane/interfaces/__init__.py
--- a/pennylane/interfaces/__init__.py
+++ b/pennylane/interfaces/__init__.py
@@ -41,7 +41,7 @@
     ~interfaces.torch
 
 """
-from .execution import cache_execute, execute, INTERFACE_NAMES, SUPPORTED_INTERFACES
+from .execution import cache_execute, execute, INTERFACE_MAP, SUPPORTED_INTERFACES
 from .set_shots import set_shots
 
 
diff --git a/pennylane/interfaces/execution.py b/pennylane/interfaces/execution.py
--- a/pennylane/interfaces/execution.py
+++ b/pennylane/interfaces/execution.py
@@ -20,7 +20,6 @@
 """
 # pylint: disable=import-outside-toplevel,too-many-arguments,too-many-branches,not-callable
 from functools import wraps
-import itertools
 import warnings
 import inspect
 from contextlib import _GeneratorContextManager
@@ -31,17 +30,25 @@
 from .set_shots import set_shots
 
 
-INTERFACE_NAMES = {
-    "NumPy": (None,),
-    "Autograd": ("autograd", "numpy"),  # for backwards compatibility
-    "JAX": ("jax", "jax-jit", "jax-python", "JAX"),
-    "PyTorch": ("torch", "pytorch"),
-    "TensorFlow": ("tf", "tensorflow", "tensorflow-autograph", "tf-autograph"),
+INTERFACE_MAP = {
+    None: "Numpy",
+    "autograd": "autograd",
+    "numpy": "autograd",
+    "jax": "jax",
+    "jax-jit": "jax",
+    "jax-python": "jax",
+    "JAX": "jax",
+    "torch": "torch",
+    "pytorch": "torch",
+    "tf": "tf",
+    "tensorflow": "tf",
+    "tensorflow-autograph": "tf",
+    "tf-autograph": "tf",
 }
-"""dict[str, str]: maps the name of the interface to allowed interface strings"""
+"""dict[str, str]: maps an allowed interface specification to its canonical name."""
 
 #: list[str]: allowed interface strings
-SUPPORTED_INTERFACES = list(itertools.chain(*INTERFACE_NAMES.values()))
+SUPPORTED_INTERFACES = list(INTERFACE_MAP)
 """list[str]: allowed interface strings"""
 
 
@@ -379,31 +386,29 @@ def cost_fn(params, x):
         raise ValueError("Gradient transforms cannot be used with mode='forward'")
 
     try:
-        if interface in INTERFACE_NAMES["Autograd"]:
+        mapped_interface = INTERFACE_MAP[interface]
+    except KeyError as e:
+        raise ValueError(
+            f"Unknown interface {interface}. Supported " f"interfaces are {SUPPORTED_INTERFACES}"
+        ) from e
+    try:
+        if mapped_interface == "autograd":
             from .autograd import execute as _execute
-        elif interface in INTERFACE_NAMES["TensorFlow"]:
+        elif mapped_interface == "tf":
             import tensorflow as tf
 
             if not tf.executing_eagerly() or "autograph" in interface:
                 from .tensorflow_autograph import execute as _execute
             else:
                 from .tensorflow import execute as _execute
-
-        elif interface in INTERFACE_NAMES["PyTorch"]:
+        elif mapped_interface == "torch":
             from .torch import execute as _execute
-        elif interface in INTERFACE_NAMES["JAX"]:
+        else:  # is jax
             _execute = _get_jax_execute_fn(interface, tapes)
-        else:
-            raise ValueError(
-                f"Unknown interface {interface}. Supported "
-                f"interfaces are {SUPPORTED_INTERFACES}"
-            )
     except ImportError as e:
-        interface_name = [k for k, v in INTERFACE_NAMES.items() if interface in v][0]
-
         raise qml.QuantumFunctionError(
-            f"{interface_name} not found. Please install the latest "
-            f"version of {interface_name} to enable the '{interface}' interface."
+            f"{mapped_interface} not found. Please install the latest "
+            f"version of {mapped_interface} to enable the '{mapped_interface}' interface."
         ) from e
 
     res = _execute(
diff --git a/pennylane/qnode.py b/pennylane/qnode.py
--- a/pennylane/qnode.py
+++ b/pennylane/qnode.py
@@ -24,7 +24,7 @@
 
 import pennylane as qml
 from pennylane import Device
-from pennylane.interfaces import set_shots, SUPPORTED_INTERFACES
+from pennylane.interfaces import set_shots, SUPPORTED_INTERFACES, INTERFACE_MAP
 
 
 class QNode:
@@ -41,7 +41,8 @@ class QNode:
         func (callable): a quantum function
         device (~.Device): a PennyLane-compatible device
         interface (str): The interface that will be used for classical backpropagation.
-            This affects the types of objects that can be passed to/returned from the QNode:
+            This affects the types of objects that can be passed to/returned from the QNode. See
+            ``qml.interfaces.SUPPORTED_INTERFACES`` for a list of all accepted strings.
 
             * ``"autograd"``: Allows autograd to backpropagate
               through the QNode. The QNode accepts default Python types
@@ -402,19 +403,18 @@ def best_method_str(device, interface):
 
     @staticmethod
     def _validate_backprop_method(device, interface):
-        # determine if the device supports backpropagation
-        backprop_interface = device.capabilities().get("passthru_interface", None)
-
-        # determine if the device has any child devices that support backpropagation
-        backprop_devices = device.capabilities().get("passthru_devices", None)
-
         if device.shots is not None:
             raise qml.QuantumFunctionError("Backpropagation is only supported when shots=None.")
 
+        mapped_interface = INTERFACE_MAP.get(interface, interface)
+
+        # determine if the device supports backpropagation
+        backprop_interface = device.capabilities().get("passthru_interface", None)
+
         if backprop_interface is not None:
             # device supports backpropagation natively
 
-            if interface == backprop_interface:
+            if mapped_interface == backprop_interface:
                 return "backprop", {}, device
 
             raise qml.QuantumFunctionError(
@@ -422,17 +422,20 @@ def _validate_backprop_method(device, interface):
                 f"{backprop_interface} interface."
             )
 
+        # determine if the device has any child devices that support backpropagation
+        backprop_devices = device.capabilities().get("passthru_devices", None)
+
         if backprop_devices is not None:
             # device is analytic and has child devices that support backpropagation natively
 
-            if interface in backprop_devices:
+            if mapped_interface in backprop_devices:
                 # TODO: need a better way of passing existing device init options
                 # to a new device?
                 expand_fn = device.expand_fn
                 batch_transform = device.batch_transform
 
                 device = qml.device(
-                    backprop_devices[interface],
+                    backprop_devices[mapped_interface],
                     wires=device.wires,
                     shots=device.shots,
                 )


Test patch:
diff --git a/tests/interfaces/test_autograd.py b/tests/interfaces/test_autograd.py
--- a/tests/interfaces/test_autograd.py
+++ b/tests/interfaces/test_autograd.py
@@ -49,8 +49,8 @@ def test_import_error(self, mocker):
 
         with pytest.raises(
             qml.QuantumFunctionError,
-            match="Autograd not found. Please install the latest version "
-            "of Autograd to enable the 'autograd' interface",
+            match="autograd not found. Please install the latest version "
+            "of autograd to enable the 'autograd' interface",
         ):
             qml.execute([tape], dev, gradient_fn=param_shift, interface="autograd")
 
diff --git a/tests/test_qnode.py b/tests/test_qnode.py
--- a/tests/test_qnode.py
+++ b/tests/test_qnode.py
@@ -129,6 +129,20 @@ def test_validate_backprop_method(self, monkeypatch):
         assert method == "backprop"
         assert device is dev
 
+    @pytest.mark.all_interfaces
+    @pytest.mark.parametrize("accepted_name, official_name", qml.interfaces.INTERFACE_MAP.items())
+    def test_validate_backprop_method_all_interface_names(self, accepted_name, official_name):
+        """Test that backprop devices are mapped for all possible interface names."""
+        if accepted_name is None:
+            pytest.skip("None is not a backprop interface.")
+
+        dev = qml.device("default.qubit", wires=1)
+
+        diff_method, _, new_dev = QNode._validate_backprop_method(dev, accepted_name)
+
+        assert diff_method == "backprop"
+        assert new_dev.capabilities().get("passthru_interface") == official_name
+
     def test_validate_backprop_child_method(self, monkeypatch):
         """Test that the method for validating the backprop diff method
         tape works as expected if a child device supports backprop"""