Repository: PennyLaneAI/pennylane (python)

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.

Pull request:
[Bugfix] Interface name variations dispatch to correct backprop device
Fixes #2546 

Now if you specify QNodes like:
```
qml.QNode(qfunc, qml.device('default.qubit', wires=1), interface="tensorflow", diff_method="backprop")
```

It will map the device to `default.qubit.tf` and using backpropagation instead of silently switching to a different `diff_method`.

In solving this bug, I substituted `qml.interfaces.INTERFACE_MAP` for `qml.interfaces.INTERFACE_NAMES`, which maps in the opposite direction.  This allowed from some readability improvements. `qml.interfaces.INTERFACE_MAP` maps provided name to canonical name, instead of canonical name to list of acceptable provided names.

Patch:
diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md
index fe9309784..c7f656c6c 100644
--- a/doc/releases/changelog-dev.md
+++ b/doc/releases/changelog-dev.md
@@ -111,6 +111,9 @@
 
 <h3>Bug fixes</h3>
 
+* `QNode`'s now can interpret variations on the interface name, like `"tensorflow"` or `"jax-jit"`, when requesting backpropagation. 
+  [(#2591)](https://github.com/PennyLaneAI/pennylane/pull/2591)
+
 * Fixed a bug for `diff_method="adjoint"` where incorrect gradients were
   computed for QNodes with parametrized observables (e.g., `qml.Hermitian`).
   [(#2543)](https://github.com/PennyLaneAI/pennylane/pull/2543)
diff --git a/pennylane/interfaces/__init__.py b/pennylane/interfaces/__init__.py
index 8488070a7..7d03080a0 100644
--- a/pennylane/interfaces/__init__.py
+++ b/pennylane/interfaces/__init__.py
@@ -41,7 +41,7 @@ Supported interfaces
     ~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
index 2c386c930..7aa7d9906 100644
--- a/pennylane/interfaces/execution.py
+++ b/pennylane/interfaces/execution.py
@@ -20,7 +20,6 @@ devices with autodifferentiation support.
 """
 # 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 @@ import pennylane as qml
 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 execute(
         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
index 40fc1f029..03536f530 100644
--- a/pennylane/qnode.py
+++ b/pennylane/qnode.py
@@ -24,7 +24,7 @@ import autograd
 
 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 @@ class QNode:
 
     @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 @@ class QNode:
                 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
index c1721fbde..c98f4711d 100644
--- a/tests/interfaces/test_autograd.py
+++ b/tests/interfaces/test_autograd.py
@@ -49,8 +49,8 @@ class TestAutogradExecuteUnitTests:
 
         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
index 7c0ee2d4f..397e5de28 100644
--- a/tests/test_qnode.py
+++ b/tests/test_qnode.py
@@ -129,6 +129,20 @@ class TestValidation:
         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"""