Sabre layout and routing transpiler passes (#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>
This commit is contained in:
Ali Javadi-Abhari 2020-06-23 08:35:17 -04:00 committed by GitHub
parent 6aca34a9e9
commit fab61d21d1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 785 additions and 36 deletions

View File

@ -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
@ -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}|(__.*__))$

View File

@ -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,

View File

@ -1124,7 +1124,7 @@ class DAGCircuit:
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
@ -1182,6 +1182,19 @@ class DAGCircuit:
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.
@ -1192,9 +1205,9 @@ class DAGCircuit:
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
@ -1214,7 +1227,7 @@ class DAGCircuit:
# 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)

View File

@ -134,6 +134,14 @@ class CouplingMap:
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.
@ -201,6 +209,17 @@ class CouplingMap:
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

View File

@ -19,7 +19,6 @@ Layout is the relation between virtual (qu)bits and physical (qu)bits.
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
@ -224,10 +223,6 @@ class 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():

View File

@ -29,6 +29,7 @@ Layout Selection (Placement)
TrivialLayout
DenseLayout
NoiseAdaptiveLayout
SabreLayout
CSPLayout
ApplyLayout
Layout2qDistance
@ -44,6 +45,7 @@ Routing
BasicSwap
LookaheadSwap
StochasticSwap
SabreSwap
Basis Change
============
@ -108,6 +110,7 @@ from .layout import SetLayout
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
@ -119,6 +122,7 @@ from .routing import BasicSwap
from .routing import LayoutTransformation
from .routing import LookaheadSwap
from .routing import StochasticSwap
from .routing import SabreSwap
# basis change
from .basis import Decompose

View File

@ -18,6 +18,7 @@ from .set_layout import SetLayout
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

View File

@ -74,8 +74,8 @@ class CSPLayout(AnalysisPass):
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.

View File

@ -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)

View File

@ -18,3 +18,4 @@ from .basic_swap import BasicSwap
from .layout_transformation import LayoutTransformation
from .lookahead_swap import LookaheadSwap
from .stochastic_swap import StochasticSwap
from .sabre_swap import SabreSwap

View File

@ -25,7 +25,7 @@ from qiskit.transpiler.exceptions import TranspilerError
from qiskit.transpiler.layout import Layout
from qiskit.dagcircuit import DAGNode
logger = logging.getLogger()
logger = logging.getLogger(__name__)
class LookaheadSwap(TransformationPass):

View File

