Repository: PennyLaneAI/pennylane

Issue:
[BUG] Broadcasting multi-qubit gates in tape mode produces new duplicate wires
#### Issue description

When using the broadcast function in tape mode, broadcasting multi-qubit gates seems to create new wires that have type tensor.

* *Expected behavior:* Broadcasted gates should be applied on the device wires.

* *Actual behavior:* Additional wires are created that have type tensor.

* *Reproduces how often:* Whenever multi-qubit gates are included in the broadcast. 

* *System information:* 
(Using dev version from 2020-01-24)

```
Name: PennyLane
Version: 0.14.0.dev0
Summary: PennyLane is a Python quantum machine learning library by Xanadu Inc.
Home-page: https://github.com/XanaduAI/pennylane
Author: None
Author-email: None
License: Apache License 2.0
Location: /home/olivia/Software/anaconda3/envs/xanadu/lib/python3.8/site-packages
Requires: scipy, networkx, semantic-version, autograd, toml, numpy, appdirs
Required-by: PennyLane-SF, pennylane-qulacs, PennyLane-qsharp, PennyLane-qiskit, PennyLane-Qchem, PennyLane-Forest, PennyLane-Cirq
Platform info:           Linux-5.4.0-64-generic-x86_64-with-glibc2.10
Python version:          3.8.5
Numpy version:           1.19.5
Scipy version:           1.4.1
Installed devices:
- default.gaussian (PennyLane-0.14.0.dev0)
- default.mixed (PennyLane-0.14.0.dev0)
- default.qubit (PennyLane-0.14.0.dev0)
- default.qubit.autograd (PennyLane-0.14.0.dev0)
- default.qubit.jax (PennyLane-0.14.0.dev0)
- default.qubit.tf (PennyLane-0.14.0.dev0)
- default.tensor (PennyLane-0.14.0.dev0)
- default.tensor.tf (PennyLane-0.14.0.dev0)
- strawberryfields.fock (PennyLane-SF-0.11.0)
- strawberryfields.gaussian (PennyLane-SF-0.11.0)
- strawberryfields.remote (PennyLane-SF-0.11.0)
- qulacs.simulator (pennylane-qulacs-0.12.0)
- microsoft.QuantumSimulator (PennyLane-qsharp-0.8.0)
- qiskit.aer (PennyLane-qiskit-0.13.0.dev0)
- qiskit.basicaer (PennyLane-qiskit-0.13.0.dev0)
- qiskit.ibmq (PennyLane-qiskit-0.13.0.dev0)
- forest.numpy_wavefunction (PennyLane-Forest-0.11.0)
- forest.qpu (PennyLane-Forest-0.11.0)
- forest.qvm (PennyLane-Forest-0.11.0)
- forest.wavefunction (PennyLane-Forest-0.11.0)
- cirq.mixedsimulator (PennyLane-Cirq-0.13.0)
- cirq.pasqal (PennyLane-Cirq-0.13.0)
- cirq.qsim (PennyLane-Cirq-0.13.0)
- cirq.qsimh (PennyLane-Cirq-0.13.0)
- cirq.simulator (PennyLane-Cirq-0.13.0)


```

#### Source code and tracebacks

```
import pennylane as qml
import numpy as np
qml.enable_tape()

N_states = 4
params = np.random.uniform(low=-np.pi/2, high=np.pi/2, size=(N_states))

def circuit(params, wires):
    qml.broadcast(qml.RY, wires, pattern='single', parameters=params)
    return qml.expval(qml.PauliZ(0))

dev = qml.device("default.qubit", wires=N_states)
my_qnode = qml.QNode(circuit, dev)
my_qnode(params, range(N_states))
print(my_qnode.draw())
```
This produces the expected output:

```
 0: ──RY(-0.195)──┤ ⟨Z⟩ 
 1: ──RY(-0.18)───┤     
 2: ──RY(-0.779)──┤     
 3: ──RY(0.404)───┤     
```

