Repository: PennyLaneAI/pennylane

Issue:
`Wires(0)` and `Wires(tensor(0))` are not treated the same
Submitting on behalf of @kshyatt-aws 

`DoubleExcitation` and `SingleExcitation` breaks when passing an `pennylane.numpy.ndarray`. The code works as expected when removing the `np.array` wrap of `excitations`.

```python
import pennylane as qml
import pennylane.numpy as np


dev = qml.device("default.qubit", wires=range(4))#, shots=shots)

@qml.qnode(dev) #, diff_method="parameter-shift")
def circuit_1(params, excitations):
    for i, excitation in enumerate(excitations):
        if len(excitation) == 4:
            qml.DoubleExcitation(params[i], wires=excitation)
        else:
            qml.SingleExcitation(params[i], wires=excitation)
    return qml.expval(qml.PauliZ(0))

excitations = np.array([[0, 1, 2, 3]])
params = np.zeros(len(excitations))
_ = circuit_1(params, excitations)
```
```pycon
---------------------------------------------------------------------------
KeyError                                  Traceback (most recent call last)
File [~/Qottmann/Xanadu/pennylane/pennylane/devices/default_qubit.py:223](https://vscode-remote+wsl-002bubuntu.vscode-resource.vscode-cdn.net/home/qottmann/Qottmann/Xanadu/Sandbox/~/Qottmann/Xanadu/pennylane/pennylane/devices/default_qubit.py:223), in DefaultQubit.map_wires(self, wires)
    222 try:
--> 223     mapped_wires = [self.wire_map[w] for w in wires]
    224 except KeyError as e:

File [~/Qottmann/Xanadu/pennylane/pennylane/devices/default_qubit.py:223](https://vscode-remote+wsl-002bubuntu.vscode-resource.vscode-cdn.net/home/qottmann/Qottmann/Xanadu/Sandbox/~/Qottmann/Xanadu/pennylane/pennylane/devices/default_qubit.py:223), in (.0)
    222 try:
--> 223     mapped_wires = [self.wire_map[w] for w in wires]
    224 except KeyError as e:

KeyError: tensor(0, requires_grad=True)

The above exception was the direct cause of the following exception:

WireError                                 Traceback (most recent call last)
/home/qottmann/Qottmann/Xanadu/Sandbox/Sandbox.ipynb Cell 3 in 1
     [16](vscode-notebook-cell://wsl%2Bubuntu/home/qottmann/Qottmann/Xanadu/Sandbox/Sandbox.ipynb#Z1352sdnNjb2RlLXJlbW90ZQ%3D%3D?line=15) excitations = np.array([[0, 1, 2, 3]])
     [17](vscode-notebook-cell://wsl%2Bubuntu/home/qottmann/Qottmann/Xanadu/Sandbox/Sandbox.ipynb#Z1352sdnNjb2RlLXJlbW90ZQ%3D%3D?line=16) params = np.zeros(len(excitations))
---> [18](vscode-notebook-cell://wsl%2Bubuntu/home/qottmann/Qottmann/Xanadu/Sandbox/Sandbox.ipynb#Z1352sdnNjb2RlLXJlbW90ZQ%3D%3D?line=17) _ = circuit_1(params, excitations)

File [~/Qottmann/Xanadu/pennylane/pennylane/qnode.py:867](https://vscode-remote+wsl-002bubuntu.vscode-resource.vscode-cdn.net/home/qottmann/Qottmann/Xanadu/Sandbox/~/Qottmann/Xanadu/pennylane/pennylane/qnode.py:867), in QNode.__call__(self, *args, **kwargs)
    865     self.execute_kwargs.pop("mode")
    866 # pylint: disable=unexpected-keyword-arg
--> 867 res = qml.execute(
    868     [self.tape],
    869     device=self.device,
    870     gradient_fn=self.gradient_fn,
    871     interface=self.interface,
    872     gradient_kwargs=self.gradient_kwargs,
    873     override_shots=override_shots,
    874     **self.execute_kwargs,
    875 )
    877 res = res[0]
    879 if old_interface == "auto":

File [~/Qottmann/Xanadu/pennylane/pennylane/interfaces/execution.py:407](https://vscode-remote+wsl-002bubuntu.vscode-resource.vscode-cdn.net/home/qottmann/Qottmann/Xanadu/Sandbox/~/Qottmann/Xanadu/pennylane/pennylane/interfaces/execution.py:407), in execute(tapes, device, gradient_fn, interface, grad_on_execution, gradient_kwargs, cache, cachesize, max_diff, override_shots, expand_fn, max_expansion, device_batch_transform)
    403     return batch_fn(res)
    405 if gradient_fn == "backprop" or interface is None:
    406     return batch_fn(
--> 407         qml.interfaces.cache_execute(
    408             batch_execute, cache, return_tuple=False, expand_fn=expand_fn
    409         )(tapes)
    410     )
    412 # the default execution function is batch_execute
    413 execute_fn = qml.interfaces.cache_execute(batch_execute, cache, expand_fn=expand_fn)

File [~/Qottmann/Xanadu/pennylane/pennylane/interfaces/execution.py:204](https://vscode-remote+wsl-002bubuntu.vscode-resource.vscode-cdn.net/home/qottmann/Qottmann/Xanadu/Sandbox/~/Qottmann/Xanadu/pennylane/pennylane/interfaces/execution.py:204), in cache_execute..wrapper(tapes, **kwargs)
    200         return (res, []) if return_tuple else res
    202 else:
    203     # execute all unique tapes that do not exist in the cache
--> 204     res = fn(execution_tapes.values(), **kwargs)
    206 final_res = []
    208 for i, tape in enumerate(tapes):

File [~/Qottmann/Xanadu/pennylane/pennylane/interfaces/execution.py:130](https://vscode-remote+wsl-002bubuntu.vscode-resource.vscode-cdn.net/home/qottmann/Qottmann/Xanadu/Sandbox/~/Qottmann/Xanadu/pennylane/pennylane/interfaces/execution.py:130), in cache_execute..fn(tapes, **kwargs)
    128 def fn(tapes: Sequence[QuantumTape], **kwargs):  # pylint: disable=function-redefined
    129     tapes = [expand_fn(tape) for tape in tapes]
--> 130     return original_fn(tapes, **kwargs)

File [~/anaconda3/envs/pennylane/lib/python3.8/contextlib.py:75](https://vscode-remote+wsl-002bubuntu.vscode-resource.vscode-cdn.net/home/qottmann/Qottmann/Xanadu/Sandbox/~/anaconda3/envs/pennylane/lib/python3.8/contextlib.py:75), in ContextDecorator.__call__..inner(*args, **kwds)
     72 @wraps(func)
     73 def inner(*args, **kwds):
     74     with self._recreate_cm():
---> 75         return func(*args, **kwds)

File [~/Qottmann/Xanadu/pennylane/pennylane/_qubit_device.py:588](https://vscode-remote+wsl-002bubuntu.vscode-resource.vscode-cdn.net/home/qottmann/Qottmann/Xanadu/Sandbox/~/Qottmann/Xanadu/pennylane/pennylane/_qubit_device.py:588), in QubitDevice.batch_execute(self, circuits)
    583 for circuit in circuits:
    584     # we need to reset the device here, else it will
    585     # not start the next computation in the zero state
    586     self.reset()
--> 588     res = self.execute(circuit)
    589     results.append(res)
    591 if self.tracker.active:

File [~/Qottmann/Xanadu/pennylane/pennylane/_qubit_device.py:318](https://vscode-remote+wsl-002bubuntu.vscode-resource.vscode-cdn.net/home/qottmann/Qottmann/Xanadu/Sandbox/~/Qottmann/Xanadu/pennylane/pennylane/_qubit_device.py:318), in QubitDevice.execute(self, circuit, **kwargs)
    315 self.check_validity(circuit.operations, circuit.observables)
    317 # apply all circuit operations
--> 318 self.apply(circuit.operations, rotations=self._get_diagonalizing_gates(circuit), **kwargs)
    320 # generate computational basis samples
    321 if self.shots is not None or circuit.is_sampled:

File [~/Qottmann/Xanadu/pennylane/pennylane/devices/default_qubit.py:274](https://vscode-remote+wsl-002bubuntu.vscode-resource.vscode-cdn.net/home/qottmann/Qottmann/Xanadu/Sandbox/~/Qottmann/Xanadu/pennylane/pennylane/devices/default_qubit.py:274), in DefaultQubit.apply(self, operations, rotations, **kwargs)
    272         self._state = self._apply_parametrized_evolution(self._state, operation)
    273     else:
--> 274         self._state = self._apply_operation(self._state, operation)
    276 # store the pre-rotated state
    277 self._pre_rotated_state = self._state

File [~/Qottmann/Xanadu/pennylane/pennylane/devices/default_qubit.py:322](https://vscode-remote+wsl-002bubuntu.vscode-resource.vscode-cdn.net/home/qottmann/Qottmann/Xanadu/Sandbox/~/Qottmann/Xanadu/pennylane/pennylane/devices/default_qubit.py:322), in DefaultQubit._apply_operation(self, state, operation)
    318 if len(wires) <= 2:
    319     # Einsum is faster for small gates
    320     return self._apply_unitary_einsum(state, matrix, wires)
--> 322 return self._apply_unitary(state, matrix, wires)

File [~/Qottmann/Xanadu/pennylane/pennylane/devices/default_qubit.py:776](https://vscode-remote+wsl-002bubuntu.vscode-resource.vscode-cdn.net/home/qottmann/Qottmann/Xanadu/Sandbox/~/Qottmann/Xanadu/pennylane/pennylane/devices/default_qubit.py:776), in DefaultQubit._apply_unitary(self, state, mat, wires)
    763 r"""Apply multiplication of a matrix to subsystems of the quantum state.
    764 
    765 Args:
   (...)
    773 Note: This function does not support simultaneously broadcasted states and matrices yet.
    774 """
    775 # translate to wire labels used by device
--> 776 device_wires = self.map_wires(wires)
    778 dim = 2 ** len(device_wires)
    779 mat_batch_size = self._get_batch_size(mat, (dim, dim), dim**2)

File [~/Qottmann/Xanadu/pennylane/pennylane/devices/default_qubit.py:225](https://vscode-remote+wsl-002bubuntu.vscode-resource.vscode-cdn.net/home/qottmann/Qottmann/Xanadu/Sandbox/~/Qottmann/Xanadu/pennylane/pennylane/devices/default_qubit.py:225), in DefaultQubit.map_wires(self, wires)
    223     mapped_wires = [self.wire_map[w] for w in wires]
    224 except KeyError as e:
--> 225     raise WireError(
    226         f"Did not find some of the wires {wires.labels} on device with wires {self.wires.labels}."
    227     ) from e
    229 return mapped_wires

WireError: Did not find some of the wires (tensor(0, requires_grad=True), tensor(1, requires_grad=True), tensor(2, requires_grad=True), tensor(3, requires_grad=True)) on device with wires (0, 1, 2, 3).
```


