Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Sabre layout and routing transpiler passes #4537

Merged
merged 36 commits into from
Jun 23, 2020
Merged
Show file tree
Hide file tree
Changes from 28 commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
350d848
add SABRE swap pass
ajavadia Apr 30, 2020
a08323d
add SABRE layout bidirectional search pass
ajavadia May 3, 2020
5fb7a47
expose sabre via preset passmanagers
ajavadia May 5, 2020
6591ba1
undo deprecation for Layout.combine_into_edge_map
ajavadia May 5, 2020
c7fffb2
add Approx2qDecompose and SimplifyU3 passes
ajavadia May 5, 2020
c93ee32
allow synthesis_fidelity in global transpile options
ajavadia May 6, 2020
7597258
stopgap fix for circuits with regs in sabre_layout
ajavadia May 7, 2020
3f63290
add test
ajavadia May 31, 2020
61d076f
add tests
ajavadia Jun 1, 2020
5a82572
clean up sabre swap
ajavadia Jun 2, 2020
2fcbac1
restore lost qasm test files
ajavadia Jun 2, 2020
dc57e59
fix tests
ajavadia Jun 2, 2020
b35c025
leave SimplifyU3 for later
ajavadia Jun 2, 2020
9257639
leave Approx2qDecompose for later
ajavadia Jun 2, 2020
7c0dcd3
Release notes
ajavadia Jun 2, 2020
adeccf4
lint
ajavadia Jun 4, 2020
2afd8d5
update level 3
ajavadia Jun 5, 2020
a4e2637
lint
ajavadia Jun 19, 2020
8b82fe7
lint relax
ajavadia Jun 19, 2020
394eeda
regenerate mapper tests
ajavadia Jun 19, 2020
feaa4ec
make set to list conversion deterministic
ajavadia Jun 19, 2020
7cb2861
cleaning the diff a bit
Jun 19, 2020
a9decf2
test.python.transpiler.test_coupling.CouplingTest.test_make_symmetric
Jun 19, 2020
33f6e40
make randomization of SabreSwap controllable via seed
ajavadia Jun 19, 2020
574b86b
control randomization of SabreSwap via seed
ajavadia Jun 20, 2020
cb7ef39
move imports
ajavadia Jun 20, 2020
010c09a
test.python.transpiler.test_coupling.CouplingTest.test_neighbors
Jun 20, 2020
5527c05
test.python.dagcircuit.test_dagcircuit.TestDagNodeSelection.test_fron…
Jun 20, 2020
c41221e
fix doc
Jun 20, 2020
444cc70
Update test/python/transpiler/test_sabre_swap.py
ajavadia Jun 20, 2020
0dc1e45
Update qiskit/transpiler/passes/routing/sabre_swap.py
ajavadia Jun 20, 2020
e65fff2
add note and test for neighbors
ajavadia Jun 20, 2020
9333b9d
lint
ajavadia Jun 22, 2020
20518a2
release note
ajavadia Jun 22, 2020
a4895b0
Merge branch 'master' into sabre
Jun 23, 2020
6f57d66
Merge branch 'master' into sabre
Jun 23, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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):
1ucian0 marked this conversation as resolved.
Show resolved Hide resolved
"""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
16 changes: 16 additions & 0 deletions qiskit/transpiler/coupling.py
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,11 @@ def is_connected(self):
except nx.exception.NetworkXException:
return False

def neighbors(self, physical_qubit):
ajavadia marked this conversation as resolved.
Show resolved Hide resolved
"""Return the nearest neighbors of a physical qubit.
"""
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 +206,17 @@ def is_symmetric(self):
self._is_symmetric = self._check_symmetry()
return self._is_symmetric

def make_symmetric(self):
ajavadia marked this conversation as resolved.
Show resolved Hide resolved
"""
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)
ajavadia marked this conversation as resolved.
Show resolved Hide resolved
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
"""
def _layout_and_route_passmanager(initial_layout):
ajavadia marked this conversation as resolved.
Show resolved Hide resolved
"""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),
ajavadia marked this conversation as resolved.
Show resolved Hide resolved
FullAncillaAllocation(self.coupling_map),
EnlargeWithAncilla(),
ApplyLayout(),
self.routing_pass]
pm = PassManager(layout_and_route)
return pm

def _compose_layouts(initial_layout, pass_final_layout, qregs):
ajavadia marked this conversation as resolved.
Show resolved Hide resolved
"""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)

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)
ajavadia marked this conversation as resolved.
Show resolved Hide resolved

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)
ajavadia marked this conversation as resolved.
Show resolved Hide resolved
for i in range(self.max_iterations):
for _ in ('forward', 'backward'):
pm = _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 = _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
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