However altering the function to include a multi-qubit gate, like so:
```
def circuit(params, wires):
    qml.broadcast(qml.RY, wires, pattern='single', parameters=params)
    qml.broadcast(qml.CRY, wires, pattern='ring', parameters=params)
    return qml.expval(qml.PauliZ(0))
```
gives the following output after running and drawing the tape:
```
 0: ──RY(-0.21)──╭C─────────────────────────────────╭RY(1.14)──┤ ⟨Z⟩ 
 1: ──RY(1.38)───╰RY(-0.21)──╭C─────────────────────│──────────┤     
 2: ──RY(-1.05)──────────────╰RY(1.38)──╭C──────────│──────────┤     
 3: ──RY(1.14)──────────────────────────╰RY(-1.05)──╰C─────────┤     
 0: ──RY(-0.21)───C──────────────────────────────────RY(1.14)──┤ ⟨Z⟩ 
 1: ──RY(1.38)────RY(-0.21)───C────────────────────────────────┤     
 2: ──RY(-1.05)───────────────RY(1.38)───C─────────────────────┤     
 3: ──RY(1.14)───────────────────────────RY(-1.05)───C─────────┤     

```
It doesn't seem to be an issue just in the tape drawer, because looking at the tape operations shows that the CRY are applied to different wires:
```
[RY(tensor(-0.20956558, requires_grad=True), wires=[0]),
 RY(tensor(1.37704322, requires_grad=True), wires=[1]),
 RY(tensor(-1.05444866, requires_grad=True), wires=[2]),
 RY(tensor(1.14254618, requires_grad=True), wires=[3]),
 CRY(tensor(-0.20956558, requires_grad=True), wires=[tensor(0, requires_grad=True), tensor(1, requires_grad=True)]),
 CRY(tensor(1.37704322, requires_grad=True), wires=[tensor(1, requires_grad=True), tensor(2, requires_grad=True)]),
 CRY(tensor(-1.05444866, requires_grad=True), wires=[tensor(2, requires_grad=True), tensor(3, requires_grad=True)]),
 CRY(tensor(1.14254618, requires_grad=True), wires=[tensor(3, requires_grad=True), tensor(0, requires_grad=True)])]
```

