Skip to content

Commit fab61d2

Browse files
ajavadiaskywalker20121ucian0
authored
Sabre layout and routing transpiler passes (Qiskit#4537)
* add SABRE swap pass * add SABRE layout bidirectional search pass * expose sabre via preset passmanagers * undo deprecation for Layout.combine_into_edge_map * add Approx2qDecompose and SimplifyU3 passes * allow synthesis_fidelity in global transpile options * stopgap fix for circuits with regs in sabre_layout * add test * add tests * clean up sabre swap * restore lost qasm test files * fix tests * leave SimplifyU3 for later * leave Approx2qDecompose for later * Release notes Co-authored-by: Gushu Li <Skywalker2012@users.noreply.github.com> * lint * update level 3 * lint * lint relax * regenerate mapper tests * make set to list conversion deterministic * cleaning the diff a bit * test.python.transpiler.test_coupling.CouplingTest.test_make_symmetric * make randomization of SabreSwap controllable via seed * control randomization of SabreSwap via seed * move imports * test.python.transpiler.test_coupling.CouplingTest.test_neighbors * test.python.dagcircuit.test_dagcircuit.TestDagNodeSelection.test_front_layer * fix doc * Update test/python/transpiler/test_sabre_swap.py Co-authored-by: Luciano Bello <luciano.bello@ibm.com> * Update qiskit/transpiler/passes/routing/sabre_swap.py Co-authored-by: Luciano Bello <luciano.bello@ibm.com> * add note and test for neighbors * lint * release note Co-authored-by: Gushu Li <Skywalker2012@users.noreply.github.com> Co-authored-by: Luciano Bello <luciano.bello@ibm.com>
1 parent 6aca34a commit fab61d2

25 files changed

Lines changed: 785 additions & 36 deletions

.pylintrc

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / stateme
116116
# pi = the PI constant
117117
# op = operation iterator
118118
# b = basis iterator
119-
good-names=i,j,k,d,n,m,ex,v,w,x,y,z,Run,_,logger,q,c,r,qr,cr,qc,nd,pi,op,b,ar,br,
119+
good-names=a,b,i,j,k,d,n,m,ex,v,w,x,y,z,Run,_,logger,q,c,r,qr,cr,qc,nd,pi,op,b,ar,br,
120120
__unittest,iSwapGate
121121

122122
# Bad variable names which should always be refused, separated by a comma
@@ -176,10 +176,10 @@ argument-rgx=[a-z_][a-z0-9_]{2,30}|ax|dt$
176176
argument-name-hint=[a-z_][a-z0-9_]{2,30}$
177177

178178
# Regular expression matching correct variable names
179-
variable-rgx=[a-z_][a-z0-9_]{2,30}$
179+
variable-rgx=[a-z_][a-z0-9_]{1,30}$
180180

181181
# Naming hint for variable names
182-
variable-name-hint=[a-z_][a-z0-9_]{2,30}$
182+
variable-name-hint=[a-z_][a-z0-9_]{1,30}$
183183

184184
# Regular expression matching correct class attribute names
185185
class-attribute-rgx=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$

qiskit/compiler/transpile.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -115,10 +115,10 @@ def transpile(circuits: Union[QuantumCircuit, List[QuantumCircuit]],
115115
116116
[qr[0], None, None, qr[1], None, qr[2]]
117117
118-
layout_method: Name of layout selection pass ('trivial', 'dense', 'noise_adaptive')
118+
layout_method: Name of layout selection pass ('trivial', 'dense', 'noise_adaptive', 'sabre')
119119
Sometimes a perfect layout can be available in which case the layout_method
120120
may not run.
121-
routing_method: Name of routing pass ('basic', 'lookahead', 'stochastic')
121+
routing_method: Name of routing pass ('basic', 'lookahead', 'stochastic', 'sabre')
122122
seed_transpiler: Sets random seed for the stochastic parts of the transpiler
123123
optimization_level: How much optimization to perform on the circuits.
124124
Higher levels generate more optimized circuits,

qiskit/dagcircuit/dagcircuit.py

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1124,7 +1124,7 @@ def bfs_successors(self, node):
11241124

11251125
def quantum_successors(self, node):
11261126
"""Returns iterator of the successors of a node that are
1127-
connected by a quantum edge as DAGNodes."""
1127+
connected by a qubit edge."""
11281128
for successor in self.successors(node):
11291129
if any(isinstance(x['wire'], Qubit)
11301130
for x in
@@ -1182,6 +1182,19 @@ def remove_nondescendants_of(self, node):
11821182
if n.type == "op":
11831183
self.remove_op_node(n)
11841184

1185+
def front_layer(self):
1186+
"""Return a list of op nodes in the first layer of this dag.
1187+
"""
1188+
graph_layers = self.multigraph_layers()
1189+
try:
1190+
next(graph_layers) # Remove input nodes
1191+
except StopIteration:
1192+
return []
1193+
1194+
op_nodes = [node for node in next(graph_layers) if node.type == "op"]
1195+
1196+
return op_nodes
1197+
11851198
def layers(self):
11861199
"""Yield a shallow view on a layer of this DAGCircuit for all d layers of this circuit.
11871200
@@ -1192,9 +1205,9 @@ def layers(self):
11921205
greedy algorithm. Each returned layer is a dict containing
11931206
{"graph": circuit graph, "partition": list of qubit lists}.
11941207
1195-
New but semantically equivalent DAGNodes will be included in the returned layers,
1196-
NOT the DAGNodes from the original DAG. The original vs. new nodes can be compared using
1197-
DAGNode.semantic_eq(node1, node2).
1208+
The returned layer contains new (but semantically equivalent) DAGNodes.
1209+
These are not the same as nodes of the original dag, but are equivalent
1210+
via DAGNode.semantic_eq(node1, node2).
11981211
11991212
TODO: Gates that use the same cbits will end up in different
12001213
layers as this is currently implemented. This may not be
@@ -1214,7 +1227,7 @@ def layers(self):
12141227
# Sort to make sure they are in the order they were added to the original DAG
12151228
# It has to be done by node_id as graph_layer is just a list of nodes
12161229
# with no implied topology
1217-
# Drawing tools that rely on _node_id to infer order of node creation
1230+
# Drawing tools rely on _node_id to infer order of node creation
12181231
# so we need this to be preserved by layers()
12191232
op_nodes.sort(key=lambda nd: nd._node_id)
12201233

qiskit/transpiler/coupling.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,14 @@ def is_connected(self):
134134
except nx.exception.NetworkXException:
135135
return False
136136

137+
def neighbors(self, physical_qubit):
138+
"""Return the nearest neighbors of a physical qubit.
139+
140+
Directionality matters, i.e. a neighbor must be reachable
141+
by going one hop in the direction of an edge.
142+
"""
143+
return self.graph.neighbors(physical_qubit)
144+
137145
def _compute_distance_matrix(self):
138146
"""Compute the full distance matrix on pairs of nodes.
139147
@@ -201,6 +209,17 @@ def is_symmetric(self):
201209
self._is_symmetric = self._check_symmetry()
202210
return self._is_symmetric
203211

212+
def make_symmetric(self):
213+
"""
214+
Convert uni-directional edges into bi-directional.
215+
"""
216+
edges = self.get_edges()
217+
for src, dest in edges:
218+
if (dest, src) not in edges:
219+
self.add_edge(dest, src)
220+
self._dist_matrix = None # invalidate
221+
self._is_symmetric = None # invalidate
222+
204223
def _check_symmetry(self):
205224
"""
206225
Calculates symmetry

qiskit/transpiler/layout.py

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@
1919
Virtual (qu)bits are tuples, e.g. `(QuantumRegister(3, 'qr'), 2)` or simply `qr[2]`.
2020
Physical (qu)bits are integers.
2121
"""
22-
import warnings
2322

2423
from qiskit.circuit.quantumregister import Qubit
2524
from qiskit.transpiler.exceptions import LayoutError
@@ -224,10 +223,6 @@ def combine_into_edge_map(self, another_layout):
224223
LayoutError: another_layout can be bigger than self, but not smaller.
225224
Otherwise, raises.
226225
"""
227-
warnings.warn('combine_into_edge_map is deprecated as of 0.14.0 and '
228-
'will be removed in a future release. Instead '
229-
'reorder_bits() should be used', DeprecationWarning,
230-
stacklevel=2)
231226
edge_map = dict()
232227

233228
for virtual, physical in self.get_virtual_bits().items():

qiskit/transpiler/passes/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
TrivialLayout
3030
DenseLayout
3131
NoiseAdaptiveLayout
32+
SabreLayout
3233
CSPLayout
3334
ApplyLayout
3435
Layout2qDistance
@@ -44,6 +45,7 @@
4445
BasicSwap
4546
LookaheadSwap
4647
StochasticSwap
48+
SabreSwap
4749
4850
Basis Change
4951
============
@@ -108,6 +110,7 @@
108110
from .layout import TrivialLayout
109111
from .layout import DenseLayout
110112
from .layout import NoiseAdaptiveLayout
113+
from .layout import SabreLayout
111114
from .layout import CSPLayout
112115
from .layout import ApplyLayout
113116
from .layout import Layout2qDistance
@@ -119,6 +122,7 @@
119122
from .routing import LayoutTransformation
120123
from .routing import LookaheadSwap
121124
from .routing import StochasticSwap
125+
from .routing import SabreSwap
122126

123127
# basis change
124128
from .basis import Decompose

qiskit/transpiler/passes/layout/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
from .trivial_layout import TrivialLayout
1919
from .dense_layout import DenseLayout
2020
from .noise_adaptive_layout import NoiseAdaptiveLayout
21+
from .sabre_layout import SabreLayout
2122
from .csp_layout import CSPLayout
2223
from .apply_layout import ApplyLayout
2324
from .layout_2q_distance import Layout2qDistance

qiskit/transpiler/passes/layout/csp_layout.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -74,8 +74,8 @@ def __init__(self, coupling_map, strict_direction=False, seed=None, call_limit=1
7474
time_limit=10):
7575
"""If possible, chooses a Layout as a CSP, using backtracking.
7676
77-
If not possible, does not set the layout property. In all the cases, the property
78-
:meth:`qiskit.transpiler.passes.CSPLayout_stop_reason` will be added with one of the
77+
If not possible, does not set the layout property. In all the cases,
78+
the property `CSPLayout_stop_reason` will be added with one of the
7979
following values:
8080
8181
* solution found: If a perfect layout was found.
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
# -*- coding: utf-8 -*-
2+
3+
# This code is part of Qiskit.
4+
#
5+
# (C) Copyright IBM 2017, 2020.
6+
#
7+
# This code is licensed under the Apache License, Version 2.0. You may
8+
# obtain a copy of this license in the LICENSE.txt file in the root directory
9+
# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.
10+
#
11+
# Any modifications or derivative works of this code must retain this
12+
# copyright notice, and modified files need to carry a notice indicating
13+
# that they have been altered from the originals.
14+
15+
"""Layout selection using the SABRE bidirectional search approach from Li et al.
16+
"""
17+
18+
import logging
19+
import numpy as np
20+
21+
from qiskit.converters import dag_to_circuit
22+
from qiskit.transpiler.passes.layout.set_layout import SetLayout
23+
from qiskit.transpiler.passes.layout.full_ancilla_allocation import FullAncillaAllocation
24+
from qiskit.transpiler.passes.layout.enlarge_with_ancilla import EnlargeWithAncilla
25+
from qiskit.transpiler.passes.layout.apply_layout import ApplyLayout
26+
from qiskit.transpiler.passes.routing import SabreSwap
27+
from qiskit.transpiler.passmanager import PassManager
28+
from qiskit.transpiler.layout import Layout
29+
from qiskit.transpiler.basepasses import AnalysisPass
30+
from qiskit.transpiler.exceptions import TranspilerError
31+
32+
logger = logging.getLogger(__name__)
33+
34+
35+
class SabreLayout(AnalysisPass):
36+
"""Choose a Layout via iterative bidirectional routing of the input circuit.
37+
38+
Starting with a random initial `Layout`, the algorithm does a full routing
39+
of the circuit (via the `routing_pass` method) to end up with a
40+
`final_layout`. This final_layout is then used as the initial_layout for
41+
routing the reverse circuit. The algorithm iterates a number of times until
42+
it finds an initial_layout that reduces full routing cost.
43+
44+
This method exploits the reversibility of quantum circuits, and tries to
45+
include global circuit information in the choice of initial_layout.
46+
47+
**References:**
48+
49+
[1] Li, Gushu, Yufei Ding, and Yuan Xie. "Tackling the qubit mapping problem
50+
for NISQ-era quantum devices." ASPLOS 2019.
51+
`arXiv:1809.02573 <https://arxiv.org/pdf/1809.02573.pdf>`_
52+
"""
53+
54+
def __init__(self, coupling_map, routing_pass=None, seed=None,
55+
max_iterations=3):
56+
"""SabreLayout initializer.
57+
58+
Args:
59+
coupling_map (Coupling): directed graph representing a coupling map.
60+
routing_pass (BasePass): the routing pass to use while iterating.
61+
seed (int): seed for setting a random first trial layout.
62+
max_iterations (int): number of forward-backward iterations.
63+
"""
64+
super().__init__()
65+
self.coupling_map = coupling_map
66+
self.routing_pass = routing_pass
67+
self.seed = seed
68+
self.max_iterations = max_iterations
69+
70+
def run(self, dag):
71+
"""Run the SabreLayout pass on `dag`.
72+
73+
Args:
74+
dag (DAGCircuit): DAG to find layout for.
75+
76+
Raises:
77+
TranspilerError: if dag wider than self.coupling_map
78+
"""
79+
if len(dag.qubits) > self.coupling_map.size():
80+
raise TranspilerError('More virtual qubits exist than physical.')
81+
82+
# Choose a random initial_layout.
83+
if self.seed is None:
84+
self.seed = np.random.randint(0, np.iinfo(np.int32).max)
85+
rng = np.random.default_rng(self.seed)
86+
87+
physical_qubits = rng.choice(self.coupling_map.size(),
88+
len(dag.qubits), replace=False)
89+
physical_qubits = rng.permutation(physical_qubits)
90+
initial_layout = Layout({q: dag.qubits[i]
91+
for i, q in enumerate(physical_qubits)})
92+
93+
if self.routing_pass is None:
94+
self.routing_pass = SabreSwap(self.coupling_map, 'decay')
95+
96+
# Do forward-backward iterations.
97+
circ = dag_to_circuit(dag)
98+
for i in range(self.max_iterations):
99+
for _ in ('forward', 'backward'):
100+
pm = self._layout_and_route_passmanager(initial_layout)
101+
new_circ = pm.run(circ)
102+
103+
# Update initial layout and reverse the unmapped circuit.
104+
pass_final_layout = pm.property_set['final_layout']
105+
final_layout = self._compose_layouts(initial_layout,
106+
pass_final_layout,
107+
circ.qregs)
108+
initial_layout = final_layout
109+
circ = circ.reverse_ops()
110+
111+
# Diagnostics
112+
logger.info('After round %d, num_swaps: %d',
113+
i+1, new_circ.count_ops().get('swap', 0))
114+
logger.info('new initial layout')
115+
logger.info(initial_layout)
116+
117+
self.property_set['layout'] = initial_layout
118+
119+
def _layout_and_route_passmanager(self, initial_layout):
120+
"""Return a passmanager for a full layout and routing.
121+
122+
We use a factory to remove potential statefulness of passes.
123+
"""
124+
layout_and_route = [SetLayout(initial_layout),
125+
FullAncillaAllocation(self.coupling_map),
126+
EnlargeWithAncilla(),
127+
ApplyLayout(),
128+
self.routing_pass]
129+
pm = PassManager(layout_and_route)
130+
return pm
131+
132+
def _compose_layouts(self, initial_layout, pass_final_layout, qregs):
133+
"""Return the real final_layout resulting from the composition
134+
of an initial_layout with the final_layout reported by a pass.
135+
136+
The routing passes internally start with a trivial layout, as the
137+
layout gets applied to the circuit prior to running them. So the
138+
"final_layout" they report must be amended to account for the actual
139+
initial_layout that was selected.
140+
"""
141+
trivial_layout = Layout.generate_trivial_layout(*qregs)
142+
pass_final_layout = Layout({trivial_layout[v.index]: p
143+
for v, p in pass_final_layout.get_virtual_bits().items()})
144+
qubit_map = Layout.combine_into_edge_map(initial_layout, trivial_layout)
145+
final_layout = {v: pass_final_layout[qubit_map[v]]
146+
for v, _ in initial_layout.get_virtual_bits().items()}
147+
return Layout(final_layout)

qiskit/transpiler/passes/routing/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,3 +18,4 @@
1818
from .layout_transformation import LayoutTransformation
1919
from .lookahead_swap import LookaheadSwap
2020
from .stochastic_swap import StochasticSwap
21+
from .sabre_swap import SabreSwap

0 commit comments

Comments
 (0)