Repository: Qiskit/qiskit

Issue:
Disjoint handling in transpiler fails to rejoin barriers
### Environment

- **Qiskit Terra version**: `main` @ ce02f06
- **Python version**: 3.10
- **Operating system**: macOS


### What is happening?

The preset passmangers for `transpile` are failing to correctly reconstruct barriers temporarily inserted during the compiler routing, which is leaking out invalid `UUID` objects inside the `label` field of those barriers.

### How can we reproduce the issue?

Given a set up of:

```python
from qiskit import transpile, QuantumCircuit
from qiskit.transpiler import CouplingMap

cm = CouplingMap([
    (0, 1),
    (1, 2),
    #
    (3, 4),
])
qc = QuantumCircuit(cm.size(), cm.size())
qc.cx(0, 1)
qc.cx(1, 2)
qc.cx(2, 0)
#
qc.cx(3, 4)
qc.measure(qc.qubits, qc.clbits)

out = transpile(qc, coupling_map=cm, seed_transpiler=0)
```

`out` contains `Barrier` instructions whose `label` attributes are not strings (they're `UUID` objects) and shouldn't exist at all, because the input circuit doesn't have any:

```python
for i, ins in enumerate(out.data):
    if ins.operation.name == "barrier":
        print(i, repr(ins.operation.label))
        # Coerce to string so I can draw the circuit after.
        ins.operation.label = str(ins.operation.label)
```
```text
8 UUID('31558989-c21a-4e5c-b014-01639295df7b')
10 UUID('31558989-c21a-4e5c-b014-01639295df7b')
12 UUID('31558989-c21a-4e5c-b014-01639295df7b')
14 UUID('31558989-c21a-4e5c-b014-01639295df7b')
15 UUID('31558989-c21a-4e5c-b014-01639295df7b')
```

I can't draw the circuit because the visualiser will explode, as will QPY (how this issue was detected).  If I coerce those particular labels just to be string instances, the circuit looks like this:
```text
                                                    ┌───┐                          ┌───┐                  31558989-c21a-4e5c-b014-01639295df7b ┌─┐
q_2 -> 0 ───────────────────────X───────────────────┤ H ├───────■──────────────────┤ H ├───────────────────────────────────░───────────────────┤M├───
                                │                   └───┘       │                  └───┘                                   ░                   └╥┘
                                │                   ┌───┐     ┌─┴─┐                ┌───┐                  31558989-c21a-4e5c-b014-01639295df7b  ║ ┌─┐
q_0 -> 1 ──■────────────────────X───────────────────┤ H ├──■──┤ X ├────────────────┤ H ├───────────────────────────────────░────────────────────╫─┤M├
           │                                        └───┘  │  └───┘                └───┘                                   ░                    ║ └╥┘
         ┌─┴─┐                ┌───┐                      ┌─┴─┐┌───┐ 31558989-c21a-4e5c-b014-01639295df7b                  ┌─┐                   ║  ║
q_1 -> 2 ┤ X ├────────────────┤ H ├──────────────────────┤ X ├┤ H ├──────────────────░────────────────────────────────────┤M├───────────────────╫──╫─
         └───┘                └───┘                      └───┘└───┘                  ░                                    └╥┘                   ║  ║
               31558989-c21a-4e5c-b014-01639295df7b  ┌─┐                                                                   ║                    ║  ║
q_3 -> 3 ──■────────────────────░────────────────────┤M├───────────────────────────────────────────────────────────────────╫────────────────────╫──╫─
           │                    ░                    └╥┘                                                                   ║                    ║  ║
         ┌─┴─┐ 31558989-c21a-4e5c-b014-01639295df7b   ║   ┌─┐                                                              ║                    ║  ║
q_4 -> 4 ┤ X ├──────────────────░─────────────────────╫───┤M├──────────────────────────────────────────────────────────────╫────────────────────╫──╫─
         └───┘                  ░                     ║   └╥┘                                                              ║                    ║  ║
    c: 5/═════════════════════════════════════════════╩════╩═══════════════════════════════════════════════════════════════╩════════════════════╩══╩═
                                                      3    4                                                               1                    0  2
```
so it's clear the temporary barrier we insert before measures isn't being recombined and then deleted.


### What should happen?

* The temporary barrier should have been recombined after the disjoint handling
* The temporary barrier should have been removed before the circuit exited
* We probably shouldn't use a literal `UUID` in the `Barrier.label` field since that's supposed to be constrained to be a string (everything except `Barrier` seems to enforce that, too).

### Any suggestions?

_No response_


Discussion:
So the root of the issue here is a shared state issue. When `SabreLayout` (or other layout passes too) calls `qiskit.transpiler.passes.layout.disjoint_utils.run_pass_over_connected_components` it passes the input dag directly and when there is >1 connected component the first thing as part of it does is split the barriers on the dag it receives in place to enable separating the circuit into it's connected components. This is modifying the input dag which when there is more > 1 connected component gets returned as is because we need to run routing separately. This is causing the barrier to leak out with the uuid label and that barrier isn't getting removed (from #10323) because the label changed. 

There are two paths to fixing this, the first is just making a deepcopy of the dag so that there isn't a shared state when we split the barriers which will preserve the original input dag.  The other option is to run `disjoint_utils.combine_barriers(retain_uuid=False)` before returning the input dag when there is more than one connected component to reverse the barrier splitting (this will also involve fixing the uuid piece to work with existing labels).

Resolving patch:
diff --git a/qiskit/transpiler/passes/layout/disjoint_utils.py b/qiskit/transpiler/passes/layout/disjoint_utils.py
--- a/qiskit/transpiler/passes/layout/disjoint_utils.py
+++ b/qiskit/transpiler/passes/layout/disjoint_utils.py
@@ -107,7 +107,10 @@ def split_barriers(dag: DAGCircuit):
         num_qubits = len(node.qargs)
         if num_qubits == 1:
             continue
-        barrier_uuid = uuid.uuid4()
+        if node.op.label:
+            barrier_uuid = f"{node.op.label}_uuid={uuid.uuid4()}"
+        else:
+            barrier_uuid = f"_none_uuid={uuid.uuid4()}"
         split_dag = DAGCircuit()
         split_dag.add_qubits([Qubit() for _ in range(num_qubits)])
         for i in range(num_qubits):
@@ -124,10 +127,13 @@ def combine_barriers(dag: DAGCircuit, retain_uuid: bool = True):
     qubit_indices = {bit: index for index, bit in enumerate(dag.qubits)}
     uuid_map: dict[uuid.UUID, DAGOpNode] = {}
     for node in dag.op_nodes(Barrier):
-        if isinstance(node.op.label, uuid.UUID):
-            barrier_uuid = node.op.label
+        if node.op.label:
+            if "_uuid=" in node.op.label:
+                barrier_uuid = node.op.label
+            else:
+                continue
             if barrier_uuid in uuid_map:
-                other_node = uuid_map[node.op.label]
+                other_node = uuid_map[barrier_uuid]
                 num_qubits = len(other_node.qargs) + len(node.qargs)
                 new_op = Barrier(num_qubits, label=barrier_uuid)
                 new_node = dag.replace_block_with_op([node, other_node], new_op, qubit_indices)
@@ -136,8 +142,11 @@ def combine_barriers(dag: DAGCircuit, retain_uuid: bool = True):
                 uuid_map[barrier_uuid] = node
     if not retain_uuid:
         for node in dag.op_nodes(Barrier):
-            if isinstance(node.op.label, uuid.UUID):
+            if isinstance(node.op.label, str) and node.op.label.startswith("_none_uuid="):
                 node.op.label = None
+            elif isinstance(node.op.label, str) and "_uuid=" in node.op.label:
+                original_label = "_uuid=".join(node.op.label.split("_uuid=")[:-1])
+                node.op.label = original_label
 
 
 def require_layout_isolated_to_component(
@@ -202,4 +211,7 @@ def separate_dag(dag: DAGCircuit) -> List[DAGCircuit]:
         new_dag.remove_clbits(*idle_clbits)
         combine_barriers(new_dag)
         decomposed_dags.append(new_dag)
+    # Reverse split barriers on input dag to avoid leaking out internal transformations as
+    # part of splitting
+    combine_barriers(dag, retain_uuid=False)
     return decomposed_dags


Test patch:
diff --git a/test/python/compiler/test_transpiler.py b/test/python/compiler/test_transpiler.py
--- a/test/python/compiler/test_transpiler.py
+++ b/test/python/compiler/test_transpiler.py
@@ -3260,6 +3260,19 @@ def test_transpile_target_with_qubits_without_ops_circuit_too_large_disconnected
         with self.assertRaises(TranspilerError):
             transpile(qc, target=target, optimization_level=opt_level)
 
+    @data(0, 1, 2, 3)
+    def test_barrier_no_leak_disjoint_connectivity(self, opt_level):
+        """Test that we don't leak an internal labelled barrier from disjoint layout processing."""
+        cmap = CouplingMap([(0, 1), (1, 2), (3, 4)])
+        qc = QuantumCircuit(cmap.size(), cmap.size())
+        qc.cx(0, 1)
+        qc.cx(1, 2)
+        qc.cx(2, 0)
+        qc.cx(3, 4)
+        qc.measure(qc.qubits, qc.clbits)
+        out = transpile(qc, coupling_map=cmap, optimization_level=opt_level)
+        self.assertNotIn("barrier", out.count_ops())
+
     @data(0, 1, 2, 3)
     def test_transpile_does_not_affect_backend_coupling(self, opt_level):
         """Test that transpiliation of a circuit does not mutate the `CouplingMap` stored by a V2