Discussion:
codecov[bot]: # [Codecov](https://codecov.io/gh/PennyLaneAI/pennylane/pull/1039?src=pr&el=h1) Report
> Merging [#1039](https://codecov.io/gh/PennyLaneAI/pennylane/pull/1039?src=pr&el=desc) (6a0cbf6) into [master](https://codecov.io/gh/PennyLaneAI/pennylane/commit/86727f5d1389fee4bcc247160d3ed99406898860?el=desc) (86727f5) will **decrease** coverage by `0.02%`.
> The diff coverage is `100.00%`.

[![Impacted file tree graph](https://codecov.io/gh/PennyLaneAI/pennylane/pull/1039/graphs/tree.svg?width=650&height=150&src=pr&token=9S3YzlUgGk)](https://codecov.io/gh/PennyLaneAI/pennylane/pull/1039?src=pr&el=tree)

```diff
@@            Coverage Diff             @@
##           master    #1039      +/-   ##
==========================================
- Coverage   97.93%   97.90%   -0.03%     
==========================================
  Files         153      153              
  Lines       11421    11423       +2     
==========================================
- Hits        11185    11184       -1     
- Misses        236      239       +3     
```


| [Impacted Files](https://codecov.io/gh/PennyLaneAI/pennylane/pull/1039?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [pennylane/wires.py](https://codecov.io/gh/PennyLaneAI/pennylane/pull/1039/diff?src=pr&el=tree#diff-cGVubnlsYW5lL3dpcmVzLnB5) | `96.75% <100.00%> (-1.94%)` | :arrow_down: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/PennyLaneAI/pennylane/pull/1039?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/PennyLaneAI/pennylane/pull/1039?src=pr&el=footer). Last update [86727f5...6a0cbf6](https://codecov.io/gh/PennyLaneAI/pennylane/pull/1039?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).

mariaschuld: Ignore codecov error @Thenerdstation, the number of misses did not increase (only the number of hit lines decreased from deleting code). Ready for review :) 
albi3ro: I looked over everything, and it all looked good to me :)
Hooray for efficiency!

Patch:
diff --git a/.github/CHANGELOG.md b/.github/CHANGELOG.md
index 851997519..d41bba9d8 100644
--- a/.github/CHANGELOG.md
+++ b/.github/CHANGELOG.md
@@ -368,14 +368,37 @@
   required to fully support end-to-end differentiable Mottonen and Amplitude embedding.
   [(#922)](https://github.com/PennyLaneAI/pennylane/pull/922)
 
-* * Several improvements have been made to the `Wires` class to reduce overhead:
+* Several improvements have been made to the `Wires` class to reduce overhead and simplify the logic 
+  of how wire labels are interpreted:
+  [(#1019)](https://github.com/PennyLaneAI/pennylane/pull/1019)
+  [(#1010)](https://github.com/PennyLaneAI/pennylane/pull/1010)
+  [(#1005)](https://github.com/PennyLaneAI/pennylane/pull/1005)
+  [(#983)](https://github.com/PennyLaneAI/pennylane/pull/983)
   [(#967)](https://github.com/PennyLaneAI/pennylane/pull/967)
 
-  - Moves the check for uniqueness of wires from `Wires` instantiation to
+  - If the input `wires` to a wires class instantiation `Wires(wires)` can be iterated over, 
+    its elements are interpreted as wire labels. Otherwise, `wires` is interpreted as a single wire label.
+    The only exception to this are strings, which are always interpreted as a single 
+    wire label, so users can address wires with labels such as `"ancilla"`. 
+     
+  - Any type can now be a wire label as long as it is hashable. The hash is used to establish
+    the uniqueness of two labels.
+    
+  - Indexing wires objects now returns a label, instead of a new `Wires` object. For example:
+    
+    ```pycon
+    >>> w = Wires([0, 1, 2])
+    >>> w[1]
+    >>> 1
+    ```
+     
+  - The check for uniqueness of wires moved from `Wires` instantiation to
     the `qml.wires._process` function in order to reduce overhead from repeated
     creation of `Wires` instances.
   
-  - Skips calling of Wires on Wires instances on `Operation` instantiation.
+  - Calls to the `Wires` class are substantially reduced, for example by avoiding to call 
+    Wires on Wires instances on `Operation` instantiation, and by using labels instead of 
+    `Wires` objects inside the default qubit device.
   
 * Adds the `PauliRot` generator to the `qml.operation` module. This 
   generator is required to construct the metric tensor. 
diff --git a/doc/code/qml_wires.rst b/doc/code/qml_wires.rst
index 38bfda3d2..6c23685e2 100644
--- a/doc/code/qml_wires.rst
+++ b/doc/code/qml_wires.rst
@@ -11,4 +11,4 @@ qml.wires
 .. automodapi:: pennylane.wires
     :no-heading:
     :no-inheritance-diagram:
-    :skip: Number, Sequence, Iterable
+    :skip: Sequence, Iterable
diff --git a/pennylane/devices/default_qubit.py b/pennylane/devices/default_qubit.py
index e709bd4d3..f9bfc8d33 100644
--- a/pennylane/devices/default_qubit.py
+++ b/pennylane/devices/default_qubit.py
@@ -27,6 +27,7 @@ import numpy as np
 
 from pennylane import QubitDevice, DeviceError, QubitStateVector, BasisState
 from pennylane.operation import DiagonalOperation
+from pennylane.wires import WireError
 
 ABC_ARRAY = np.array(list(ABC))
 
@@ -149,7 +150,15 @@ class DefaultQubit(QubitDevice):
     def map_wires(self, wires):
         # temporarily overwrite this method to bypass
         # wire map that produces Wires objects
-        mapped_wires = [self.wire_map[w] for w in wires]
+        try:
+            mapped_wires = [self.wire_map[w] for w in wires]
+        except KeyError as e:
+            raise WireError(
+                "Did not find some of the wires {} on device with wires {}.".format(
+                    wires.labels, self.wires.labels
+                )
+            ) from e
+
         return mapped_wires
 
     def define_wire_map(self, wires):
diff --git a/pennylane/devices/default_qubit_autograd.py b/pennylane/devices/default_qubit_autograd.py
index 5d94ca872..d5cb1883a 100644
--- a/pennylane/devices/default_qubit_autograd.py
+++ b/pennylane/devices/default_qubit_autograd.py
@@ -16,8 +16,6 @@ reference plugin.
 """
 from pennylane.operation import DiagonalOperation
 from pennylane import numpy as np
-from pennylane.wires import Wires
-from pennylane.numpy.tensor import tensor
 
 from pennylane.devices import DefaultQubit
 from pennylane.devices import autograd_ops
@@ -169,17 +167,3 @@ class DefaultQubitAutograd(DefaultQubit):
             return unitary.eigvals
 
         return unitary.matrix
-
-    def map_wires(self, wires):
-        """Map the wire labels of wires using this device's wire map.
-
-        Args:
-            wires (Wires): wires whose labels we want to map to the device's internal labelling scheme
-
-        Returns:
-            Wires: wires with new labels
-        """
-        wires = Wires([w.item() if isinstance(w, tensor) else w for w in wires])
-        mapped_wires = super().map_wires(wires)
-
-        return mapped_wires
diff --git a/pennylane/devices/default_qubit_tf.py b/pennylane/devices/default_qubit_tf.py
index 1d2287010..7be299835 100644
--- a/pennylane/devices/default_qubit_tf.py
+++ b/pennylane/devices/default_qubit_tf.py
@@ -18,8 +18,6 @@ import numpy as np
 import semantic_version
 
 from pennylane.operation import DiagonalOperation
-from pennylane.wires import Wires, WireError
-from pennylane.numpy.tensor import tensor
 
 try:
     import tensorflow as tf
@@ -225,17 +223,3 @@ class DefaultQubitTF(DefaultQubit):
             return unitary.eigvals
 
         return unitary.matrix
-
-    def map_wires(self, wires):
-        """Map the wire labels of wires using this device's wire map.
-
-        Args:
-            wires (Wires): wires whose labels we want to map to the device's internal labelling scheme
-
-        Returns:
-            Wires: wires with new labels
-        """
-        wires = Wires([w.item() if isinstance(w, tensor) else w for w in wires])
-        mapped_wires = super().map_wires(wires)
-
-        return mapped_wires
diff --git a/pennylane/templates/broadcast.py b/pennylane/templates/broadcast.py
index b77094804..433079c46 100644
--- a/pennylane/templates/broadcast.py
+++ b/pennylane/templates/broadcast.py
@@ -65,7 +65,7 @@ def wires_all_to_all(wires):
 
 # define wire sequences for patterns
 PATTERN_TO_WIRES = {
-    "single": lambda wires: [wires[i] for i in range(len(wires))],
+    "single": lambda wires: [wires.subset([i]) for i in range(len(wires))],
     "double": lambda wires: [wires.subset([i, i + 1]) for i in range(0, len(wires) - 1, 2)],
     "double_odd": lambda wires: [wires.subset([i, i + 1]) for i in range(1, len(wires) - 1, 2)],
     "chain": lambda wires: [wires.subset([i, i + 1]) for i in range(len(wires) - 1)],
diff --git a/pennylane/wires.py b/pennylane/wires.py
index 5e3806eca..07960c1ad 100644
--- a/pennylane/wires.py
+++ b/pennylane/wires.py
@@ -16,7 +16,6 @@ This module contains the :class:`Wires` class, which takes care of wire bookkeep
 """
 from collections.abc import Sequence, Iterable
 import functools
-from numbers import Number
 import numpy as np
 
 
@@ -25,55 +24,73 @@ class WireError(Exception):
 
 
 def _process(wires):
-    """Converts the input to a tuple of numbers or strings."""
+    """Converts the input to a tuple of wire labels.
 
-    if isinstance(wires, (Number, str)):
-        # interpret as a single wire
-        return (wires,)
+    If `wires` can be iterated over, its elements are interpreted as wire labels
+    and turned into a tuple. Otherwise, `wires` is interpreted as a single wire label.
+
+    The only exception to this are strings, which are always interpreted as a single
+    wire label, so users can address wires with labels such as `"ancilla"`.
 
-    if isinstance(wires, Wires):
-        # if input is already a Wires object, just return its wire tuple
-        return wires.labels
+    Any type can be a wire label, as long as it is hashable. We need this to establish
+    the uniqueness of two labels. For example, `0` and `0.` are interpreted as
+    the same wire label because `hash(0.) == hash(0)` evaluates to true.
+
+    Note that opposed to numpy arrays, `pennylane.numpy` 0-dim array are hashable.
+    """
 
-    if getattr(wires, "shape", None) == tuple():
-        # Scalar NumPy array
-        return (wires.item(),)
+    if isinstance(wires, str):
+        # Interpret string as a non-iterable object.
+        # This is the only exception to the logic
+        # of considering the elements of iterables as wire labels.
+        wires = [wires]
 
-    if isinstance(wires, Iterable):
+    try:
+        # Use tuple conversion as a check for whether `wires` can be iterated over.
+        # Note, this is not the same as `isinstance(wires, Iterable)` which would
+        # pass for 0-dim numpy arrays that cannot be iterated over.
         tuple_of_wires = tuple(wires)
-        try:  # We need the set for the uniqueness check, so we can use it for hashability check
-            set_of_wires = set(wires)
+    except TypeError:
+        # if not iterable, interpret as single wire label
+        try:
+            hash(wires)
         except TypeError as e:
-            # Make sure it really was a hashability issue
+            # if object is not hashable, cannot identify unique wires
             if str(e).startswith("unhashable"):
-                raise WireError("Wires must be hashable; got {}.".format(wires)) from e
+                raise WireError(
+                    "Wires must be hashable; got object of type {}.".format(type(wires))
+                ) from e
+        return (wires,)
 
-        if len(set_of_wires) != len(tuple_of_wires):
-            raise WireError("Wires must be unique; got {}.".format(wires))
+    try:
+        # We need the set for the uniqueness check,
+        # so we can use it for hashability check of iterables.
+        set_of_wires = set(wires)
+    except TypeError as e:
+        if str(e).startswith("unhashable"):
+            raise WireError("Wires must be hashable; got {}.".format(wires)) from e
 
-        return tuple_of_wires
+    if len(set_of_wires) != len(tuple_of_wires):
+        raise WireError("Wires must be unique; got {}.".format(wires))
 
-    raise WireError(
-        "Wires must be represented by a number or string; got {} of type {}.".format(
-            wires, type(wires)
-        )
-    )
+    return tuple_of_wires
 
 
 class Wires(Sequence):
     r"""
-    A bookkeeping class for wires, which are ordered collections of unique objects. The :math:`i\mathrm{th}` object
-    addresses the :math:`i\mathrm{th}` quantum subsystem.
+    A bookkeeping class for wires, which are ordered collections of unique objects.
+
+    If the input `wires` can be iterated over, it is interpreted as a sequence of wire labels that have to be
+    unique and hashable. Else it is interpreted as a single wire label that has to be hashable. The
+    only exception are strings which are interpreted as wire labels.
 
-    There is no conceptual difference between registers of multiple wires and single wires,
-    which are just wire registers of length one.
+    The hash function of a wire label is considered the source of truth when deciding whether
+    two wire labels are the same or not.
 
-    Indexing and slicing this sequence will return another ``Wires`` object.
+    Indexing an instance of this class will return a wire label.
 
     Args:
-         wires (Iterable[Number,str], Number): If iterable, interpreted as an ordered collection of unique objects
-            representing wires. If a Number, the input is converted into an iterable of a single entry,
-            and hence interpreted as a single wire.
+         wires (Any): the wire label(s)
     """
 
     def __init__(self, wires, _override=False):


Test patch:
diff --git a/tests/devices/test_default_qubit.py b/tests/devices/test_default_qubit.py
index 640ee89b5..9031d228a 100644
--- a/tests/devices/test_default_qubit.py
+++ b/tests/devices/test_default_qubit.py
@@ -22,7 +22,7 @@ import pytest
 import pennylane as qml
 from pennylane import numpy as np, DeviceError
 from pennylane.devices.default_qubit import _get_slice, DefaultQubit
-from pennylane.operation import Operation
+from pennylane.wires import WireError
 
 U = np.array(
     [
@@ -1756,6 +1756,19 @@ class TestWiresIntegration:
 
         assert np.allclose(circuit1(), circuit2(), tol)
 
+    def test_wires_not_found_exception(self):
+        """Tests that an exception is raised when wires not present on the device are adressed. """
+        dev = qml.device("default.qubit", wires=['a', 'b'])
+
+        with qml.tape.QuantumTape() as tape:
+            qml.RX(0.5, wires='c')
+
+        with pytest.raises(
+                WireError,
+                match="Did not find some of the wires"
+        ):
+            dev.execute(tape)
+
 
 class TestGetSlice:
     """Tests for the _get_slice function."""
diff --git a/tests/interfaces/test_autograd.py b/tests/interfaces/test_autograd.py
index 6be821511..5e90ac90f 100644
--- a/tests/interfaces/test_autograd.py
+++ b/tests/interfaces/test_autograd.py
@@ -893,7 +893,8 @@ class TestParameterHandlingIntegration:
     def test_non_diff_wires_argument(self, w, expected_res, expected_grad, tol):
         """Test that passing wires as a non-differentiable positional
         argument works correctly."""
-        dev = qml.device("default.qubit", wires=2)
+        dev = qml.device("default.qubit", wires=[qml.numpy.array(0, requires_grad=False),
+                                                 qml.numpy.array(1, requires_grad=False)])
 
         @qml.qnode(dev, interface="autograd")
         def circuit(wires, params):
@@ -904,7 +905,7 @@ class TestParameterHandlingIntegration:
             qml.CNOT(wires=[wires[1], wires[0]])
             qml.RX(params[0], wires=wires[0])
             qml.RY(params[1], wires=wires[1])
-            return qml.expval(qml.PauliZ(0))
+            return qml.expval(qml.PauliZ(qml.numpy.array(0, requires_grad=False)))
 
         params = qml.numpy.array([0.6, 0.2])
         wires = qml.numpy.array(w, requires_grad=False)
diff --git a/tests/test_wires.py b/tests/test_wires.py
index 8195177c6..4ef479792 100644
--- a/tests/test_wires.py
+++ b/tests/test_wires.py
@@ -79,14 +79,15 @@ class TestWires:
         wires = Wires(wire)
         assert wires.labels == (wire,)
 
-    @pytest.mark.parametrize("iterable", [None,
-                                          qml.RX])
-    def test_error_for_incorrect_wire_types(self, iterable):
-        """Tests that a Wires object cannot be created from wire types that are not allowed."""
+    @pytest.mark.parametrize("input", [[np.array([0, 1, 2]), np.array([3, 4])],
+                                       [[0, 1, 2], [3, 4]],
+                                       np.array(0.)])
+    def test_error_for_incorrect_wire_types(self, input):
+        """Tests that a Wires object cannot be created from unhashable objects such as np arrays or lists."""
+
+        with pytest.raises(WireError, match="Wires must be hashable"):
+            Wires(input)
 
-        with pytest.raises(WireError, match="Wires must be represented"):
-            Wires(iterable)
-            
     @pytest.mark.parametrize("iterable", [np.array([4, 1, 1, 3]),
                                           [4, 1, 1, 3],
                                           (4, 1, 1, 3),