Repository: Qiskit/qiskit — pull request #11655 (python)
Title: Fix split barriers leaking during disjoint layout processing

<!--\r\n⚠️ If you do not respect this template, your pull request will be closed.\r\n⚠️ Your pull request title should be short detailed and understandable for all.\r\n⚠️ Also, please add a release note file using reno if the change needs to be\r\n  documented in the release notes.\r\n⚠️ If your pull request fixes an open issue, please link to the issue.\r\n\r\n- [ ] I have added the tests to cover my changes.\r\n- [ ] I have updated the documentation accordingly.\r\n- [ ] I have read the CONTRIBUTING document.\r\n-->\r\n\r\n### Summary\r\n\r\nThis commit fixes an issue in the disjoint layout processing caused by a shared reference from the input dag to a layout pass to the internal transformations the layout processing performs. As part of the disjoint layout processing the input dag needs to be split into smaller dags for each connected component in the circuit. To enable this any barriers present in the circuit need to be split up prior to analyzing the connected components in the dag. A multiqubit barrier doesn't represent a real connectivity requirement so they need to be split prior to analysis to ensure they don't factor into the connected component computation. To faciliate not losing the semantic meaning of the barrier for the layout analysis of each connected component, the barrier split is done by first taking an n-qubit barrier and converting it into n parallel 1q barriers with a uuid label applied to it. Then after we split the circuit into the separate connected components the uuid is used to identify any components of the same barrier and combine them together.\r\n\r\nThere were two issues with this approach the first is that the splitting of the barriers was done on the input dag without copying first. This causes the input dag to be modified and because in most cases it's a shared instance which would leak out the barrier splitting if the input dag was returned verbatim from the pass, which in most cases it would be. This issue is fixed by ensuring that we re-combine any split barriers after we've split the dag into it's connected components. The second issue is the uuid label assignment would overwrite any existing labels on barriers. While setting labels on barriers is uncommon for users to do (but still completely valid) this is causing a bigger issues since #10323 because the transpiler is assigning labels to barriers it creates internally so they can be removed before the circuit is returned. This is also fixed in this commit by appending a uuid to the existing label instead of overwriting it, so we're able to restore the original label when we recombine barriers.\r\n\r\n### Details and comments\r\n\r\nFixes #11649

Resolved issues:
title: Disjoint handling in transpiler fails to rejoin barriers
body: ### Environment\n\n- **Qiskit Terra version**: `main` @ ce02f06\r\n- **Python version**: 3.10\r\n- **Operating system**: macOS\r\n\n\n### What is happening?\n\nThe 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.\n\n### How can we reproduce the issue?\n\nGiven a set up of:\r\n\r\n```python\r\nfrom qiskit import transpile, QuantumCircuit\r\nfrom qiskit.transpiler import CouplingMap\r\n\r\ncm = CouplingMap([\r\n    (0, 1),\r\n    (1, 2),\r\n    #\r\n    (3, 4),\r\n])\r\nqc = QuantumCircuit(cm.size(), cm.size())\r\nqc.cx(0, 1)\r\nqc.cx(1, 2)\r\nqc.cx(2, 0)\r\n#\r\nqc.cx(3, 4)\r\nqc.measure(qc.qubits, qc.clbits)\r\n\r\nout = transpile(qc, coupling_map=cm, seed_transpiler=0)\r\n```\r\n\r\n`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:\r\n\r\n```python\r\nfor i, ins in enumerate(out.data):\r\n    if ins.operation.name == "barrier":\r\n        print(i, repr(ins.operation.label))\r\n        # Coerce to string so I can draw the circuit after.\r\n        ins.operation.label = str(ins.operation.label)\r\n```\r\n```text\r\n8 UUID('31558989-c21a-4e5c-b014-01639295df7b')\r\n10 UUID('31558989-c21a-4e5c-b014-01639295df7b')\r\n12 UUID('31558989-c21a-4e5c-b014-01639295df7b')\r\n14 UUID('31558989-c21a-4e5c-b014-01639295df7b')\r\n15 UUID('31558989-c21a-4e5c-b014-01639295df7b')\r\n```\r\n\r\nI 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:\r\n```text\r\n                                                    ┌───┐                          ┌───┐                  31558989-c21a-4e5c-b014-01639295df7b ┌─┐\r\nq_2 -> 0 ───────────────────────X───────────────────┤ H ├───────■──────────────────┤ H ├───────────────────────────────────░───────────────────┤M├───\r\n                                │                   └───┘       │                  └───┘                                   ░                   └╥┘\r\n                                │                   ┌───┐     ┌─┴─┐                ┌───┐                  31558989-c21a-4e5c-b014-01639295df7b  ║ ┌─┐\r\nq_0 -> 1 ──■────────────────────X───────────────────┤ H ├──■──┤ X ├────────────────┤ H ├───────────────────────────────────░────────────────────╫─┤M├\r\n           │                                        └───┘  │  └───┘                └───┘                                   ░                    ║ └╥┘\r\n         ┌─┴─┐                ┌───┐                      ┌─┴─┐┌───┐ 31558989-c21a-4e5c-b014-01639295df7b                  ┌─┐                   ║  ║\r\nq_1 -> 2 ┤ X ├────────────────┤ H ├──────────────────────┤ X ├┤ H ├──────────────────░────────────────────────────────────┤M├───────────────────╫──╫─\r\n         └───┘                └───┘                      └───┘└───┘                  ░                                    └╥┘                   ║  ║\r\n               31558989-c21a-4e5c-b014-01639295df7b  ┌─┐                                                                   ║                    ║  ║\r\nq_3 -> 3 ──■────────────────────░────────────────────┤M├───────────────────────────────────────────────────────────────────╫────────────────────╫──╫─\r\n           │                    ░                    └╥┘                                                                   ║                    ║  ║\r\n         ┌─┴─┐ 31558989-c21a-4e5c-b014-01639295df7b   ║   ┌─┐                                                              ║                    ║  ║\r\nq_4 -> 4 ┤ X ├──────────────────░─────────────────────╫───┤M├──────────────────────────────────────────────────────────────╫────────────────────╫──╫─\r\n         └───┘                  ░                     ║   └╥┘                                                              ║                    ║  ║\r\n    c: 5/═════════════════════════════════════════════╩════╩═══════════════════════════════════════════════════════════════╩════════════════════╩══╩═\r\n                                                      3    4                                                               1                    0  2\r\n```\r\nso it's clear the temporary barrier we insert before measures isn't being recombined and then deleted.\r\n\n\n### What should happen?\n\n* The temporary barrier should have been recombined after the disjoint handling\r\n* The temporary barrier should have been removed before the circuit exited\r\n* 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).\n\n### Any suggestions?\n\n_No response_