Discussion:
This will happen for any operator that doesn't have special native handling from default.qubit (ie. any operator whose application reaches [this line](https://github.com/PennyLaneAI/pennylane/blob/c0fb9b851fcdf004b943f475cbeae47a86b7d7db/pennylane/devices/default_qubit.py#L313)). For example, `qml.device("default.qubit", wires=1).apply([qml.RX(0.1, np.array(0))])` will raise the same error.

This can be considered an operator using wires that are not on the device (as the error says), because the operator wire includes `tensor(0, requires_grad=True)` which is not the same as `0`. This is a consequence of PennyLane trying to support wire labels of any type (not just ints). We _could_ add custom handling for numpy arrays, but I'm inclined to say that from PennyLane's perspective, the error raised is correct.

If the user ensures the data types of op wires and device wires are consistent, then the error won't occur. ie. use vanilla python lists for operator wires, or initialize the device wires as `np.arange(4)` (but you'd have to change the wire being measured to a numpy value as well if you do that 😄)
Just to jump into the issue quickly with some historical context: this has come up a lot in the past, and as @timmysilv mentions, the core issue is that PL's wire handling is completely arbitrary and _object_ based -- PL will interpret `[np.array(0), 0, tf.Tensor(0)]` as three separate wires on the device!

When this is has come up in the past, we have always considered: should we treat numeric objects differently from non-numeric objects? and it becomes a much deeper discussion. We opted multiple times not to change this as treating numeric objects differently unearths a bunch of other edge cases and unintuitive behaviour.

