Skip to content

Commit 20ba77d

Browse files
Fix VF2 layout allocation with idle qubits (Qiskit#14938) (Qiskit#14940)
The two VF2 passes failed to allocate completely idle qubits into the `Layout` objects they returned. While the mapping doesn't really _mean_ much, it's important for the rest of the pipeline that a `Layout` object is a complete layout for the incoming virtuals, even if some of the choices are arbitrary. This typically appeared to users via calls to `TranspileLayout.initial_index_layout(filter_ancillas=True)` bugging out, but really anything inspecting the `Layout` before that was susceptible. (cherry picked from commit 2577bc1) Co-authored-by: Jake Lishman <[email protected]>
1 parent b7c28b7 commit 20ba77d

File tree

6 files changed

+73
-6
lines changed

6 files changed

+73
-6
lines changed

qiskit/transpiler/passes/layout/vf2_layout.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,8 @@ def run(self, dag):
159159
self.property_set["VF2Layout_stop_reason"] = VF2LayoutStopReason.SOLUTION_FOUND
160160
mapping = {dag.qubits[virt]: phys for virt, phys in layout.items()}
161161
chosen_layout = Layout(mapping)
162-
self.property_set["layout"] = chosen_layout
162+
163+
self.property_set["layout"] = vf2_utils.allocate_idle_qubits(dag, target, chosen_layout)
163164
for reg in dag.qregs.values():
164165
self.property_set["layout"].add_register(reg)
165166
return
@@ -285,7 +286,7 @@ def mapping_to_layout(layout_mapping):
285286
if chosen_layout is None:
286287
self.property_set["VF2Layout_stop_reason"] = VF2LayoutStopReason.NO_SOLUTION_FOUND
287288
return
288-
self.property_set["layout"] = chosen_layout
289+
self.property_set["layout"] = vf2_utils.allocate_idle_qubits(dag, target, chosen_layout)
289290
for reg in dag.qregs.values():
290291
self.property_set["layout"].add_register(reg)
291292

qiskit/transpiler/passes/layout/vf2_post_layout.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -346,7 +346,9 @@ def run(self, dag):
346346
used_bits.add(i)
347347
chosen_layout.add(bit, i)
348348
break
349-
self.property_set["post_layout"] = chosen_layout
349+
self.property_set["post_layout"] = vf2_utils.allocate_idle_qubits(
350+
dag, self.target, chosen_layout
351+
)
350352
else:
351353
if chosen_layout is None:
352354
stop_reason = VF2PostLayoutStopReason.NO_SOLUTION_FOUND

qiskit/transpiler/passes/layout/vf2_utils.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,17 @@
2626
from qiskit._accelerate.error_map import ErrorMap
2727

2828

29+
def allocate_idle_qubits(dag, target, layout):
30+
"""Allocate the idle virtual qubits in the input DAG to arbitrary physical qubits."""
31+
# Extend with arbitrary decisions for idle qubits.
32+
used_physical = set(layout.get_physical_bits())
33+
unused_physicals = (q for q in range(target.num_qubits) if q not in used_physical)
34+
for bit in dag.qubits:
35+
if bit not in layout:
36+
layout[bit] = next(unused_physicals)
37+
return layout
38+
39+
2940
def build_interaction_graph(dag, strict_direction=True):
3041
"""Build an interaction graph from a dag."""
3142
im_graph = PyDiGraph(multigraph=False) if strict_direction else PyGraph(multigraph=False)
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
fixes:
3+
- |
4+
:class:`.VF2Layout` and :class:`.VF2PostLayout` will now correctly include (arbitrary) layout
5+
assignments for completely idle qubits. Previously this might have been observed by calls to
6+
:meth:`.TranspileLayout.initial_index_layout` failing after a compilation.

test/python/transpiler/test_vf2_layout.py

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
import rustworkx
2323

2424
from qiskit import QuantumRegister, QuantumCircuit, ClassicalRegister
25-
from qiskit.circuit import ControlFlowOp
25+
from qiskit.circuit import ControlFlowOp, Qubit
2626
from qiskit.transpiler import CouplingMap, Target, TranspilerError
2727
from qiskit.transpiler.passes.layout.vf2_layout import VF2Layout, VF2LayoutStopReason
2828
from qiskit._accelerate.error_map import ErrorMap
@@ -33,7 +33,7 @@
3333
from qiskit.transpiler import PassManager, AnalysisPass
3434
from qiskit.transpiler.target import InstructionProperties
3535
from qiskit.transpiler.preset_passmanagers.common import generate_embed_passmanager
36-
from test import QiskitTestCase # pylint: disable=wrong-import-order
36+
from test import QiskitTestCase, combine # pylint: disable=wrong-import-order
3737

3838
from ..legacy_cmaps import TENERIFE_CMAP, RUESCHLIKON_CMAP, MANHATTAN_CMAP, YORKTOWN_CMAP
3939

@@ -307,6 +307,25 @@ def test_determinism_all_1q(self):
307307
layouts[0], layout, f"Layout for execution {i} differs from the expected"
308308
)
309309

