Repository: Qiskit/qiskit — pull request #15001 (python)
Title: Fix `VF2PostLayout` with uncoupled qubits in `strict_direction=True` (backport #14998)

When `VF2PostLayout` is set in strict mode, we have to include even active but uncoupled qubits in the interaction graph, because of the semantic constraints.  We need to limit the number of these uncoupled qubits we'll consider, though, because there's a factorial overhead to handling them.\n\nA previous commit[^1] added this skip to `strict_direction=False`, but didn't correctly handle the continuation case where we're within the limit.  This commit ~ups the limit to 3 uncoupled qubits (a potential 6x overhead) and~ correctly clears out the uncoupled-qubit metadata that was causing problems in later layout reconstruction.\n\n[^1]: b4094ddc: Fix calls to `VF2PostLayout` after optimization loop at O3\n\n\n\n### Summary\n\n\n\n### Details and comments\n\nNo release note because the bug isn't released.\n\nFix #14997 <hr>This is an automatic backport of pull request #14998 done by [Mergify](https://mergify.com).

Resolved issues:
title: VF2PostLayout fails during layout finalisation with 1q wires
body: ### Environment\n\n- **Qiskit version**: 2.2.0rc1\n- **Python version**: 3.13\n- **Operating system**: macOS\n\n\n### What is happening?\n\n`VF2PostLayout` can fail during layout expansion, seemingly if there is a single qubit that has only 1q operations on it (but not more than one).\n\n### How can we reproduce the issue?\n\n```python\nfrom qiskit import QuantumCircuit, transpile\nfrom qiskit.providers.fake_provider import GenericBackendV2\n\nqc = QuantumCircuit(5, 5)\nqc.h(0)\nfor k in range(1, 4):\n    qc.cx(0, k)\nqc.measure(qc.qubits, qc.clbits)\n\nbackend = GenericBackendV2(num_qubits=5, basis_gates=["cx", "id", "rz", "sx", "x"], seed=0)\ntqc = transpile(qc, backend=backend, optimization_level=3, seed_transpiler=0)\n```\nraises\n```text\nTypeError                                 Traceback (most recent call last)\nCell In[7], line 11\n      8 qc.measure(qc.qubits, qc.clbits)\n     10 backend = GenericBackendV2(num_qubits=5, basis_gates=["cx", "id", "rz", "sx", "x"], seed=0)\n---> 11 tqc = transpile(qc, backend=backend, optimization_level=3, seed_transpiler=0)\n\n[...snip...]\n\nFile ~/code/qiskit/terra/qiskit/transpiler/passes/layout/vf2_post_layout.py:341, in VF2PostLayout.run(self, dag)\n    339 existing_layout = self.property_set["layout"]\n    340 # If any ancillas in initial layout map them back to the final layout output\n--> 341 if existing_layout is not None and len(existing_layout) > len(chosen_layout):\n    342     virtual_bits = chosen_layout.get_virtual_bits()\n    343     used_bits = set(virtual_bits.values())\n\nTypeError: object of type 'NoneType' has no len()\n```\n\n### What should happen?\n\nSuccessful compilation.\n\n### Any suggestions?\n\nI suspect that this was introduced during #14869 - it's suspicious to me that the catch for single-qubit operations is `if len(free) > 1` and not just `if free` - certainly the logic dictates that even a _single_ 1q wire can cause spurious failures.

Hints:

Fix patch:
diff --git a/qiskit/transpiler/passes/layout/vf2_post_layout.py b/qiskit/transpiler/passes/layout/vf2_post_layout.py\nindex d5b2b3c73182..5473f1a57f6d 100644\n--- a/qiskit/transpiler/passes/layout/vf2_post_layout.py\n+++ b/qiskit/transpiler/passes/layout/vf2_post_layout.py\n@@ -153,11 +153,19 @@ def run(self, dag):\n             self.property_set["VF2PostLayout_stop_reason"] = VF2PostLayoutStopReason.MORE_THAN_2Q\n             return\n         im_graph, im_graph_node_map, reverse_im_graph_node_map, free_nodes = result\n-        if len(free_nodes) > 1 and self.strict_direction:\n-            self.property_set["VF2PostLayout_stop_reason"] = (\n-                VF2PostLayoutStopReason.NO_BETTER_SOLUTION_FOUND\n-            )\n-            return\n+        if self.strict_direction and free_nodes:\n+            # If there are uncoupled qubits, in non-strict modes we just allocate them to the\n+            # lowest-error states at the end.  However, in strict mode, we have to consider them at\n+            # the same time to handle heterogeneous targets correctly.  This risks a factorial\n+            # combinatoric explosion in complexity, though, so we put a limit on how many we'll\n+            # handle.  The builder still builds the graph entirely, it just returns the free nodes\n+            # for us to check on, so we clear that out after we've checked it.\n+            if len(free_nodes) > 1:\n+                self.property_set["VF2PostLayout_stop_reason"] = (\n+                    VF2PostLayoutStopReason.NO_BETTER_SOLUTION_FOUND\n+                )\n+                return\n+            free_nodes.clear()\n         scoring_bit_list = vf2_utils.build_bit_list(im_graph, im_graph_node_map)\n         scoring_edge_list = vf2_utils.build_edge_list(im_graph)\n \n

Test patch:
diff --git a/test/python/transpiler/test_vf2_post_layout.py b/test/python/transpiler/test_vf2_post_layout.py\nindex a1c4d728542b..d8f75cc2dbcf 100644\n--- a/test/python/transpiler/test_vf2_post_layout.py\n+++ b/test/python/transpiler/test_vf2_post_layout.py\n@@ -18,7 +18,7 @@\n from qiskit import QuantumRegister, QuantumCircuit\n from qiskit.circuit import ControlFlowOp\n from qiskit.circuit.library import CXGate, XGate\n-from qiskit.transpiler import Layout, TranspilerError\n+from qiskit.transpiler import Layout, TranspilerError, PassManager, passes\n from qiskit.transpiler.passes.layout.vf2_post_layout import VF2PostLayout, VF2PostLayoutStopReason\n from qiskit.converters import circuit_to_dag\n from qiskit.providers.fake_provider import GenericBackendV2\n@@ -352,6 +352,33 @@ def test_complete_layout_with_idle_qubits(self, seed, strict_direction):\n         }\n         self.assertEqual(unallocated, set())\n \n+    @ddt.data(-1, 12)\n+    def test_complete_layout_with_one_uncoupled_qubit(self, seed):\n+        """Test that a single uncoupled qubit is handled correctly.\n+\n+        Regression test of https://github.com/Qiskit/qiskit/issues/14997."""\n+        qc = QuantumCircuit(3)\n+        qc.x(0)\n+        qc.cx(1, 2)\n+\n+        bad = InstructionProperties(error=1e-1)\n+        good = InstructionProperties(error=1e-5)\n+\n+        target = Target()\n+        target.add_instruction(XGate(), {(0,): bad, (1,): good, (2,): good})\n+        target.add_instruction(CXGate(), {(0, 1): good, (1, 2): bad})\n+\n+        pm = PassManager(\n+            [\n+                passes.TrivialLayout(target),\n+                passes.ApplyLayout(),\n+                passes.VF2PostLayout(target, seed=seed, strict_direction=True),\n+                passes.ApplyLayout(),\n+            ]\n+        )\n+        out = pm.run(qc)\n+        self.assertEqual(out.layout.final_index_layout(), [2, 0, 1])\n+\n \n class TestVF2PostLayoutScoring(QiskitTestCase):\n     """Test scoring heuristic function for VF2PostLayout."""\n