diff --git a/qiskit/transpiler/passes/layout/vf2_layout.py b/qiskit/transpiler/passes/layout/vf2_layout.py index a65146bff434..b0a5836b5cea 100644 --- a/qiskit/transpiler/passes/layout/vf2_layout.py +++ b/qiskit/transpiler/passes/layout/vf2_layout.py @@ -159,7 +159,8 @@ def run(self, dag): self.property_set["VF2Layout_stop_reason"] = VF2LayoutStopReason.SOLUTION_FOUND mapping = {dag.qubits[virt]: phys for virt, phys in layout.items()} chosen_layout = Layout(mapping) - self.property_set["layout"] = chosen_layout + + self.property_set["layout"] = vf2_utils.allocate_idle_qubits(dag, target, chosen_layout) for reg in dag.qregs.values(): self.property_set["layout"].add_register(reg) return @@ -285,7 +286,7 @@ def mapping_to_layout(layout_mapping): if chosen_layout is None: self.property_set["VF2Layout_stop_reason"] = VF2LayoutStopReason.NO_SOLUTION_FOUND return - self.property_set["layout"] = chosen_layout + self.property_set["layout"] = vf2_utils.allocate_idle_qubits(dag, target, chosen_layout) for reg in dag.qregs.values(): self.property_set["layout"].add_register(reg) diff --git a/qiskit/transpiler/passes/layout/vf2_post_layout.py b/qiskit/transpiler/passes/layout/vf2_post_layout.py index e85608c2e3da..cd1846f22d30 100644 --- a/qiskit/transpiler/passes/layout/vf2_post_layout.py +++ b/qiskit/transpiler/passes/layout/vf2_post_layout.py @@ -346,7 +346,9 @@ def run(self, dag): used_bits.add(i) chosen_layout.add(bit, i) break - self.property_set["post_layout"] = chosen_layout + self.property_set["post_layout"] = vf2_utils.allocate_idle_qubits( + dag, self.target, chosen_layout + ) else: if chosen_layout is None: stop_reason = VF2PostLayoutStopReason.NO_SOLUTION_FOUND diff --git a/qiskit/transpiler/passes/layout/vf2_utils.py b/qiskit/transpiler/passes/layout/vf2_utils.py index b832de0478fa..904220355a1b 100644 --- a/qiskit/transpiler/passes/layout/vf2_utils.py +++ b/qiskit/transpiler/passes/layout/vf2_utils.py @@ -26,6 +26,17 @@ from qiskit._accelerate.error_map import ErrorMap +def allocate_idle_qubits(dag, target, layout): + """Allocate the idle virtual qubits in the input DAG to arbitrary physical qubits.""" + # Extend with arbitrary decisions for idle qubits. + used_physical = set(layout.get_physical_bits()) + unused_physicals = (q for q in range(target.num_qubits) if q not in used_physical) + for bit in dag.qubits: + if bit not in layout: + layout[bit] = next(unused_physicals) + return layout + + def build_interaction_graph(dag, strict_direction=True): """Build an interaction graph from a dag.""" im_graph = PyDiGraph(multigraph=False) if strict_direction else PyGraph(multigraph=False) diff --git a/releasenotes/notes/vf2-idle-qubits-65d8875b2fb67fe1.yaml b/releasenotes/notes/vf2-idle-qubits-65d8875b2fb67fe1.yaml new file mode 100644 index 000000000000..acb97f707dd6 --- /dev/null +++ b/releasenotes/notes/vf2-idle-qubits-65d8875b2fb67fe1.yaml @@ -0,0 +1,6 @@ +--- +fixes: + - | + :class:`.VF2Layout` and :class:`.VF2PostLayout` will now correctly include (arbitrary) layout + assignments for completely idle qubits. Previously this might have been observed by calls to + :meth:`.TranspileLayout.initial_index_layout` failing after a compilation. diff --git a/test/python/transpiler/test_vf2_layout.py b/test/python/transpiler/test_vf2_layout.py index 019ba2880ad5..5fce75deac68 100644 --- a/test/python/transpiler/test_vf2_layout.py +++ b/test/python/transpiler/test_vf2_layout.py @@ -22,7 +22,7 @@ import rustworkx from qiskit import QuantumRegister, QuantumCircuit, ClassicalRegister -from qiskit.circuit import ControlFlowOp +from qiskit.circuit import ControlFlowOp, Qubit from qiskit.transpiler import CouplingMap, Target, TranspilerError from qiskit.transpiler.passes.layout.vf2_layout import VF2Layout, VF2LayoutStopReason from qiskit._accelerate.error_map import ErrorMap @@ -33,7 +33,7 @@ from qiskit.transpiler import PassManager, AnalysisPass from qiskit.transpiler.target import InstructionProperties from qiskit.transpiler.preset_passmanagers.common import generate_embed_passmanager -from test import QiskitTestCase # pylint: disable=wrong-import-order +from test import QiskitTestCase, combine # pylint: disable=wrong-import-order from ..legacy_cmaps import TENERIFE_CMAP, RUESCHLIKON_CMAP, MANHATTAN_CMAP, YORKTOWN_CMAP @@ -307,6 +307,25 @@ def test_determinism_all_1q(self): layouts[0], layout, f"Layout for execution {i} differs from the expected" ) + @combine( + seed=(-1, 12), # This hits both the "seeded" and "unseeded" paths. + strict_direction=(True, False), + ) + def test_complete_layout_with_idle_qubits(self, seed, strict_direction): + """Test that completely idle qubits are included in the resulting layout.""" + # Use registerless qubits to avoid any register-based shenangigans from adding the bits + # automatically. + qc = QuantumCircuit([Qubit() for _ in range(3)]) + qc.cx(0, 1) + target = Target.from_configuration( + num_qubits=3, basis_gates=["sx", "rz", "cx"], coupling_map=CouplingMap.from_line(3) + ) + property_set = {} + pass_ = VF2Layout(target=target, seed=seed, strict_direction=strict_direction) + pass_(qc, property_set=property_set) + unallocated = {i for i, bit in enumerate(qc.qubits) if bit not in property_set["layout"]} + self.assertEqual(unallocated, set()) + @ddt.ddt class TestVF2LayoutLattice(LayoutTestCase): diff --git a/test/python/transpiler/test_vf2_post_layout.py b/test/python/transpiler/test_vf2_post_layout.py index 66b138b69a45..a1c4d728542b 100644 --- a/test/python/transpiler/test_vf2_post_layout.py +++ b/test/python/transpiler/test_vf2_post_layout.py @@ -12,6 +12,7 @@ """Test the VF2Layout pass""" +import ddt import rustworkx from qiskit import QuantumRegister, QuantumCircuit @@ -24,11 +25,12 @@ from qiskit.circuit import Qubit from qiskit.compiler.transpiler import transpile from qiskit.transpiler.target import Target, InstructionProperties -from test import QiskitTestCase # pylint: disable=wrong-import-order +from test import QiskitTestCase, combine # pylint: disable=wrong-import-order from ..legacy_cmaps import LIMA_CMAP, YORKTOWN_CMAP, BOGOTA_CMAP +@ddt.ddt class TestVF2PostLayout(QiskitTestCase): """Tests the VF2Layout pass""" @@ -324,6 +326,32 @@ def test_last_qubits_best(self): vf2_pass.run(dag) self.assertLayoutV2(dag, target_last_qubits_best, vf2_pass.property_set) + @combine( + seed=(-1, 12), # This hits both the "seeded" and "unseeded" paths. + strict_direction=(True, False), + ) + def test_complete_layout_with_idle_qubits(self, seed, strict_direction): + """Test that completely idle qubits are included in the resulting layout.""" + qc = QuantumCircuit(3) + qc.cx(0, 1) + # We need to ensure that VF2Post actually triggers a remapping. + target = Target(3) + target.add_instruction( + CXGate(), + properties={ + (0, 1): InstructionProperties(error=1e-1), + (1, 0): InstructionProperties(error=1e-1), + (2, 1): InstructionProperties(error=1e-8), + }, + ) + property_set = {} + pass_ = VF2PostLayout(target=target, seed=seed, strict_direction=strict_direction) + pass_(qc, property_set=property_set) + unallocated = { + i for i, bit in enumerate(qc.qubits) if bit not in property_set["post_layout"] + } + self.assertEqual(unallocated, set()) + class TestVF2PostLayoutScoring(QiskitTestCase): """Test scoring heuristic function for VF2PostLayout."""