Perhaps the bigger/deeper discussion is: do we want to change PL's wire handling _itself_? Should we allow arbitrary objects to be wires? The answer here is maybe (cc @trbromley) but this is a much bigger work package 😅 

So in the mean time, I would concur with @timmysilv's solution.
@Qottmann @kshyatt-aws @josh146 , Should we keep this issue open so we keep the reminder of this "known sharp bit", or close it?
Does closing change anything in terms of how well this issue can be found (on here and/or google)? If not, then I think it is fine to close
I think this is an opportunity to improve docs a bit. It won't totally fix it, but this particular example took a second to click for me and I work on the darn thing! I'm going to add a warning about types to the docs [here](https://docs.pennylane.ai/en/stable/introduction/circuits.html#wires), and in the `Wires` docstring (it's under the "Internal" category on the docs page but still), then I will call this issue "complete" and close it
This would be a good candidate for a 'sharp bits' page

Resolving patch:
diff --git a/pennylane/wires.py b/pennylane/wires.py
--- a/pennylane/wires.py
+++ b/pennylane/wires.py
@@ -89,6 +89,12 @@ class Wires(Sequence):
 
     Indexing an instance of this class will return a wire label.
 
+    .. warning::
+
+        In order to support wire labels of any hashable type, integers and 0-d arrays are considered different.
+        For example, running ``qml.RX(1.1, qml.numpy.array(0))`` on a device initialized with ``wires=[0]``
+        will fail because ``qml.numpy.array(0)`` does not exist in the device's wire map.
+
     Args:
          wires (Any): the wire label(s)
     """


Test patch: