Repository: Qiskit/qiskit — pull request #13835 (python)
Title: Fix tracking of routing permutation in Sabre with disjoint backends (backport #13833)

### Summary\n\nIf the backing coupling graph is disjoint, and unused components of the coupling graph would not be considered when constructing the complete routing permutation.  In practice, Sabre aborts immediately after layout without attempting to route, if it needed to split the DAG across more than one disjoint component, because it can't guarantee correctness of the final routing in the presence of component-spanning barriers or classical communication, so the only way for a component to be forgotten is if the backend is disjoint, but the DAG fits into a single component.\n\n\n\n\n### Details and comments\n\nFix #13732, though there's a _whole_ cornucopia of bugs split between very old versions of Qiskit, this commit's parent, and the example in the issue itself that lead from that issue seeing a regression in Qiskit 1.3 to this one.\n\nIn actuality, this bug has been around since the disjoint-coupling handling was first added in #9802 (qiskit-terra 0.24), and the test case in this PR would fail that version too (the fact that `TranspileLayout.initial_index_layout` wasn't added til later notwithstanding - the first assertion fails).<hr>This is an automatic backport of pull request #13833 done by [Mergify](https://mergify.com).

Resolved issues:
title: Error when running `final_index_layout(filter_ancillas=False)`
body: ### Environment\n\n- **Qiskit version**: 1.3.2\n- **Python version**:  3.11\n- **Operating system**: macos/linux\n\n\n### What is happening?\n\nOn certain circuits transpiled with a backend and a coupling map, this call fails\n```\ntranspiled_circuit.layout.final_index_layout(filter_ancillas=False)\n```\nraising `KeyError: "The item Qubit(QuantumRegister(5, 'q'), 3) does not exist in the Layout"`.\n\nThis worked fine until qiskit 1.3.0\n\n### How can we reproduce the issue?\n\nRunning the following code:\n\n```\nfrom qiskit import QuantumCircuit, transpile, qasm2\nfrom qiskit_ibm_runtime import QiskitRuntimeService\n\nservice = QiskitRuntimeService(channel="ibm_quantum")\nbackend = service.backend("ibmq_manila")\n\nqc = qasm2.loads('OPENQASM 2.0;include "qelib1.inc";qreg q[3];rccx q[2],q[1],q[0];t q[2];u(0.28905709212845454,5.626877140821112,5.481851443455051) q[1];h q[0];cx q[1],q[2];s q[0];', custom_instructions=qasm2.LEGACY_CUSTOM_INSTRUCTIONS)\n\ntranspiled_circuit = transpile(qc,backend=backend,coupling_map=[[0,1],[1,2],[2,3]],optimization_level=2,seed_transpiler=42)\ntranspiled_circuit.layout.final_index_layout(filter_ancillas=False)\n```\n\n\n\n### What should happen?\n\nThere should not be any error in the call. The output to that particular example in `qiskit 1.2.4` is `[3, 1, 2, 0]`\n\n\n### Any suggestions?\n\n_No response_

Hints:

Fix patch:
diff --git a/qiskit/transpiler/passes/layout/sabre_layout.py b/qiskit/transpiler/passes/layout/sabre_layout.py\nindex af17cc226cb7..360e8d003e79 100644\n--- a/qiskit/transpiler/passes/layout/sabre_layout.py\n+++ b/qiskit/transpiler/passes/layout/sabre_layout.py\n@@ -328,6 +328,19 @@ def run(self, dag):\n                 for initial, final in enumerate(component.final_permutation)\n             }\n         )\n+\n+        # The coupling map may have been split into more components than the DAG.  In this case,\n+        # there will be some physical qubits unaccounted for in our `final_layout`.  Strictly the\n+        # `if` check is unnecessary, but we can avoid the loop for most circuits and backends.\n+        if len(final_layout) != len(physical_qubits):\n+            used_qubits = {\n+                qubit for component in components for qubit in component.coupling_map.graph.nodes()\n+            }\n+            for index, qubit in enumerate(physical_qubits):\n+                if index in used_qubits:\n+                    continue\n+                final_layout[qubit] = index\n+\n         if self.property_set["final_layout"] is None:\n             self.property_set["final_layout"] = final_layout\n         else:\ndiff --git a/releasenotes/notes/sabre-disjoint-routing-85c6f6481c9ffca4.yaml b/releasenotes/notes/sabre-disjoint-routing-85c6f6481c9ffca4.yaml\nnew file mode 100644\nindex 000000000000..e0618dd601f7\n--- /dev/null\n+++ b/releasenotes/notes/sabre-disjoint-routing-85c6f6481c9ffca4.yaml\n@@ -0,0 +1,15 @@\n+---\n+fixes:\n+  - |\n+    When :class:`.SabreLayout` is used to do both layout and routing simultaneously (as is the case\n+    for the default options to :func:`.transpile` and :func:`.generate_preset_pass_manager`) on a\n+    :class:`.Target` or :class:`.CouplingMap` with disjoint connectivity, and the input circuit fits\n+    into a single component of the coupling map, the routing permutation will now be tracked\n+    correctly.\n+\n+    Previously, any qubits in the coupling map that were not connected, even indirectly, to a qubit\n+    used by the routed circuit would not be included in the final routing permutation.  This could\n+    cause surprising behaviour a long way from the point of failure, even if compilation appeared to\n+    succeed, such as calls to :meth:`.TranspileLayout.final_index_layout` raising :exc:`KeyError`.\n+\n+    This bug did not affect backends that were fully connected, as most are.\n

Test patch:
diff --git a/test/python/transpiler/test_sabre_layout.py b/test/python/transpiler/test_sabre_layout.py\nindex 2942cab30cdd..932822af012b 100644\n--- a/test/python/transpiler/test_sabre_layout.py\n+++ b/test/python/transpiler/test_sabre_layout.py\n@@ -470,6 +470,21 @@ def test_with_partial_layout(self):\n         layout = pm.property_set["layout"]\n         self.assertEqual([layout[q] for q in qc.qubits], [3, 1, 2, 5, 4, 6, 7, 8])\n \n+    def test_dag_fits_in_one_component(self):\n+        """Test that the output is valid if the DAG all fits in a single component of a disjoint\n+        coupling map.."""\n+        qc = QuantumCircuit(3)\n+        qc.cx(0, 1)\n+        qc.cx(1, 2)\n+        qc.cx(2, 0)\n+\n+        disjoint = CouplingMap([(0, 1), (1, 2), (3, 4), (4, 5)])\n+        layout_routing_pass = SabreLayout(disjoint, seed=2025_02_12, swap_trials=1, layout_trials=1)\n+        out = layout_routing_pass(qc)\n+        self.assertEqual(len(out.layout.initial_layout), len(out.layout.final_layout))\n+        self.assertEqual(out.layout.initial_index_layout(filter_ancillas=False), [1, 0, 2, 3, 4, 5])\n+        self.assertEqual(out.layout.final_index_layout(filter_ancillas=False), [2, 0, 1, 3, 4, 5])\n+\n \n class TestSabrePreLayout(QiskitTestCase):\n     """Tests the SabreLayout pass with starting layout created by SabrePreLayout."""\n