mirror of https://github.com/Qiskit/qiskit.git
Starting layout analysis pass for sabre (#10829)
* adding SabreStartingLayoutUsingVF2 analysis pass * fixing imports * bug fix * fixing test after changing some of the options * renaming * adding target; more renaming; tests * release notes * applying suggestions from code review * removing debug print * Update qiskit/transpiler/passes/layout/sabre_pre_layout.py I didn't know this existed :) Co-authored-by: Matthew Treinish <mtreinish@kortar.org> * adding missing : * collecting edges into a set * adjusting error_rate with respect to distance * letting coupling_map be either coupling map or target, for consistency with other passes * apply suggestions from code review * Update qiskit/transpiler/passes/layout/sabre_pre_layout.py Co-authored-by: Matthew Treinish <mtreinish@kortar.org> --------- Co-authored-by: Matthew Treinish <mtreinish@kortar.org>
This commit is contained in:
parent
3b97b3745e
commit
df9eae42c4
|
@ -34,6 +34,7 @@ Layout Selection (Placement)
|
|||
Layout2qDistance
|
||||
EnlargeWithAncilla
|
||||
FullAncillaAllocation
|
||||
SabrePreLayout
|
||||
|
||||
Routing
|
||||
=======
|
||||
|
@ -193,6 +194,7 @@ from .layout import ApplyLayout
|
|||
from .layout import Layout2qDistance
|
||||
from .layout import EnlargeWithAncilla
|
||||
from .layout import FullAncillaAllocation
|
||||
from .layout import SabrePreLayout
|
||||
|
||||
# routing
|
||||
from .routing import BasicSwap
|
||||
|
|
|
@ -24,3 +24,4 @@ from .apply_layout import ApplyLayout
|
|||
from .layout_2q_distance import Layout2qDistance
|
||||
from .enlarge_with_ancilla import EnlargeWithAncilla
|
||||
from .full_ancilla_allocation import FullAncillaAllocation
|
||||
from .sabre_pre_layout import SabrePreLayout
|
||||
|
|
|
@ -0,0 +1,217 @@
|
|||
# This code is part of Qiskit.
|
||||
#
|
||||
# (C) Copyright IBM 2023.
|
||||
#
|
||||
# This code is licensed under the Apache License, Version 2.0. You may
|
||||
# obtain a copy of this license in the LICENSE.txt file in the root directory
|
||||
# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.
|
||||
#
|
||||
# Any modifications or derivative works of this code must retain this
|
||||
# copyright notice, and modified files need to carry a notice indicating
|
||||
# that they have been altered from the originals.
|
||||
|
||||
"""Creating Sabre starting layouts."""
|
||||
|
||||
import itertools
|
||||
|
||||
from qiskit.transpiler import CouplingMap, Target, AnalysisPass, TranspilerError
|
||||
from qiskit.transpiler.passes.layout.vf2_layout import VF2Layout
|
||||
from qiskit._accelerate.error_map import ErrorMap
|
||||
|
||||
|
||||
class SabrePreLayout(AnalysisPass):
|
||||
"""Choose a starting layout to use for additional Sabre layout trials.
|
||||
|
||||
Property Set Values Written
|
||||
---------------------------
|
||||
|
||||
``sabre_starting_layouts`` (``list[Layout]``)
|
||||
An optional list of :class:`~.Layout` objects to use for additional Sabre layout trials.
|
||||
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coupling_map,
|
||||
max_distance=2,
|
||||
error_rate=0.1,
|
||||
max_trials_vf2=100,
|
||||
call_limit_vf2=None,
|
||||
improve_layout=True,
|
||||
):
|
||||
"""SabrePreLayout initializer.
|
||||
|
||||
The pass works by augmenting the coupling map with more and more "extra" edges
|
||||
until VF2 succeeds to find a perfect graph isomorphism. More precisely, the
|
||||
augmented coupling map contains edges between nodes that are within a given
|
||||
distance ``d`` in the original coupling map, and the value of ``d`` is increased
|
||||
until an isomorphism is found.
|
||||
|
||||
Intuitively, a better layout involves fewer extra edges. The pass also optionally
|
||||
minimizes the number of extra edges involved in the layout until a local minimum
|
||||
is found. This involves removing extra edges and running VF2 to see if an
|
||||
isomorphism still exists.
|
||||
|
||||
Args:
|
||||
coupling_map (Union[CouplingMap, Target]): directed graph representing the
|
||||
original coupling map or a target modelling the backend (including its
|
||||
connectivity).
|
||||
max_distance (int): the maximum distance to consider for augmented coupling maps.
|
||||
error_rate (float): the error rate to assign to the "extra" edges. A non-zero
|
||||
error rate prioritizes VF2 to choose original edges over extra edges.
|
||||
max_trials_vf2 (int): specifies the maximum number of VF2 trials. A larger number
|
||||
allows VF2 to explore more layouts, eventually choosing the one with the smallest
|
||||
error rate.
|
||||
call_limit_vf2 (int): limits each call to VF2 by bounding the number of VF2 state visits.
|
||||
improve_layout (bool): whether to improve the layout by minimizing the number of
|
||||
extra edges involved. This might be time-consuming as this requires additional
|
||||
VF2 calls.
|
||||
|
||||
Raises:
|
||||
TranspilerError: At runtime, if neither ``coupling_map`` or ``target`` are provided.
|
||||
"""
|
||||
|
||||
self.max_distance = max_distance
|
||||
self.error_rate = error_rate
|
||||
self.max_trials_vf2 = max_trials_vf2
|
||||
self.call_limit_vf2 = call_limit_vf2
|
||||
self.improve_layout = improve_layout
|
||||
|
||||
if isinstance(coupling_map, Target):
|
||||
self.target = coupling_map
|
||||
self.coupling_map = self.target.build_coupling_map()
|
||||
else:
|
||||
self.target = None
|
||||
self.coupling_map = coupling_map
|
||||
|
||||
super().__init__()
|
||||
|
||||
def run(self, dag):
|
||||
"""Run the SabrePreLayout pass on `dag`.
|
||||
|
||||
The discovered starting layout is written to the property set
|
||||
value ``sabre_starting_layouts``.
|
||||
|
||||
Args:
|
||||
dag (DAGCircuit): DAG to create starting layout for.
|
||||
"""
|
||||
|
||||
if self.coupling_map is None:
|
||||
raise TranspilerError(
|
||||
"SabrePreLayout requires coupling_map to be used with either"
|
||||
"CouplingMap or a Target."
|
||||
)
|
||||
|
||||
starting_layout = None
|
||||
cur_distance = 1
|
||||
while cur_distance <= self.max_distance:
|
||||
augmented_map, augmented_error_map = self._add_extra_edges(cur_distance)
|
||||
pass_ = VF2Layout(
|
||||
augmented_map,
|
||||
seed=0,
|
||||
max_trials=self.max_trials_vf2,
|
||||
call_limit=self.call_limit_vf2,
|
||||
)
|
||||
pass_.property_set["vf2_avg_error_map"] = augmented_error_map
|
||||
pass_.run(dag)
|
||||
|
||||
if "layout" in pass_.property_set:
|
||||
starting_layout = pass_.property_set["layout"]
|
||||
break
|
||||
|
||||
cur_distance += 1
|
||||
|
||||
if cur_distance > 1 and starting_layout is not None:
|
||||
# optionally improve starting layout
|
||||
if self.improve_layout:
|
||||
starting_layout = self._minimize_extra_edges(dag, starting_layout)
|
||||
# write discovered layout into the property set
|
||||
if "sabre_starting_layouts" not in self.property_set:
|
||||
self.property_set["sabre_starting_layouts"] = [starting_layout]
|
||||
else:
|
||||
self.property_set["sabre_starting_layouts"].append(starting_layout)
|
||||
|
||||
def _add_extra_edges(self, distance):
|
||||
"""Augments the coupling map with extra edges that connect nodes ``distance``
|
||||
apart in the original graph. The extra edges are assigned errors allowing VF2
|
||||
to prioritize real edges over extra edges.
|
||||
"""
|
||||
nq = len(self.coupling_map.graph)
|
||||
augmented_coupling_map = CouplingMap()
|
||||
augmented_coupling_map.graph = self.coupling_map.graph.copy()
|
||||
augmented_error_map = ErrorMap(nq)
|
||||
|
||||
for (x, y) in itertools.combinations(self.coupling_map.graph.node_indices(), 2):
|
||||
d = self.coupling_map.distance(x, y)
|
||||
if 1 < d <= distance:
|
||||
error_rate = 1 - ((1 - self.error_rate) ** d)
|
||||
augmented_coupling_map.add_edge(x, y)
|
||||
augmented_error_map.add_error((x, y), error_rate)
|
||||
augmented_coupling_map.add_edge(y, x)
|
||||
augmented_error_map.add_error((y, x), error_rate)
|
||||
|
||||
return augmented_coupling_map, augmented_error_map
|
||||
|
||||
def _get_extra_edges_used(self, dag, layout):
|
||||
"""Returns the set of extra edges involved in the layout."""
|
||||
extra_edges_used = set()
|
||||
virtual_bits = layout.get_virtual_bits()
|
||||
for node in dag.two_qubit_ops():
|
||||
p0 = virtual_bits[node.qargs[0]]
|
||||
p1 = virtual_bits[node.qargs[1]]
|
||||
if self.coupling_map.distance(p0, p1) > 1:
|
||||
extra_edge = (p0, p1) if p0 < p1 else (p1, p0)
|
||||
extra_edges_used.add(extra_edge)
|
||||
return extra_edges_used
|
||||
|
||||
def _find_layout(self, dag, edges):
|
||||
"""Checks if there is a layout for a given set of edges."""
|
||||
cm = CouplingMap(edges)
|
||||
pass_ = VF2Layout(cm, seed=0, max_trials=1, call_limit=self.call_limit_vf2)
|
||||
pass_.run(dag)
|
||||
return pass_.property_set.get("layout", None)
|
||||
|
||||
def _minimize_extra_edges(self, dag, starting_layout):
|
||||
"""Minimizes the set of extra edges involved in the layout. This iteratively
|
||||
removes extra edges from the coupling map and uses VF2 to check if a layout
|
||||
still exists. This is reasonably efficiently as it only looks for a local
|
||||
minimum.
|
||||
"""
|
||||
# compute the set of edges in the original coupling map
|
||||
real_edges = []
|
||||
for (x, y) in itertools.combinations(self.coupling_map.graph.node_indices(), 2):
|
||||
d = self.coupling_map.distance(x, y)
|
||||
if d == 1:
|
||||
real_edges.append((x, y))
|
||||
|
||||
best_layout = starting_layout
|
||||
|
||||
# keeps the set of "necessary" extra edges: without a necessary edge
|
||||
# a layout no longer exists
|
||||
extra_edges_necessary = []
|
||||
|
||||
extra_edges_unprocessed_set = self._get_extra_edges_used(dag, starting_layout)
|
||||
|
||||
while extra_edges_unprocessed_set:
|
||||
# choose some unprocessed edge
|
||||
edge_chosen = next(iter(extra_edges_unprocessed_set))
|
||||
extra_edges_unprocessed_set.remove(edge_chosen)
|
||||
|
||||
# check if a layout still exists without this edge
|
||||
layout = self._find_layout(
|
||||
dag, real_edges + extra_edges_necessary + list(extra_edges_unprocessed_set)
|
||||
)
|
||||
|
||||
if layout is None:
|
||||
# without this edge the layout either does not exist or is too hard to find
|
||||
extra_edges_necessary.append(edge_chosen)
|
||||
|
||||
else:
|
||||
# this edge is not necessary, furthermore we can trim the set of edges to examine based
|
||||
# in the edges involved in the layout.
|
||||
extra_edges_unprocessed_set = self._get_extra_edges_used(dag, layout).difference(
|
||||
set(extra_edges_necessary)
|
||||
)
|
||||
best_layout = layout
|
||||
|
||||
return best_layout
|
|
@ -0,0 +1,37 @@
|
|||
---
|
||||
features:
|
||||
- |
|
||||
Added a new analysis :class:`.SabrePreLayout` pass that creates a starting
|
||||
layout for :class:`.SabreLayout`, writing the layout into the property set
|
||||
value ``sabre_starting_layouts``.
|
||||
|
||||
The pass works by augmenting the coupling map with more and more "extra" edges
|
||||
until :class:`.VF2Layout` succeeds to find a perfect graph isomorphism.
|
||||
More precisely, the augmented coupling map contains edges between nodes that are
|
||||
within a given distance ``d`` in the original coupling map, and the value of ``d``
|
||||
is increased until an isomorphism is found. The pass also optionally minimizes
|
||||
the number of extra edges involved in the layout until a local minimum is found.
|
||||
This involves removing extra edges and calling :class:`.VF2Layout` to check if
|
||||
an isomorphism still exists.
|
||||
|
||||
Here is an example of calling the :class:`.SabrePreLayout` before :class:`.SabreLayout`::
|
||||
|
||||
import math
|
||||
from qiskit.transpiler import CouplingMap, PassManager
|
||||
from qiskit.circuit.library import EfficientSU2
|
||||
from qiskit.transpiler.passes import SabrePreLayout, SabreLayout
|
||||
|
||||
qc = EfficientSU2(16, entanglement='circular', reps=6, flatten=True)
|
||||
qc.assign_parameters([math.pi / 2] * len(qc.parameters), inplace=True)
|
||||
qc.measure_all()
|
||||
|
||||
coupling_map = CouplingMap.from_heavy_hex(7)
|
||||
|
||||
pm = PassManager(
|
||||
[
|
||||
SabrePreLayout(coupling_map=coupling_map),
|
||||
SabreLayout(coupling_map),
|
||||
]
|
||||
)
|
||||
|
||||
pm.run(qc)
|
|
@ -14,7 +14,10 @@
|
|||
|
||||
import unittest
|
||||
|
||||
import math
|
||||
|
||||
from qiskit import QuantumRegister, QuantumCircuit
|
||||
from qiskit.circuit.library import EfficientSU2
|
||||
from qiskit.transpiler import CouplingMap, AnalysisPass, PassManager
|
||||
from qiskit.transpiler.passes import SabreLayout, DenseLayout
|
||||
from qiskit.transpiler.exceptions import TranspilerError
|
||||
|
@ -24,6 +27,8 @@ from qiskit.compiler.transpiler import transpile
|
|||
from qiskit.providers.fake_provider import FakeAlmaden, FakeAlmadenV2
|
||||
from qiskit.providers.fake_provider import FakeKolkata
|
||||
from qiskit.providers.fake_provider import FakeMontreal
|
||||
from qiskit.transpiler.passes.layout.sabre_pre_layout import SabrePreLayout
|
||||
from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager
|
||||
|
||||
|
||||
class TestSabreLayout(QiskitTestCase):
|
||||
|
@ -389,5 +394,44 @@ class TestDisjointDeviceSabreLayout(QiskitTestCase):
|
|||
self.assertEqual([layout[q] for q in qc.qubits], [3, 1, 2, 5, 4, 6, 7, 8])
|
||||
|
||||
|
||||
class TestSabrePreLayout(QiskitTestCase):
|
||||
"""Tests the SabreLayout pass with starting layout created by SabrePreLayout."""
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
circuit = EfficientSU2(16, entanglement="circular", reps=6, flatten=True)
|
||||
circuit.assign_parameters([math.pi / 2] * len(circuit.parameters), inplace=True)
|
||||
circuit.measure_all()
|
||||
self.circuit = circuit
|
||||
self.coupling_map = CouplingMap.from_heavy_hex(7)
|
||||
|
||||
def test_starting_layout(self):
|
||||
"""Test that a starting layout is created and looks as expected."""
|
||||
pm = PassManager(
|
||||
[
|
||||
SabrePreLayout(coupling_map=self.coupling_map),
|
||||
SabreLayout(self.coupling_map, seed=123456, swap_trials=1, layout_trials=1),
|
||||
]
|
||||
)
|
||||
pm.run(self.circuit)
|
||||
layout = pm.property_set["layout"]
|
||||
self.assertEqual(
|
||||
[layout[q] for q in self.circuit.qubits],
|
||||
[30, 98, 104, 36, 103, 35, 65, 28, 61, 91, 22, 92, 23, 93, 62, 99],
|
||||
)
|
||||
|
||||
def test_integration_with_pass_manager(self):
|
||||
"""Tests SabrePreLayoutIntegration with the rest of PassManager pipeline."""
|
||||
backend = FakeAlmadenV2()
|
||||
pm = generate_preset_pass_manager(1, backend, seed_transpiler=0)
|
||||
pm.pre_layout = PassManager([SabrePreLayout(backend.target)])
|
||||
qct = pm.run(self.circuit)
|
||||
qct_initial_layout = qct.layout.initial_layout
|
||||
self.assertEqual(
|
||||
[qct_initial_layout[q] for q in self.circuit.qubits],
|
||||
[1, 6, 5, 10, 11, 12, 16, 17, 18, 13, 14, 9, 8, 3, 2, 0],
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
|
|
@ -0,0 +1,98 @@
|
|||
# This code is part of Qiskit.
|
||||
#
|
||||
# (C) Copyright IBM 2023.
|
||||
#
|
||||
# This code is licensed under the Apache License, Version 2.0. You may
|
||||
# obtain a copy of this license in the LICENSE.txt file in the root directory
|
||||
# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.
|
||||
#
|
||||
# Any modifications or derivative works of this code must retain this
|
||||
# copyright notice, and modified files need to carry a notice indicating
|
||||
# that they have been altered from the originals.
|
||||
|
||||
"""Test the SabrePreLayout pass"""
|
||||
|
||||
from qiskit.circuit import QuantumCircuit
|
||||
from qiskit.transpiler import TranspilerError, CouplingMap, PassManager
|
||||
from qiskit.transpiler.passes.layout.sabre_pre_layout import SabrePreLayout
|
||||
from qiskit.converters import circuit_to_dag
|
||||
from qiskit.test import QiskitTestCase
|
||||
|
||||
|
||||
class TestSabrePreLayout(QiskitTestCase):
|
||||
"""Tests the SabrePreLayout pass."""
|
||||
|
||||
def test_no_constraints(self):
|
||||
"""Test we raise at runtime if no target or coupling graph are provided."""
|
||||
qc = QuantumCircuit(2)
|
||||
empty_pass = SabrePreLayout(coupling_map=None)
|
||||
with self.assertRaises(TranspilerError):
|
||||
empty_pass.run(circuit_to_dag(qc))
|
||||
|
||||
def test_starting_layout_created(self):
|
||||
"""Test the case that no perfect layout exists and SabrePreLayout can find a
|
||||
starting layout."""
|
||||
qc = QuantumCircuit(4)
|
||||
qc.cx(0, 1)
|
||||
qc.cx(1, 2)
|
||||
qc.cx(2, 3)
|
||||
qc.cx(3, 0)
|
||||
coupling_map = CouplingMap.from_ring(5)
|
||||
pm = PassManager([SabrePreLayout(coupling_map=coupling_map)])
|
||||
pm.run(qc)
|
||||
|
||||
# SabrePreLayout should discover a single layout.
|
||||
self.assertIn("sabre_starting_layouts", pm.property_set)
|
||||
layouts = pm.property_set["sabre_starting_layouts"]
|
||||
self.assertEqual(len(layouts), 1)
|
||||
layout = layouts[0]
|
||||
self.assertEqual([layout[q] for q in qc.qubits], [2, 1, 0, 4])
|
||||
|
||||
def test_perfect_layout_exists(self):
|
||||
"""Test the case that a perfect layout exists."""
|
||||
qc = QuantumCircuit(4)
|
||||
qc.cx(0, 1)
|
||||
qc.cx(1, 2)
|
||||
qc.cx(2, 3)
|
||||
qc.cx(3, 0)
|
||||
coupling_map = CouplingMap.from_ring(4)
|
||||
pm = PassManager([SabrePreLayout(coupling_map=coupling_map)])
|
||||
pm.run(qc)
|
||||
|
||||
# SabrePreLayout should not create starting layouts when a perfect layout exists.
|
||||
self.assertNotIn("sabre_starting_layouts", pm.property_set)
|
||||
|
||||
def test_max_distance(self):
|
||||
"""Test the ``max_distance`` option to SabrePreLayout."""
|
||||
qc = QuantumCircuit(6)
|
||||
qc.cx(0, 1)
|
||||
qc.cx(0, 2)
|
||||
qc.cx(0, 3)
|
||||
qc.cx(0, 4)
|
||||
qc.cx(0, 5)
|
||||
coupling_map = CouplingMap.from_ring(6)
|
||||
|
||||
# It is not possible to map a star-graph with 5 leaves into a ring with 6 nodes,
|
||||
# so that all nodes are distance-2 apart.
|
||||
pm = PassManager([SabrePreLayout(coupling_map=coupling_map, max_distance=2)])
|
||||
pm.run(qc)
|
||||
self.assertNotIn("sabre_starting_layouts", pm.property_set)
|
||||
|
||||
# But possible with distance-3.
|
||||
pm = PassManager([SabrePreLayout(coupling_map=coupling_map, max_distance=3)])
|
||||
pm.run(qc)
|
||||
self.assertIn("sabre_starting_layouts", pm.property_set)
|
||||
|
||||
def test_call_limit_vf2(self):
|
||||
"""Test the ``call_limit_vf2`` option to SabrePreLayout."""
|
||||
qc = QuantumCircuit(4)
|
||||
qc.cx(0, 1)
|
||||
qc.cx(1, 2)
|
||||
qc.cx(2, 3)
|
||||
qc.cx(3, 0)
|
||||
coupling_map = CouplingMap.from_ring(5)
|
||||
pm = PassManager(
|
||||
[SabrePreLayout(coupling_map=coupling_map, call_limit_vf2=1, max_distance=3)]
|
||||
)
|
||||
pm.run(qc)
|
||||
self.assertNotIn("sabre_starting_layouts", pm.property_set)
|
Loading…
Reference in New Issue