Hints:

Fix patch:
diff --git a/qiskit/transpiler/passes/layout/disjoint_utils.py b/qiskit/transpiler/passes/layout/disjoint_utils.py\nindex ffbb5076c1ef..42f665c473a1 100644\n--- a/qiskit/transpiler/passes/layout/disjoint_utils.py\n+++ b/qiskit/transpiler/passes/layout/disjoint_utils.py\n@@ -107,7 +107,10 @@ def split_barriers(dag: DAGCircuit):\n         num_qubits = len(node.qargs)\n         if num_qubits == 1:\n             continue\n-        barrier_uuid = uuid.uuid4()\n+        if node.op.label:\n+            barrier_uuid = f"{node.op.label}_uuid={uuid.uuid4()}"\n+        else:\n+            barrier_uuid = f"_none_uuid={uuid.uuid4()}"\n         split_dag = DAGCircuit()\n         split_dag.add_qubits([Qubit() for _ in range(num_qubits)])\n         for i in range(num_qubits):\n@@ -124,10 +127,13 @@ def combine_barriers(dag: DAGCircuit, retain_uuid: bool = True):\n     qubit_indices = {bit: index for index, bit in enumerate(dag.qubits)}\n     uuid_map: dict[uuid.UUID, DAGOpNode] = {}\n     for node in dag.op_nodes(Barrier):\n-        if isinstance(node.op.label, uuid.UUID):\n-            barrier_uuid = node.op.label\n+        if node.op.label:\n+            if "_uuid=" in node.op.label:\n+                barrier_uuid = node.op.label\n+            else:\n+                continue\n             if barrier_uuid in uuid_map:\n-                other_node = uuid_map[node.op.label]\n+                other_node = uuid_map[barrier_uuid]\n                 num_qubits = len(other_node.qargs) + len(node.qargs)\n                 new_op = Barrier(num_qubits, label=barrier_uuid)\n                 new_node = dag.replace_block_with_op([node, other_node], new_op, qubit_indices)\n@@ -136,8 +142,11 @@ def combine_barriers(dag: DAGCircuit, retain_uuid: bool = True):\n                 uuid_map[barrier_uuid] = node\n     if not retain_uuid:\n         for node in dag.op_nodes(Barrier):\n-            if isinstance(node.op.label, uuid.UUID):\n+            if isinstance(node.op.label, str) and node.op.label.startswith("_none_uuid="):\n                 node.op.label = None\n+            elif isinstance(node.op.label, str) and "_uuid=" in node.op.label:\n+                original_label = "_uuid=".join(node.op.label.split("_uuid=")[:-1])\n+                node.op.label = original_label\n \n \n def require_layout_isolated_to_component(\n@@ -202,4 +211,7 @@ def separate_dag(dag: DAGCircuit) -> List[DAGCircuit]:\n         new_dag.remove_clbits(*idle_clbits)\n         combine_barriers(new_dag)\n         decomposed_dags.append(new_dag)\n+    # Reverse split barriers on input dag to avoid leaking out internal transformations as\n+    # part of splitting\n+    combine_barriers(dag, retain_uuid=False)\n     return decomposed_dags\ndiff --git a/releasenotes/notes/fix-leaking-split-barrier-7c143d6b13b96ced.yaml b/releasenotes/notes/fix-leaking-split-barrier-7c143d6b13b96ced.yaml\nnew file mode 100644\nindex 000000000000..5cd8f19dc150\n--- /dev/null\n+++ b/releasenotes/notes/fix-leaking-split-barrier-7c143d6b13b96ced.yaml\n@@ -0,0 +1,12 @@\n+---\n+fixes:\n+  - |\n+    Fixed an issue when using :func:`.transpile` or running a preset pass\n+    manager (such as generated by :func:`.generate_preset_pass_manager`) when\n+    targetting a backend that has disjoint connectivity adding extra barriers\n+    to the output :class:`.QuantumCircuit`. In some cases several\n+    single qubit :class:`.Barrier` directives would be included in the output\n+    circuit right before any final measurements in the circuit. This was\n+    internal state generated by the internal processing for disjoint\n+    connectivity that was incorrectly being added into the output circuit.\n+    Fixed `#11649 <https://github.com/Qiskit/qiskit/issues/11649>`__\n

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