310+
@combine(
311+
seed=(-1, 12), # This hits both the "seeded" and "unseeded" paths.
312+
strict_direction=(True, False),
313+
)
314+
def test_complete_layout_with_idle_qubits(self, seed, strict_direction):
315+
"""Test that completely idle qubits are included in the resulting layout."""
316+
# Use registerless qubits to avoid any register-based shenangigans from adding the bits
317+
# automatically.
318+
qc = QuantumCircuit([Qubit() for _ in range(3)])
319+
qc.cx(0, 1)
320+
target = Target.from_configuration(
321+
num_qubits=3, basis_gates=["sx", "rz", "cx"], coupling_map=CouplingMap.from_line(3)
322+
)
323+
property_set = {}
324+
pass_ = VF2Layout(target=target, seed=seed, strict_direction=strict_direction)
325+
pass_(qc, property_set=property_set)
326+
unallocated = {i for i, bit in enumerate(qc.qubits) if bit not in property_set["layout"]}
327+
self.assertEqual(unallocated, set())
328+
310329

311330
@ddt.ddt
312331
class TestVF2LayoutLattice(LayoutTestCase):

test/python/transpiler/test_vf2_post_layout.py

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212

1313
"""Test the VF2Layout pass"""
1414

15+
import ddt
1516
import rustworkx
1617

1718
from qiskit import QuantumRegister, QuantumCircuit
@@ -24,11 +25,12 @@
2425
from qiskit.circuit import Qubit
2526
from qiskit.compiler.transpiler import transpile
2627
from qiskit.transpiler.target import Target, InstructionProperties
27-
from test import QiskitTestCase # pylint: disable=wrong-import-order
28+
from test import QiskitTestCase, combine # pylint: disable=wrong-import-order
2829

2930
from ..legacy_cmaps import LIMA_CMAP, YORKTOWN_CMAP, BOGOTA_CMAP
3031

3132

33+
@ddt.ddt
3234
class TestVF2PostLayout(QiskitTestCase):
3335
"""Tests the VF2Layout pass"""
3436

@@ -324,6 +326,32 @@ def test_last_qubits_best(self):
324326
vf2_pass.run(dag)
325327
self.assertLayoutV2(dag, target_last_qubits_best, vf2_pass.property_set)
326328

329+
@combine(
330+
seed=(-1, 12), # This hits both the "seeded" and "unseeded" paths.
331+
strict_direction=(True, False),
332+
)
333+
def test_complete_layout_with_idle_qubits(self, seed, strict_direction):
334+
"""Test that completely idle qubits are included in the resulting layout."""
335+
qc = QuantumCircuit(3)
336+
qc.cx(0, 1)
337+
# We need to ensure that VF2Post actually triggers a remapping.
338+
target = Target(3)
339+
target.add_instruction(
340+
CXGate(),
341+
properties={
342+
(0, 1): InstructionProperties(error=1e-1),
343+
(1, 0): InstructionProperties(error=1e-1),
344+
(2, 1): InstructionProperties(error=1e-8),
345+
},
346+
)
347+
property_set = {}
348+
pass_ = VF2PostLayout(target=target, seed=seed, strict_direction=strict_direction)
349+
pass_(qc, property_set=property_set)
350+
unallocated = {
351+
i for i, bit in enumerate(qc.qubits) if bit not in property_set["post_layout"]
352+
}
353+
self.assertEqual(unallocated, set())
354+
327355

328356
class TestVF2PostLayoutScoring(QiskitTestCase):
329357
"""Test scoring heuristic function for VF2PostLayout."""

0 commit comments

Comments
 (0)