Skip to content

Commit

Permalink
Sabre layout and routing transpiler passes (Qiskit#4537)
Browse files Browse the repository at this point in the history
* 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>
  • Loading branch information
3 people authored Jun 23, 2020
1 parent 59b8b30 commit d566dc1
Show file tree
Hide file tree
Showing 25 changed files with 785 additions and 36 deletions.
6 changes: 3 additions & 3 deletions .pylintrc
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / stateme
# pi = the PI constant
# op = operation iterator
# b = basis iterator
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,
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,
__unittest,iSwapGate

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

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

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

# Regular expression matching correct class attribute names
class-attribute-rgx=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$
Expand Down
4 changes: 2 additions & 2 deletions qiskit/compiler/transpile.py
Original file line number Diff line number Diff line change
Expand Up @@ -115,10 +115,10 @@ def transpile(circuits: Union[QuantumCircuit, List[QuantumCircuit]],
[qr[0], None, None, qr[1], None, qr[2]]
layout_method: Name of layout selection pass ('trivial', 'dense', 'noise_adaptive')
layout_method: Name of layout selection pass ('trivial', 'dense', 'noise_adaptive', 'sabre')
Sometimes a perfect layout can be available in which case the layout_method
may not run.
routing_method: Name of routing pass ('basic', 'lookahead', 'stochastic')
routing_method: Name of routing pass ('basic', 'lookahead', 'stochastic', 'sabre')
seed_transpiler: Sets random seed for the stochastic parts of the transpiler
optimization_level: How much optimization to perform on the circuits.
Higher levels generate more optimized circuits,
Expand Down
23 changes: 18 additions & 5 deletions qiskit/dagcircuit/dagcircuit.py
Original file line number Diff line number Diff line change
Expand Up @@ -1124,7 +1124,7 @@ def bfs_successors(self, node):

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

def front_layer(self):
"""Return a list of op nodes in the first layer of this dag.
"""
graph_layers = self.multigraph_layers()
try:
next(graph_layers) # Remove input nodes
except StopIteration:
return []

op_nodes = [node for node in next(graph_layers) if node.type == "op"]

return op_nodes

def layers(self):
"""Yield a shallow view on a layer of this DAGCircuit for all d layers of this circuit.
Expand All @@ -1192,9 +1205,9 @@ def layers(self):
greedy algorithm. Each returned layer is a dict containing
{"graph": circuit graph, "partition": list of qubit lists}.
New but semantically equivalent DAGNodes will be included in the returned layers,
NOT the DAGNodes from the original DAG. The original vs. new nodes can be compared using
DAGNode.semantic_eq(node1, node2).
The returned layer contains new (but semantically equivalent) DAGNodes.
These are not the same as nodes of the original dag, but are equivalent
via DAGNode.semantic_eq(node1, node2).
TODO: Gates that use the same cbits will end up in different
layers as this is currently implemented. This may not be
Expand All @@ -1214,7 +1227,7 @@ def layers(self):
# Sort to make sure they are in the order they were added to the original DAG
# It has to be done by node_id as graph_layer is just a list of nodes
# with no implied topology
# Drawing tools that rely on _node_id to infer order of node creation
# Drawing tools rely on _node_id to infer order of node creation
# so we need this to be preserved by layers()
op_nodes.sort(key=lambda nd: nd._node_id)

Expand Down
19 changes: 19 additions & 0 deletions qiskit/transpiler/coupling.py
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,14 @@ def is_connected(self):
except nx.exception.NetworkXException:
return False

def neighbors(self, physical_qubit):
"""Return the nearest neighbors of a physical qubit.
Directionality matters, i.e. a neighbor must be reachable
by going one hop in the direction of an edge.
"""
return self.graph.neighbors(physical_qubit)

def _compute_distance_matrix(self):
"""Compute the full distance matrix on pairs of nodes.
Expand Down Expand Up @@ -201,6 +209,17 @@ def is_symmetric(self):
self._is_symmetric = self._check_symmetry()
return self._is_symmetric

def make_symmetric(self):
"""
Convert uni-directional edges into bi-directional.
"""
edges = self.get_edges()
for src, dest in edges:
if (dest, src) not in edges:
self.add_edge(dest, src)
self._dist_matrix = None # invalidate
self._is_symmetric = None # invalidate

def _check_symmetry(self):
"""
Calculates symmetry
Expand Down
5 changes: 0 additions & 5 deletions qiskit/transpiler/layout.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@
Virtual (qu)bits are tuples, e.g. `(QuantumRegister(3, 'qr'), 2)` or simply `qr[2]`.
Physical (qu)bits are integers.
"""
import warnings

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

for virtual, physical in self.get_virtual_bits().items():
Expand Down
4 changes: 4 additions & 0 deletions qiskit/transpiler/passes/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
TrivialLayout
DenseLayout
NoiseAdaptiveLayout
SabreLayout
CSPLayout
ApplyLayout
Layout2qDistance
Expand All @@ -44,6 +45,7 @@
BasicSwap
LookaheadSwap
StochasticSwap
SabreSwap
Basis Change
============
Expand Down Expand Up @@ -108,6 +110,7 @@
from .layout import TrivialLayout
from .layout import DenseLayout
from .layout import NoiseAdaptiveLayout
from .layout import SabreLayout
from .layout import CSPLayout
from .layout import ApplyLayout
from .layout import Layout2qDistance
Expand All @@ -119,6 +122,7 @@
from .routing import LayoutTransformation
from .routing import LookaheadSwap
from .routing import StochasticSwap
from .routing import SabreSwap

# basis change
from .basis import Decompose
Expand Down
1 change: 1 addition & 0 deletions qiskit/transpiler/passes/layout/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
from .trivial_layout import TrivialLayout
from .dense_layout import DenseLayout
from .noise_adaptive_layout import NoiseAdaptiveLayout
from .sabre_layout import SabreLayout
from .csp_layout import CSPLayout
from .apply_layout import ApplyLayout
from .layout_2q_distance import Layout2qDistance
Expand Down
4 changes: 2 additions & 2 deletions qiskit/transpiler/passes/layout/csp_layout.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,8 +74,8 @@ def __init__(self, coupling_map, strict_direction=False, seed=None, call_limit=1
time_limit=10):
"""If possible, chooses a Layout as a CSP, using backtracking.
If not possible, does not set the layout property. In all the cases, the property
:meth:`qiskit.transpiler.passes.CSPLayout_stop_reason` will be added with one of the
If not possible, does not set the layout property. In all the cases,
the property `CSPLayout_stop_reason` will be added with one of the
following values:
* solution found: If a perfect layout was found.
Expand Down
147 changes: 147 additions & 0 deletions qiskit/transpiler/passes/layout/sabre_layout.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
# -*- coding: utf-8 -*-

# This code is part of Qiskit.
#
# (C) Copyright IBM 2017, 2020.
#
# 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.

"""Layout selection using the SABRE bidirectional search approach from Li et al.
"""

import logging
import numpy as np

from qiskit.converters import dag_to_circuit
from qiskit.transpiler.passes.layout.set_layout import SetLayout
from qiskit.transpiler.passes.layout.full_ancilla_allocation import FullAncillaAllocation
from qiskit.transpiler.passes.layout.enlarge_with_ancilla import EnlargeWithAncilla
from qiskit.transpiler.passes.layout.apply_layout import ApplyLayout
from qiskit.transpiler.passes.routing import SabreSwap
from qiskit.transpiler.passmanager import PassManager
from qiskit.transpiler.layout import Layout
from qiskit.transpiler.basepasses import AnalysisPass
from qiskit.transpiler.exceptions import TranspilerError

logger = logging.getLogger(__name__)


class SabreLayout(AnalysisPass):
"""Choose a Layout via iterative bidirectional routing of the input circuit.
Starting with a random initial `Layout`, the algorithm does a full routing
of the circuit (via the `routing_pass` method) to end up with a
`final_layout`. This final_layout is then used as the initial_layout for
routing the reverse circuit. The algorithm iterates a number of times until
it finds an initial_layout that reduces full routing cost.
This method exploits the reversibility of quantum circuits, and tries to
include global circuit information in the choice of initial_layout.
**References:**
[1] Li, Gushu, Yufei Ding, and Yuan Xie. "Tackling the qubit mapping problem
for NISQ-era quantum devices." ASPLOS 2019.
`arXiv:1809.02573 <https://arxiv.org/pdf/1809.02573.pdf>`_
"""

def __init__(self, coupling_map, routing_pass=None, seed=None,
max_iterations=3):
"""SabreLayout initializer.
Args:
coupling_map (Coupling): directed graph representing a coupling map.
routing_pass (BasePass): the routing pass to use while iterating.
seed (int): seed for setting a random first trial layout.
max_iterations (int): number of forward-backward iterations.
"""
super().__init__()
self.coupling_map = coupling_map
self.routing_pass = routing_pass
self.seed = seed
self.max_iterations = max_iterations

def run(self, dag):
"""Run the SabreLayout pass on `dag`.
Args:
dag (DAGCircuit): DAG to find layout for.
Raises:
TranspilerError: if dag wider than self.coupling_map
"""
if len(dag.qubits) > self.coupling_map.size():
raise TranspilerError('More virtual qubits exist than physical.')

# Choose a random initial_layout.
if self.seed is None:
self.seed = np.random.randint(0, np.iinfo(np.int32).max)
rng = np.random.default_rng(self.seed)

physical_qubits = rng.choice(self.coupling_map.size(),
len(dag.qubits), replace=False)
physical_qubits = rng.permutation(physical_qubits)
initial_layout = Layout({q: dag.qubits[i]
for i, q in enumerate(physical_qubits)})

if self.routing_pass is None:
self.routing_pass = SabreSwap(self.coupling_map, 'decay')

# Do forward-backward iterations.
circ = dag_to_circuit(dag)
for i in range(self.max_iterations):
for _ in ('forward', 'backward'):
pm = self._layout_and_route_passmanager(initial_layout)
new_circ = pm.run(circ)

# Update initial layout and reverse the unmapped circuit.
pass_final_layout = pm.property_set['final_layout']
final_layout = self._compose_layouts(initial_layout,
pass_final_layout,
circ.qregs)
initial_layout = final_layout
circ = circ.reverse_ops()

# Diagnostics
logger.info('After round %d, num_swaps: %d',
i+1, new_circ.count_ops().get('swap', 0))
logger.info('new initial layout')
logger.info(initial_layout)

self.property_set['layout'] = initial_layout

def _layout_and_route_passmanager(self, initial_layout):
"""Return a passmanager for a full layout and routing.
We use a factory to remove potential statefulness of passes.
"""
layout_and_route = [SetLayout(initial_layout),
FullAncillaAllocation(self.coupling_map),
EnlargeWithAncilla(),
ApplyLayout(),
self.routing_pass]
pm = PassManager(layout_and_route)
return pm

def _compose_layouts(self, initial_layout, pass_final_layout, qregs):
"""Return the real final_layout resulting from the composition
of an initial_layout with the final_layout reported by a pass.
The routing passes internally start with a trivial layout, as the
layout gets applied to the circuit prior to running them. So the
"final_layout" they report must be amended to account for the actual
initial_layout that was selected.
"""
trivial_layout = Layout.generate_trivial_layout(*qregs)
pass_final_layout = Layout({trivial_layout[v.index]: p
for v, p in pass_final_layout.get_virtual_bits().items()})
qubit_map = Layout.combine_into_edge_map(initial_layout, trivial_layout)
final_layout = {v: pass_final_layout[qubit_map[v]]
for v, _ in initial_layout.get_virtual_bits().items()}
return Layout(final_layout)
1 change: 1 addition & 0 deletions qiskit/transpiler/passes/routing/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,4 @@
from .layout_transformation import LayoutTransformation
from .lookahead_swap import LookaheadSwap
from .stochastic_swap import StochasticSwap
from .sabre_swap import SabreSwap
2 changes: 1 addition & 1 deletion qiskit/transpiler/passes/routing/lookahead_swap.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
from qiskit.transpiler.layout import Layout
from qiskit.dagcircuit import DAGNode

logger = logging.getLogger()
logger = logging.getLogger(__name__)


class LookaheadSwap(TransformationPass):
Expand Down
Loading

0 comments on commit d566dc1

Please sign in to comment.