Repository: PennyLaneAI/pennylane (python)

Issue:
[BUG] wires initialization issue
### Expected behavior

It would expect the following script to return `True`

`Wires(["a"]) == Wires([Wires("a")])`

i.e. detect if there are nested wires, and not accumulate them as different objects



### Actual behavior

Currently, they are different wires, which generates unwanted situations such as:

![image](https://github.com/user-attachments/assets/e7d7135f-4d65-47d0-a05f-c097f61f259a)


### Additional information

_No response_

### Source code

_No response_

### Tracebacks

_No response_

### System information

```shell
Platform info:           macOS-14.6.1-arm64-arm-64bit
Python version:          3.11.10
Numpy version:           1.26.4
Scipy version:           1.13.0
Installed devices:
- default.clifford (PennyLane-0.40.0.dev27)
- default.gaussian (PennyLane-0.40.0.dev27)
- default.mixed (PennyLane-0.40.0.dev27)
- default.qubit (PennyLane-0.40.0.dev27)
- default.qutrit (PennyLane-0.40.0.dev27)
- default.qutrit.mixed (PennyLane-0.40.0.dev27)
- default.tensor (PennyLane-0.40.0.dev27)
- null.qubit (PennyLane-0.40.0.dev27)
- reference.qubit (PennyLane-0.40.0.dev27)
- lightning.kokkos (PennyLane_Lightning_Kokkos-0.40.0.dev14)
- lightning.qubit (PennyLane_Lightning-0.40.0.dev14)
```


### Existing GitHub issues

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

Pull request description:
Fixes `Wires` objects as wire labels bug
**Context:**

Prior to this PR we had the following behaviour,
```python
a = qml.wires.Wires("a")
b = qml.wires.Wires("b")
wires = [a,b,"c"]

>>> print(qml.wires.Wires(wires))
Wires([Wires(['a']), Wires(['b']), 'c'])

>>> print(qml.wires.Wires([0, 1, 2, 3, qml.wires.Wires([4, 5]), None]))
Wires([0, 1, 2, 3, Wires([4, 5]), None])

>>> print(qml.wires.Wires([qml.wires.Wires([(0,0), (0,1)])]))
Wires([Wires([(0, 0), (0, 1)])])
```

**Description of the Change:**

Add handling to `_process` that dissolves any `Wires` objects that survived to that point. This results in improved behaviour like,
```python
a = qml.wires.Wires("a")
b = qml.wires.Wires("b")
wires = [a,b,"c"]

>>> print(qml.wires.Wires(wires))
Wires(['a', 'b', 'c'])

>>> print(qml.wires.Wires([0, 1, 2, 3, qml.wires.Wires([4, 5]), None]))
Wires([0, 1, 2, 3, 4, 5, None])

>>> print(qml.wires.Wires([qml.wires.Wires([(0,0), (0,1)])]))
Wires([(0, 0), (0, 1)])
```

**Benefits:**

Better visualization of wire objects.

**Possible Drawbacks:** Wire objects will look slightly different depending on your workflow.

**Related GitHub Issues:** Fixes #6669 

[sc-79749]


Patch:
diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md
index 83d74e1f5..1e56914c5 100644
--- a/doc/releases/changelog-dev.md
+++ b/doc/releases/changelog-dev.md
@@ -282,6 +282,10 @@
 
 <h3>Bug fixes 🐛</h3>
 
+* Fixed `qml.wires.Wires` initialization to disallow `Wires` objects as wires labels.
+  Now, `Wires` is idempotent, e.g. `Wires([Wires([0]), Wires([1])])==Wires([0, 1])`.
+  [(#6933)](https://github.com/PennyLaneAI/pennylane/pull/6933)
+
 * `qml.capture.PlxprInterpreter` now correctly handles propagation of constants when interpreting higher-order primitives
   [(#6913)](https://github.com/PennyLaneAI/pennylane/pull/6913)
 
diff --git a/pennylane/wires.py b/pennylane/wires.py
index 14b517467..ba1fc175a 100644
--- a/pennylane/wires.py
+++ b/pennylane/wires.py
@@ -93,7 +93,8 @@ def _process(wires):
     if len(set_of_wires) != len(tuple_of_wires):
         raise WireError(f"Wires must be unique; got {wires}.")
 
-    return tuple_of_wires
+    # required to make `Wires` object idempotent
+    return tuple(itertools.chain(*(_flatten_wires_object(x) for x in tuple_of_wires)))
 
 
 class Wires(Sequence):
@@ -120,7 +121,7 @@ class Wires(Sequence):
     """
 
     def _flatten(self):
-        """Serialize Wires into a flattened representation according to the PyTree convension."""
+        """Serialize Wires into a flattened representation according to the PyTree convention."""
         return self._labels, ()
 
     @classmethod
@@ -731,5 +732,13 @@ class Wires(Sequence):
 
 WiresLike = Union[Wires, Iterable[Hashable], Hashable]
 
+
+def _flatten_wires_object(wire_label):
+    """Converts the input to a tuple of wire labels."""
+    if isinstance(wire_label, Wires):
+        return wire_label.labels
+    return [wire_label]
+
+
 # Register Wires as a PyTree-serializable class
 register_pytree(Wires, Wires._flatten, Wires._unflatten)  # pylint: disable=protected-access


Test patch:
diff --git a/tests/test_wires.py b/tests/test_wires.py
index 0ae3e85b6..d80f5e5cd 100644
--- a/tests/test_wires.py
+++ b/tests/test_wires.py
@@ -34,6 +34,12 @@ else:
 class TestWires:
     """Tests for the ``Wires`` class."""
 
+    def test_wires_object_as_label(self):
+        """Tests that a Wires object can be used as a label for another Wires object."""
+        assert Wires([0, 1]) == Wires([Wires([0]), Wires([1])])
+        assert Wires(["a", "b", 1]) == Wires([Wires(["a", "b"]), Wires([1])])
+        assert Wires([Wires([(0, 0), (0, 1)])]) == Wires([(0, 0), (0, 1)])
+
     def test_error_if_wires_none(self):
         """Tests that a TypeError is raised if None is given as wires."""
         with pytest.raises(TypeError, match="Must specify a set of wires."):
@@ -74,7 +80,7 @@ class TestWires:
         """Tests that a Wires object can be created from a list of Wires."""
 
         wires = Wires([Wires([0]), Wires([1]), Wires([2])])
-        assert wires.labels == (Wires([0]), Wires([1]), Wires([2]))
+        assert wires.labels == (0, 1, 2)
 
     @pytest.mark.parametrize(
         "iterable", [[1, 0, 4], ["a", "b", "c"], [0, 1, None], ["a", 1, "ancilla"]]
@@ -148,7 +154,7 @@ class TestWires:
         wires = Wires([0, 1, 2, 3, Wires([4, 5]), None])
 
         assert 0 in wires
-        assert Wires([4, 5]) in wires
+        assert Wires([4, 5]) not in wires
         assert None in wires
         assert Wires([1]) not in wires
         assert Wires([0, 3]) not in wires
@@ -170,7 +176,7 @@ class TestWires:
 
         assert not wires.contains_wires(0)  # wrong type
         assert not wires.contains_wires([0, 1])  # wrong type
-        assert not wires.contains_wires(
+        assert wires.contains_wires(
             Wires([4, 5])
         )  # looks up 4 and 5 in wires, which are not present