@ -0,0 +1,363 @@
# -*- 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.
"""Routing via SWAP insertion using the SABRE method from Li et al."""
import logging
from copy import deepcopy
from itertools import cycle
import numpy as np
from qiskit.dagcircuit import DAGCircuit
from qiskit.circuit.library.standard_gates import SwapGate
from qiskit.transpiler.basepasses import TransformationPass
from qiskit.transpiler.exceptions import TranspilerError
from qiskit.transpiler.layout import Layout
from qiskit.dagcircuit import DAGNode
logger = logging.getLogger(__name__)
EXTENDED_SET_SIZE = 20 # Size of lookahead window. TODO: set dynamically to len(current_layout)
EXTENDED_SET_WEIGHT = 0.5 # Weight of lookahead window compared to front_layer.
DECAY_RATE = 0.001 # Decay cooefficient for penalizing serial swaps.
DECAY_RESET_INTERVAL = 5 # How often to reset all decay rates to 1.
class SabreSwap(TransformationPass):
r"""Map input circuit onto a backend topology via insertion of SWAPs.
Implementation of the SWAP-based heuristic search from the SABRE qubit
mapping paper [1] (Algorithm 1). The hueristic aims to minimize the number
of lossy SWAPs inserted and the depth of the circuit.
This algorithm starts from an initial layout of virtual qubits onto physical
qubits, and iterates over the circuit DAG until all gates are exhausted,
inserting SWAPs along the way. It only considers 2-qubit gates as only those
are germane for the mapping problem (it is assumed that 3+ qubit gates are
already decomposed).
In each iteration, it will first check if there are any gates in the
``front_layer`` that can be directly applied. If so, it will apply them and
remove them from ``front_layer``, and replenish that layer with new gates
if possible. Otherwise, it will try to search for SWAPs, insert the SWAPs,
and update the mapping.
The search for SWAPs is restricted, in the sense that we only consider
physical qubits in the neighoborhood of those qubits involved in
``front_layer``. These give rise to a ``swap_candidate_list`` which is
scored according to some heuristic cost function. The best SWAP is
implemented and ``current_layout`` updated.
**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, heuristic='basic', seed=None):
r"""SabreSwap initializer.
Args:
coupling_map (CouplingMap): CouplingMap of the target backend.
heuristic (str): The type of heuristic to use when deciding best
swap strategy ('basic' or 'lookahead' or 'decay').
seed (int): random seed used to tie-break among candidate swaps.
Additional Information:
The search space of possible SWAPs on physical qubits is explored
by assigning a score to the layout that would result from each SWAP.
The goodness of a layout is evaluated based on how viable it makes
the remaining virtual gates that must be applied. A few heuristic
cost functions are supported
- 'basic':
The sum of distances for corresponding physical qubits of
interacting virtual qubits in the front_layer.
.. math::
H_{basic} = \sum_{gate \in F} D[\pi(gate.q_1)][\pi(gate.q2)]
- 'lookahead':
This is the sum of two costs: first is the same as the basic cost.
Second is the basic cost but now evaluated for the
extended set as well (i.e. :math:`|E|` number of upcoming successors to gates in
front_layer F). This is weighted by some amount EXTENDED_SET_WEIGHT (W) to
signify that upcoming gates are less important that the front_layer.
.. math::
H_{decay} = \frac{1}{\abs{F}} \sum_{gate \in F} D[\pi(gate.q_1)][\pi(gate.q2)]
+ W * \frac{1}{\abs{E}} \sum_{gate \in E} D[\pi(gate.q_1)][\pi(gate.q2)]
- 'decay':
This is the same as 'lookahead', but the whole cost is multiplied by a
decay factor. This increases the cost if the SWAP that generated the
trial layout was recently used (i.e. it penalizes increase in depth).
.. math::
H_{decay} = max(decay(SWAP.q_1), decay(SWAP.q_2)) {
\frac{1}{\abs{F}} \sum_{gate \in F} D[\pi(gate.q_1)][\pi(gate.q2)]
+ W * \frac{1}{\abs{E}} \sum_{gate \in E} D[\pi(gate.q_1)][\pi(gate.q2)]
}
"""
super().__init__()
self.coupling_map = coupling_map
self.heuristic = heuristic
self.seed = seed
self.applied_gates = None
self.qubits_decay = None
def run(self, dag):
"""Run the SabreSwap pass on `dag`.
Args:
dag (DAGCircuit): the directed acyclic graph to be mapped.
Returns:
DAGCircuit: A dag mapped to be compatible with the coupling_map.
Raises:
TranspilerError: if the coupling map or the layout are not
compatible with the DAG
"""
if len(dag.qregs) != 1 or dag.qregs.get('q', None) is None:
raise TranspilerError('Sabre swap runs on physical circuits only.')
if len(dag.qubits) > self.coupling_map.size():
raise TranspilerError('More virtual qubits exist than physical.')
rng = np.random.default_rng(self.seed)
# Preserve input DAG's name, regs, wire_map, etc. but replace the graph.
mapped_dag = _copy_circuit_metadata(dag)
# Assume bidirectional couplings, fixing gate direction is easy later.
self.coupling_map.make_symmetric()
canonical_register = dag.qregs['q']
current_layout = Layout.generate_trivial_layout(canonical_register)
# A decay factor for each qubit used to heuristically penalize recently
# used qubits (to encourage parallelism).
self.qubits_decay = {qubit: 1 for qubit in dag.qubits}
# Start algorithm from the front layer and iterate until all gates done.
num_search_steps = 0
front_layer = dag.front_layer()
self.applied_gates = set()
while front_layer:
execute_gate_list = []
# Remove as many immediately applicable gates as possible
for node in front_layer:
if len(node.qargs) == 2:
v0, v1 = node.qargs
physical_qubits = (current_layout[v0], current_layout[v1])
if physical_qubits in self.coupling_map.get_edges():
execute_gate_list.append(node)
else: # Single-qubit gates as well as barriers are free
execute_gate_list.append(node)
if execute_gate_list:
for node in execute_gate_list:
new_node = _transform_gate_for_layout(node, current_layout)
mapped_dag.apply_operation_back(new_node.op,
new_node.qargs,
new_node.cargs,
new_node.condition)
front_layer.remove(node)
self.applied_gates.add(node)
for successor in dag.quantum_successors(node):
if successor.type != 'op':
continue
if self._is_resolved(successor, dag):
front_layer.append(successor)
if node.qargs:
self._reset_qubits_decay()
# Diagnostics
logger.debug('free! %s',
[(n.name, n.qargs) for n in execute_gate_list])
logger.debug('front_layer: %s',
[(n.name, n.qargs) for n in front_layer])
continue
# After all free gates are exhausted, heuristically find
# the best swap and insert it. When two or more swaps tie
# for best score, pick one randomly.
extended_set = self._obtain_extended_set(dag, front_layer)
swap_candidates = self._obtain_swaps(front_layer, current_layout)
swap_scores = dict.fromkeys(swap_candidates, 0)
for swap_qubits in swap_scores:
trial_layout = current_layout.copy()
trial_layout.swap(*swap_qubits)
score = self._score_heuristic(self.heuristic,
front_layer,
extended_set,
trial_layout,
swap_qubits)
swap_scores[swap_qubits] = score
min_score = min(swap_scores.values())
best_swaps = [k for k, v in swap_scores.items() if v == min_score]
best_swaps.sort(key=lambda x: (x[0].index, x[1].index))
best_swap = rng.choice(best_swaps)
swap_node = DAGNode(op=SwapGate(), qargs=best_swap, type='op')
swap_node = _transform_gate_for_layout(swap_node, current_layout)
mapped_dag.apply_operation_back(swap_node.op, swap_node.qargs)
current_layout.swap(*best_swap)
num_search_steps += 1
if num_search_steps % DECAY_RESET_INTERVAL == 0:
self._reset_qubits_decay()
else:
self.qubits_decay[best_swap[0]] += DECAY_RATE
self.qubits_decay[best_swap[1]] += DECAY_RATE
# Diagnostics
logger.debug('SWAP Selection...')
logger.debug('extended_set: %s',
[(n.name, n.qargs) for n in extended_set])
logger.debug('swap scores: %s', swap_scores)
logger.debug('best swap: %s', best_swap)
logger.debug('qubits decay: %s', self.qubits_decay)
self.property_set['final_layout'] = current_layout
return mapped_dag
def _reset_qubits_decay(self):
"""Reset all qubit decay factors to 1 upon request (to forget about
past penalizations).
"""
self.qubits_decay = {k: 1 for k in self.qubits_decay.keys()}
def _is_resolved(self, node, dag):
"""Return True if all of a node's predecessors in dag are applied.
"""
predecessors = dag.quantum_predecessors(node)
predecessors = filter(lambda x: x.type == 'op', predecessors)
return all([n in self.applied_gates for n in predecessors])
def _obtain_extended_set(self, dag, front_layer):
"""Populate extended_set by looking ahead a fixed number of gates.
For each existing element add a successor until reaching limit.
"""
# TODO: use layers instead of bfs_successors so long range successors aren't included.
extended_set = set()
bfs_successors_pernode = [dag.bfs_successors(n) for n in front_layer]
node_lookahead_exhausted = [False] * len(front_layer)
for i, node_successor_generator in cycle(enumerate(bfs_successors_pernode)):
if all(node_lookahead_exhausted) or len(extended_set) >= EXTENDED_SET_SIZE:
break
try:
_, successors = next(node_successor_generator)
successors = list(filter(lambda x: x.type == 'op' and len(x.qargs) == 2,
successors))
except StopIteration:
node_lookahead_exhausted[i] = True
continue
successors = iter(successors)
while len(extended_set) < EXTENDED_SET_SIZE:
try:
extended_set.add(next(successors))
except StopIteration:
break
return extended_set
def _obtain_swaps(self, front_layer, current_layout):
"""Return a set of candidate swaps that affect qubits in front_layer.
For each virtual qubit in front_layer, find its current location
on hardware and the physical qubits in that neighborhood. Every SWAP
on virtual qubits that corresponds to one of those physical couplings
is a candidate SWAP.
Candidate swaps are sorted so SWAP(i,j) and SWAP(j,i) are not duplicated.
"""
candidate_swaps = set()
for node in front_layer:
for virtual in node.qargs:
physical = current_layout[virtual]
for neighbor in self.coupling_map.neighbors(physical):
virtual_neighbor = current_layout[neighbor]
swap = sorted([virtual, virtual_neighbor],
key=lambda q: (q.register.name, q.index))
candidate_swaps.add(tuple(swap))
return candidate_swaps
def _score_heuristic(self, heuristic, front_layer, extended_set, layout, swap_qubits=None):
"""Return a heuristic score for a trial layout.
Assuming a trial layout has resulted from a SWAP, we now assign a cost
to it. The goodness of a layout is evaluated based on how viable it makes
the remaining virtual gates that must be applied.
"""
if heuristic == 'basic':
return sum(self.coupling_map.distance(*[layout[q] for q in node.qargs])
for node in front_layer)
elif heuristic == 'lookahead':
first_cost = self._score_heuristic('basic', front_layer, [], layout)
first_cost /= len(front_layer)
second_cost = self._score_heuristic('basic', extended_set, [], layout)
second_cost = 0.0 if not extended_set else second_cost / len(extended_set)
return first_cost + EXTENDED_SET_WEIGHT * second_cost
elif heuristic == 'decay':
return max(self.qubits_decay[swap_qubits[0]], self.qubits_decay[swap_qubits[1]]) * \
self._score_heuristic('lookahead', front_layer, extended_set, layout)
else:
raise TranspilerError('Heuristic %s not recognized.' % heuristic)
def _copy_circuit_metadata(source_dag):
"""Return a copy of source_dag with metadata but empty.
"""
target_dag = DAGCircuit()
target_dag.name = source_dag.name
for qreg in source_dag.qregs.values():
target_dag.add_qreg(qreg)
for creg in source_dag.cregs.values():
target_dag.add_creg(creg)
return target_dag
def _transform_gate_for_layout(op_node, layout):
"""Return node implementing a virtual op on given layout."""
mapped_op_node = deepcopy(op_node)
device_qreg = op_node.qargs[0].register
premap_qargs = op_node.qargs
mapped_qargs = map(lambda x: device_qreg[layout[x]], premap_qargs)
mapped_op_node.qargs = mapped_op_node.op.qargs = list(mapped_qargs)
return mapped_op_node

View File

@ -28,10 +28,12 @@ from qiskit.transpiler.passes import SetLayout
from qiskit.transpiler.passes import TrivialLayout
from qiskit.transpiler.passes import DenseLayout
from qiskit.transpiler.passes import NoiseAdaptiveLayout
from qiskit.transpiler.passes import SabreLayout
from qiskit.transpiler.passes import BarrierBeforeFinalMeasurements
from qiskit.transpiler.passes import BasicSwap
from qiskit.transpiler.passes import LookaheadSwap
from qiskit.transpiler.passes import StochasticSwap
from qiskit.transpiler.passes import SabreSwap
from qiskit.transpiler.passes import FullAncillaAllocation
from qiskit.transpiler.passes import EnlargeWithAncilla
from qiskit.transpiler.passes import ApplyLayout
@ -83,6 +85,8 @@ def level_0_pass_manager(pass_manager_config: PassManagerConfig) -> PassManager:
_choose_layout = DenseLayout(coupling_map, backend_properties)
elif layout_method == 'noise_adaptive':
_choose_layout = NoiseAdaptiveLayout(backend_properties)
elif layout_method == 'sabre':
_choose_layout = SabreLayout(coupling_map, max_iterations=1, seed=seed_transpiler)
else:
raise TranspilerError("Invalid layout method %s." % layout_method)
@ -105,6 +109,8 @@ def level_0_pass_manager(pass_manager_config: PassManagerConfig) -> PassManager:
_swap += [StochasticSwap(coupling_map, trials=20, seed=seed_transpiler)]
elif routing_method == 'lookahead':
_swap += [LookaheadSwap(coupling_map, search_depth=2, search_width=2)]
elif routing_method == 'sabre':
_swap += [SabreSwap(coupling_map, heuristic='basic', seed=seed_transpiler)]
else:
raise TranspilerError("Invalid routing method %s." % routing_method)

View File

@ -29,10 +29,12 @@ from qiskit.transpiler.passes import SetLayout
from qiskit.transpiler.passes import TrivialLayout
from qiskit.transpiler.passes import DenseLayout
from qiskit.transpiler.passes import NoiseAdaptiveLayout
from qiskit.transpiler.passes import SabreLayout
from qiskit.transpiler.passes import BarrierBeforeFinalMeasurements
from qiskit.transpiler.passes import BasicSwap
from qiskit.transpiler.passes import LookaheadSwap
from qiskit.transpiler.passes import StochasticSwap
from qiskit.transpiler.passes import SabreSwap
from qiskit.transpiler.passes import FullAncillaAllocation
from qiskit.transpiler.passes import EnlargeWithAncilla
from qiskit.transpiler.passes import FixedPoint
@ -96,6 +98,8 @@ def level_1_pass_manager(pass_manager_config: PassManagerConfig) -> PassManager:
_improve_layout = DenseLayout(coupling_map, backend_properties)
elif layout_method == 'noise_adaptive':
_improve_layout = NoiseAdaptiveLayout(backend_properties)
elif layout_method == 'sabre':
_improve_layout = SabreLayout(coupling_map, max_iterations=2, seed=seed_transpiler)
else:
raise TranspilerError("Invalid layout method %s." % layout_method)
@ -122,6 +126,8 @@ def level_1_pass_manager(pass_manager_config: PassManagerConfig) -> PassManager:
_swap += [StochasticSwap(coupling_map, trials=20, seed=seed_transpiler)]
elif routing_method == 'lookahead':
_swap += [LookaheadSwap(coupling_map, search_depth=4, search_width=4)]
elif routing_method == 'sabre':
_swap += [SabreSwap(coupling_map, heuristic='lookahead', seed=seed_transpiler)]
else:
raise TranspilerError("Invalid routing method %s." % routing_method)

View File

@ -30,10 +30,12 @@ from qiskit.transpiler.passes import CSPLayout
from qiskit.transpiler.passes import TrivialLayout
from qiskit.transpiler.passes import DenseLayout
from qiskit.transpiler.passes import NoiseAdaptiveLayout
from qiskit.transpiler.passes import SabreLayout
from qiskit.transpiler.passes import BarrierBeforeFinalMeasurements
from qiskit.transpiler.passes import BasicSwap
from qiskit.transpiler.passes import LookaheadSwap
from qiskit.transpiler.passes import StochasticSwap
from qiskit.transpiler.passes import SabreSwap
from qiskit.transpiler.passes import FullAncillaAllocation
from qiskit.transpiler.passes import EnlargeWithAncilla
from qiskit.transpiler.passes import FixedPoint
@ -95,6 +97,8 @@ def level_2_pass_manager(pass_manager_config: PassManagerConfig) -> PassManager:
_choose_layout_2 = DenseLayout(coupling_map, backend_properties)
elif layout_method == 'noise_adaptive':
_choose_layout_2 = NoiseAdaptiveLayout(backend_properties)
elif layout_method == 'sabre':
_choose_layout_2 = SabreLayout(coupling_map, max_iterations=2, seed=seed_transpiler)
else:
raise TranspilerError("Invalid layout method %s." % layout_method)
@ -117,6 +121,8 @@ def level_2_pass_manager(pass_manager_config: PassManagerConfig) -> PassManager:
_swap += [StochasticSwap(coupling_map, trials=20, seed=seed_transpiler)]
elif routing_method == 'lookahead':
_swap += [LookaheadSwap(coupling_map, search_depth=5, search_width=5)]
elif routing_method == 'sabre':
_swap += [SabreSwap(coupling_map, heuristic='decay', seed=seed_transpiler)]
else:
raise TranspilerError("Invalid routing method %s." % routing_method)

View File

@ -30,10 +30,12 @@ from qiskit.transpiler.passes import CSPLayout
from qiskit.transpiler.passes import TrivialLayout
from qiskit.transpiler.passes import DenseLayout
from qiskit.transpiler.passes import NoiseAdaptiveLayout
from qiskit.transpiler.passes import SabreLayout
from qiskit.transpiler.passes import BarrierBeforeFinalMeasurements
from qiskit.transpiler.passes import BasicSwap
from qiskit.transpiler.passes import LookaheadSwap
from qiskit.transpiler.passes import StochasticSwap
from qiskit.transpiler.passes import SabreSwap
from qiskit.transpiler.passes import FullAncillaAllocation
from qiskit.transpiler.passes import EnlargeWithAncilla
from qiskit.transpiler.passes import FixedPoint
@ -86,8 +88,8 @@ def level_3_pass_manager(pass_manager_config: PassManagerConfig) -> PassManager:
seed_transpiler = pass_manager_config.seed_transpiler
backend_properties = pass_manager_config.backend_properties
# 1. Unroll to the basis first, to prepare for noise-adaptive layout
_unroll = Unroller(basis_gates)
# 1. Unroll to 1q or 2q gates
_unroll3q = Unroll3qOrMore()
# 2. Layout on good qubits if calibration info available, otherwise on dense links
_given_layout = SetLayout(initial_layout)
@ -102,39 +104,34 @@ def level_3_pass_manager(pass_manager_config: PassManagerConfig) -> PassManager:
_choose_layout_2 = DenseLayout(coupling_map, backend_properties)
elif layout_method == 'noise_adaptive':
_choose_layout_2 = NoiseAdaptiveLayout(backend_properties)
elif layout_method == 'sabre':
_choose_layout_2 = SabreLayout(coupling_map, max_iterations=4, seed=seed_transpiler)
else:
raise TranspilerError("Invalid layout method %s." % layout_method)
# 3. Extend dag/layout with ancillas using the full coupling map
_embed = [FullAncillaAllocation(coupling_map), EnlargeWithAncilla(), ApplyLayout()]
# 4. Unroll to 1q or 2q gates, swap to fit the coupling map
# 4. Swap to fit the coupling map
_swap_check = CheckMap(coupling_map)
def _swap_condition(property_set):
return not property_set['is_swap_mapped']
_swap = [BarrierBeforeFinalMeasurements(), Unroll3qOrMore()]
_swap = [BarrierBeforeFinalMeasurements()]
if routing_method == 'basic':
_swap += [BasicSwap(coupling_map)]
elif routing_method == 'stochastic':
_swap += [StochasticSwap(coupling_map, trials=200, seed=seed_transpiler)]
elif routing_method == 'lookahead':
_swap += [LookaheadSwap(coupling_map, search_depth=5, search_width=6)]
elif routing_method == 'sabre':
_swap += [SabreSwap(coupling_map, heuristic='decay', seed=seed_transpiler)]
else:
raise TranspilerError("Invalid routing method %s." % routing_method)
# 5. 1q rotation merge and commutative cancellation iteratively until no more change in depth
_depth_check = [Depth(), FixedPoint('depth')]
def _opt_control(property_set):
return not property_set['depth_fixed_point']
_opt = [RemoveResetInZeroState(),
Collect2qBlocks(), ConsolidateBlocks(),
Unroller(basis_gates), # unroll unitaries
Optimize1qGates(basis_gates), CommutativeCancellation(),
OptimizeSwapBeforeMeasure(), RemoveDiagonalGatesBeforeMeasure()]
# 5. Unroll to the basis
_unroll = [Unroller(basis_gates)]
# 6. Fix any CX direction mismatch
_direction_check = [CheckCXDirection(coupling_map)]
@ -144,19 +141,35 @@ def level_3_pass_manager(pass_manager_config: PassManagerConfig) -> PassManager:
_direction = [CXDirection(coupling_map)]
# 8. Optimize iteratively until no more change in depth. Removes useless gates
# after reset and before measure, commutes gates and optimizes continguous blocks.
_depth_check = [Depth(), FixedPoint('depth')]
def _opt_control(property_set):
return not property_set['depth_fixed_point']
_reset = [RemoveResetInZeroState()]
_meas = [OptimizeSwapBeforeMeasure(), RemoveDiagonalGatesBeforeMeasure()]
_opt = [Collect2qBlocks(), ConsolidateBlocks(),
Optimize1qGates(basis_gates), CommutativeCancellation()]
# Build pass manager
pm3 = PassManager()
pm3.append(_unroll)
pm3.append(_unroll3q)
if coupling_map:
pm3.append(_given_layout)
pm3.append(_choose_layout_1, condition=_choose_layout_condition)
pm3.append(_choose_layout_2, condition=_choose_layout_condition)
pm3.append(_embed)
pm3.append(_reset + _meas)
pm3.append(_swap_check)
pm3.append(_swap, condition=_swap_condition)
pm3.append(_depth_check + _opt, do_while=_opt_control)
pm3.append(_depth_check + _opt + _unroll, do_while=_opt_control)
if coupling_map and not coupling_map.is_symmetric:
pm3.append(_direction_check)
pm3.append(_direction, condition=_direction_condition)
pm3.append(_reset)
return pm3

View File

@ -0,0 +1,5 @@
---
features:
- |
A :meth:`~qiskit.transpiler.Layout.combine_into_edge_map()` method is added
for converting two Layouts into a qubit map composing two circuits.

View File

@ -0,0 +1,9 @@
---
features:
- |
Two new methods for layout and routing have been added
to the transpiler. They can be selected by
passing the `layout_method='sabre'` and `routing_method='sabre'`
to the :meth:`~qiskit.transpile()` function. The methods are based on
Li et al. Tackling the Qubit Mapping Problem for NISQ-Era Quantum Devices,
ASPLOS 2019.

View File

@ -382,6 +382,16 @@ class TestDagNodeSelection(QiskitTestCase):
self.clbit1 = creg[1]
self.condition = (creg, 3)
def test_front_layer(self):
"""The method dag.front_layer() returns first layer"""
self.dag.apply_operation_back(HGate(), [self.qubit0], [])
self.dag.apply_operation_back(CXGate(), [self.qubit0, self.qubit1], [])
self.dag.apply_operation_back(Reset(), [self.qubit0], [])
op_nodes = self.dag.front_layer()
self.assertEqual(len(op_nodes), 1)
self.assertIsInstance(op_nodes[0].op, HGate)
def test_get_op_nodes_all(self):
"""The method dag.op_nodes() returns all op nodes"""
self.dag.apply_operation_back(HGate(), [self.qubit0], [])

View File

@ -0,0 +1,10 @@
OPENQASM 2.0;
include "qelib1.inc";
qreg q[3];
creg c[3];
h q[1];
measure q[0] -> c[0];
swap q[0],q[2];
cx q[1],q[0];
measure q[1] -> c[1];
measure q[0] -> c[2];

View File

@ -0,0 +1,14 @@
OPENQASM 2.0;
include "qelib1.inc";
qreg q[4];
creg c[4];
cx q[0],q[1];
h q[3];
measure q[2] -> c[2];
swap q[2],q[3];
cx q[2],q[1];
measure q[1] -> c[1];
swap q[1],q[2];
cx q[1],q[0];
measure q[0] -> c[0];
measure q[1] -> c[3];

View File

@ -0,0 +1,11 @@
OPENQASM 2.0;
include "qelib1.inc";
qreg q[4];
creg c[4];
h q[1];
measure q[0] -> c[0];
swap q[0],q[2];
cx q[1],q[0];
measure q[1] -> c[1];
measure q[0] -> c[2];
measure q[3] -> c[3];

View File

@ -63,6 +63,15 @@ class CouplingTest(QiskitTestCase):
expected = ("[[0, 1]]")
self.assertEqual(expected, str(coupling))
def test_neighbors(self):
"""Test neighboring qubits are found correctly."""
coupling = CouplingMap([[0, 1], [0, 2], [1, 0]])
physical_qubits = coupling.physical_qubits
self.assertEqual(set(coupling.neighbors(physical_qubits[0])), set([1, 2]))
self.assertEqual(set(coupling.neighbors(physical_qubits[1])), set([0]))
self.assertEqual(set(coupling.neighbors(physical_qubits[2])), set([]))
def test_distance_error(self):
"""Test distance between unconnected physical_qubits."""
graph = CouplingMap()
@ -114,6 +123,15 @@ class CouplingTest(QiskitTestCase):
self.assertFalse(coupling.is_symmetric)
def test_make_symmetric(self):
coupling_list = [[0, 1], [0, 2]]
coupling = CouplingMap(coupling_list)
coupling.make_symmetric()
edges = coupling.get_edges()
self.assertEqual(set(edges), set([(0, 1), (0, 2), (2, 0), (1, 0)]))
def test_full_factory(self):
coupling = CouplingMap.from_full(4)
edges = coupling.get_edges()

View File

@ -77,7 +77,8 @@ import os
from qiskit import execute
from qiskit import ClassicalRegister, QuantumRegister, QuantumCircuit, BasicAer
from qiskit.transpiler import PassManager
from qiskit.transpiler.passes import BasicSwap, LookaheadSwap, StochasticSwap, SetLayout
from qiskit.transpiler.passes import BasicSwap, LookaheadSwap, StochasticSwap, SabreSwap
from qiskit.transpiler.passes import SetLayout
from qiskit.transpiler import CouplingMap, Layout
from qiskit.test import QiskitTestCase
@ -284,6 +285,12 @@ class TestsStochasticSwap(SwapperCommonTestCases, QiskitTestCase):
additional_args = {'seed': 0}
class TestsSabreSwap(SwapperCommonTestCases, QiskitTestCase):
"""Test SwapperCommonTestCases using SabreSwap."""
pass_class = SabreSwap
additional_args = {'seed': 0}
if __name__ == '__main__':
if len(sys.argv) >= 2 and sys.argv[1] == 'regenerate':
CommonUtilitiesMixin.regenerate_expected = True

View File

@ -0,0 +1,91 @@
# -*- 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.
"""Test the Sabre Swap pass"""
import unittest
from qiskit.transpiler.passes import SabreSwap
from qiskit.transpiler import CouplingMap, PassManager
from qiskit import QuantumRegister, QuantumCircuit
from qiskit.test import QiskitTestCase
class TestSabreSwap(QiskitTestCase):
"""Tests the SabreSwap pass."""
def test_trivial_case(self):
"""Test that an already mapped circuit is unchanged.
q_0: H X
q_1: X
q_2: X
q_3: X X
q_4: X
"""
coupling = CouplingMap.from_ring(5)
qr = QuantumRegister(5, 'q')
qc = QuantumCircuit(qr)
qc.cx(0, 1) # free
qc.cx(2, 3) # free
qc.h(0) # free
qc.cx(1, 2) # F
qc.cx(1, 0)
qc.cx(4, 3) # F
qc.cx(0, 4)
passmanager = PassManager(SabreSwap(coupling, 'basic'))
new_qc = passmanager.run(qc)
self.assertEqual(new_qc, qc)
def test_lookahead_mode(self):
"""Test lookahead mode's lookahead finds single SWAP gate.
q_0: H
q_1: X
q_2: X
q_3: X X X X
q_4:
"""
coupling = CouplingMap.from_line(5)
qr = QuantumRegister(5, 'q')
qc = QuantumCircuit(qr)
qc.cx(0, 1) # free
qc.cx(2, 3) # free
qc.h(0) # free
qc.cx(1, 2) # free
qc.cx(1, 3) # F
qc.cx(2, 3) # E
qc.cx(1, 3) # E
pm = PassManager(SabreSwap(coupling, 'lookahead'))
new_qc = pm.run(qc)
self.assertEqual(new_qc.num_nonlocal_gates(), 7)
if __name__ == '__main__':
unittest.main()