Fix handling of holes in physical bits list in Layout (#8767)

* Fix handling of holes in physical bits list in Layout

Previously the behavior of `Layout.add(virtual)` would attempt to select
an available physical bit to pair for the layout. However, the manner in
which this selection was done was buggy and it would potentially skip
over available physical bits on the device and instead add a new
physical bit to the layout. This had unintended consequences for layouts
that added bits in higher numbers first. This commit fixes this behavior
by first checking that we've exhausted all available physical bits from
0 to max bit in layout. Then if there are no holes in the physical bits
in the layout it will add a bit to the top.

Fixes #8667

* Add release note and clarify docstring

* Apply suggestions from code review

Co-authored-by: Jake Lishman <jake@binhbar.com>

Co-authored-by: Jake Lishman <jake@binhbar.com>
Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
This commit is contained in:
Matthew Treinish 2022-09-20 13:35:45 -04:00 committed by GitHub
parent 5c19a1f349
commit c0f5ae02a9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 90 additions and 10 deletions

View File

@ -157,18 +157,26 @@ class Layout:
def add(self, virtual_bit, physical_bit=None):
"""
Adds a map element between `bit` and `physical_bit`. If `physical_bit` is not
defined, `bit` will be mapped to a new physical bit (extending the length of the
layout by one.)
defined, `bit` will be mapped to a new physical bit.
Args:
virtual_bit (tuple): A (qu)bit. For example, (QuantumRegister(3, 'qr'), 2).
physical_bit (int): A physical bit. For example, 3.
"""
if physical_bit is None:
physical_candidate = len(self)
while physical_candidate in self._p2v:
physical_candidate += 1
physical_bit = physical_candidate
if len(self._p2v) == 0:
physical_bit = 0
else:
max_physical = max(self._p2v)
# Fill any gaps in the existing bits
for physical_candidate in range(max_physical):
if physical_candidate not in self._p2v:
physical_bit = physical_candidate
break
# If there are no free bits in the allocated physical bits add new ones
else:
physical_bit = max_physical + 1
self[virtual_bit] = physical_bit
def add_register(self, reg):

View File

@ -0,0 +1,26 @@
---
upgrade:
- |
The :meth:`.Layout.add` behavior when not specifying a ``physical_bit``
has changed from previous releases. In previous releases, a new physical
bit would be added based on the length of the :class:`~.Layout` object. For
example if you had a :class:`~.Layout` with the physical bits 1 and 3
successive calls to :meth:`~.Layout.add` would add physical bits 2, 4, 5, 6,
etc. While if the physical bits were 2 and 3 then successive calls would
add 4, 5, 6, 7, etc. This has changed so that instead :meth:`.Layout.add`
will first add any missing physical bits between 0 and the max physical bit
contained in the :class:`~.Layout`. So for the 1 and 3 example it now
adds 0, 2, 4, 5 and for the 2 and 3 example it adds 0, 1, 4, 5 to the
:class:`~.Layout`. This change was made for both increased predictability
of the outcome, and also to fix a class of bugs caused by the unexpected
behavior. As physical bits on a backend always are contiguous sequences from
0 to :math:`n` adding new bits when there are still unused physical bits
could potentially cause the layout to use more bits than available on the
backend. If you desire the previous behavior, you can specify the desired
physical bit manually when calling :meth:`.Layout.add`.
fixes:
- |
Fixed the behavior of :meth:`.Layout.add` which was potentially causing the
output of :meth:`~.transpile` to be invalid and contain more Qubits than
what was available on the target backend. Fixed:
`#8667 <https://github.com/Qiskit/qiskit-terra/issues/8667>`__

View File

@ -217,7 +217,7 @@ class LayoutTest(QiskitTestCase):
layout.add(self.qr[1], 2)
layout.add(self.qr[0])
self.assertDictEqual(layout.get_virtual_bits(), {self.qr[0]: 1, self.qr[1]: 2})
self.assertDictEqual(layout.get_virtual_bits(), {self.qr[0]: 0, self.qr[1]: 2})
def test_layout_combine_smaller(self):
"""combine_into_edge_map() method with another_layout is smaller and raises an Error"""

View File

@ -13,10 +13,12 @@
"""Test the VF2Layout pass"""
import unittest
from math import pi
import numpy
import retworkx
from qiskit import QuantumRegister, QuantumCircuit
from qiskit import QuantumRegister, QuantumCircuit, ClassicalRegister
from qiskit.transpiler import CouplingMap, Target, TranspilerError
from qiskit.transpiler.passes.layout.vf2_layout import VF2Layout, VF2LayoutStopReason
from qiskit.converters import circuit_to_dag
@ -26,8 +28,11 @@ from qiskit.providers.fake_provider import (
FakeRueschlikon,
FakeManhattan,
FakeYorktown,
FakeGuadalupeV2,
)
from qiskit.circuit.library import GraphState, CXGate
from qiskit.transpiler import PassManager
from qiskit.transpiler.preset_passmanagers.common import generate_embed_passmanager
class LayoutTestCase(QiskitTestCase):
@ -497,7 +502,7 @@ class TestMultipleTrials(QiskitTestCase):
"DEBUG:qiskit.transpiler.passes.layout.vf2_layout:Trial 159 is >= configured max trials 159",
cm.output,
)
self.assertEqual(set(property_set["layout"].get_physical_bits()), {49, 40, 58, 3, 4})
self.assertEqual(set(property_set["layout"].get_physical_bits()), {49, 40, 58, 0, 1})
def test_no_limits_with_negative(self):
"""Test that we're not enforcing a trial limit if set to negative."""
@ -518,7 +523,48 @@ class TestMultipleTrials(QiskitTestCase):
vf2_pass(qc, property_set)
for output in cm.output:
self.assertNotIn("is >= configured max trials", output)
self.assertEqual(set(property_set["layout"].get_physical_bits()), {3, 1, 2})
self.assertEqual(set(property_set["layout"].get_physical_bits()), {3, 1, 0})
def test_qregs_valid_layout_output(self):
"""Test that vf2 layout doesn't add extra qubits.
Reproduce from https://github.com/Qiskit/qiskit-terra/issues/8667
"""
backend = FakeGuadalupeV2()
qr = QuantumRegister(16, name="qr")
cr = ClassicalRegister(5)
qc = QuantumCircuit(qr, cr)
qc.rz(pi / 2, qr[0])
qc.sx(qr[0])
qc.sx(qr[1])
qc.rz(-pi / 4, qr[1])
qc.sx(qr[1])
qc.rz(pi / 2, qr[1])
qc.rz(2.8272143, qr[0])
qc.rz(0.43324854, qr[1])
qc.sx(qr[1])
qc.rz(-0.95531662, qr[7])
qc.sx(qr[7])
qc.rz(3 * pi / 4, qr[7])
qc.barrier([qr[1], qr[10], qr[4], qr[0], qr[7]])
vf2_pass = VF2Layout(
seed=12345,
target=backend.target,
)
vf2_pass(qc)
self.assertEqual(len(vf2_pass.property_set["layout"].get_physical_bits()), 16)
self.assertEqual(len(vf2_pass.property_set["layout"].get_virtual_bits()), 16)
pm = PassManager(
[
VF2Layout(
seed=12345,
target=backend.target,
)
]
)
pm += generate_embed_passmanager(backend.coupling_map)
res = pm.run(qc)
self.assertEqual(res.num_qubits, 16)
if __name__ == "__main__":