diff --git a/.pylintrc b/.pylintrc index 78d9e029e364..e4ff30a0c24e 100644 --- a/.pylintrc +++ b/.pylintrc @@ -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}|(__.*__))$ diff --git a/qiskit/compiler/transpile.py b/qiskit/compiler/transpile.py index 95b8f047670f..bd2d1fa91fc9 100644 --- a/qiskit/compiler/transpile.py +++ b/qiskit/compiler/transpile.py @@ -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, diff --git a/qiskit/dagcircuit/dagcircuit.py b/qiskit/dagcircuit/dagcircuit.py index bac79bacef9a..d0efbd18911b 100644 --- a/qiskit/dagcircuit/dagcircuit.py +++ b/qiskit/dagcircuit/dagcircuit.py @@ -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 @@ -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. @@ -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 @@ -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) diff --git a/qiskit/transpiler/coupling.py b/qiskit/transpiler/coupling.py index 519c7c7f7f5b..6272b8127d73 100644 --- a/qiskit/transpiler/coupling.py +++ b/qiskit/transpiler/coupling.py @@ -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. @@ -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 diff --git a/qiskit/transpiler/layout.py b/qiskit/transpiler/layout.py index ba53a917b33c..68d9575595fb 100644 --- a/qiskit/transpiler/layout.py +++ b/qiskit/transpiler/layout.py @@ -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 @@ -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(): diff --git a/qiskit/transpiler/passes/__init__.py b/qiskit/transpiler/passes/__init__.py index bdf213e6539a..2179bac17a83 100644 --- a/qiskit/transpiler/passes/__init__.py +++ b/qiskit/transpiler/passes/__init__.py @@ -29,6 +29,7 @@ TrivialLayout DenseLayout NoiseAdaptiveLayout + SabreLayout CSPLayout ApplyLayout Layout2qDistance @@ -44,6 +45,7 @@ BasicSwap LookaheadSwap StochasticSwap + SabreSwap Basis Change ============ @@ -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 @@ -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 diff --git a/qiskit/transpiler/passes/layout/__init__.py b/qiskit/transpiler/passes/layout/__init__.py index 1461b3cc0eca..dcbf9c77cba3 100644 --- a/qiskit/transpiler/passes/layout/__init__.py +++ b/qiskit/transpiler/passes/layout/__init__.py @@ -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 diff --git a/qiskit/transpiler/passes/layout/csp_layout.py b/qiskit/transpiler/passes/layout/csp_layout.py index 7765b1da544f..078f82d951fd 100644 --- a/qiskit/transpiler/passes/layout/csp_layout.py +++ b/qiskit/transpiler/passes/layout/csp_layout.py @@ -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. diff --git a/qiskit/transpiler/passes/layout/sabre_layout.py b/qiskit/transpiler/passes/layout/sabre_layout.py new file mode 100644 index 000000000000..750e17c1520e --- /dev/null +++ b/qiskit/transpiler/passes/layout/sabre_layout.py @@ -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 `_ + """ + + 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) diff --git a/qiskit/transpiler/passes/routing/__init__.py b/qiskit/transpiler/passes/routing/__init__.py index c32baa354706..c4085d72da32 100644 --- a/qiskit/transpiler/passes/routing/__init__.py +++ b/qiskit/transpiler/passes/routing/__init__.py @@ -18,3 +18,4 @@ from .layout_transformation import LayoutTransformation from .lookahead_swap import LookaheadSwap from .stochastic_swap import StochasticSwap +from .sabre_swap import SabreSwap diff --git a/qiskit/transpiler/passes/routing/lookahead_swap.py b/qiskit/transpiler/passes/routing/lookahead_swap.py index 95ce69a17716..4fcadc1c254e 100644 --- a/qiskit/transpiler/passes/routing/lookahead_swap.py +++ b/qiskit/transpiler/passes/routing/lookahead_swap.py @@ -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): diff --git a/qiskit/transpiler/passes/routing/sabre_swap.py b/qiskit/transpiler/passes/routing/sabre_swap.py new file mode 100644 index 000000000000..024f55d2f0bd --- /dev/null +++ b/qiskit/transpiler/passes/routing/sabre_swap.py @@ -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 `_ + """ + + 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 diff --git a/qiskit/transpiler/preset_passmanagers/level0.py b/qiskit/transpiler/preset_passmanagers/level0.py index 5c2655bf44f8..2cc76f72c3d5 100644 --- a/qiskit/transpiler/preset_passmanagers/level0.py +++ b/qiskit/transpiler/preset_passmanagers/level0.py @@ -28,10 +28,12 @@ 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 _choose_layout_condition(property_set): _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 _swap_condition(property_set): _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) diff --git a/qiskit/transpiler/preset_passmanagers/level1.py b/qiskit/transpiler/preset_passmanagers/level1.py index bd72419479d9..c44d7a80af89 100644 --- a/qiskit/transpiler/preset_passmanagers/level1.py +++ b/qiskit/transpiler/preset_passmanagers/level1.py @@ -29,10 +29,12 @@ 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 _choose_layout_condition(property_set): _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 _swap_condition(property_set): _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) diff --git a/qiskit/transpiler/preset_passmanagers/level2.py b/qiskit/transpiler/preset_passmanagers/level2.py index f04d842cfdac..7b6352ce83a2 100644 --- a/qiskit/transpiler/preset_passmanagers/level2.py +++ b/qiskit/transpiler/preset_passmanagers/level2.py @@ -30,10 +30,12 @@ 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 _choose_layout_condition(property_set): _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 _swap_condition(property_set): _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) diff --git a/qiskit/transpiler/preset_passmanagers/level3.py b/qiskit/transpiler/preset_passmanagers/level3.py index 9ca72586b716..4e8d4dec0ead 100644 --- a/qiskit/transpiler/preset_passmanagers/level3.py +++ b/qiskit/transpiler/preset_passmanagers/level3.py @@ -30,10 +30,12 @@ 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 _choose_layout_condition(property_set): _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 _direction_condition(property_set): _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 diff --git a/releasenotes/notes/combine-into-edge-map-279441b53ed4e790.yaml b/releasenotes/notes/combine-into-edge-map-279441b53ed4e790.yaml new file mode 100644 index 000000000000..bab3998e4273 --- /dev/null +++ b/releasenotes/notes/combine-into-edge-map-279441b53ed4e790.yaml @@ -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. diff --git a/releasenotes/notes/sabre-2a3bba505e48ee82.yaml b/releasenotes/notes/sabre-2a3bba505e48ee82.yaml new file mode 100644 index 000000000000..dee12e47a076 --- /dev/null +++ b/releasenotes/notes/sabre-2a3bba505e48ee82.yaml @@ -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. diff --git a/test/python/dagcircuit/test_dagcircuit.py b/test/python/dagcircuit/test_dagcircuit.py index 54e4bc802b87..c2b5db4875e2 100644 --- a/test/python/dagcircuit/test_dagcircuit.py +++ b/test/python/dagcircuit/test_dagcircuit.py @@ -382,6 +382,16 @@ def setUp(self): 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], []) diff --git a/test/python/qasm/TestsSabreSwap_a_cx_to_map.qasm b/test/python/qasm/TestsSabreSwap_a_cx_to_map.qasm new file mode 100644 index 000000000000..bceabc6db0ab --- /dev/null +++ b/test/python/qasm/TestsSabreSwap_a_cx_to_map.qasm @@ -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]; diff --git a/test/python/qasm/TestsSabreSwap_handle_measurement.qasm b/test/python/qasm/TestsSabreSwap_handle_measurement.qasm new file mode 100644 index 000000000000..11370fef5993 --- /dev/null +++ b/test/python/qasm/TestsSabreSwap_handle_measurement.qasm @@ -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]; diff --git a/test/python/qasm/TestsSabreSwap_initial_layout.qasm b/test/python/qasm/TestsSabreSwap_initial_layout.qasm new file mode 100644 index 000000000000..ea822fc97a5e --- /dev/null +++ b/test/python/qasm/TestsSabreSwap_initial_layout.qasm @@ -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]; diff --git a/test/python/transpiler/test_coupling.py b/test/python/transpiler/test_coupling.py index df35af310661..178a9b1fb425 100644 --- a/test/python/transpiler/test_coupling.py +++ b/test/python/transpiler/test_coupling.py @@ -63,6 +63,15 @@ def test_add_edge(self): 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 @@ def test_symmetric_big_false(self): 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() diff --git a/test/python/transpiler/test_mappers.py b/test/python/transpiler/test_mappers.py index 5095c1e60c99..7910499181a3 100644 --- a/test/python/transpiler/test_mappers.py +++ b/test/python/transpiler/test_mappers.py @@ -77,7 +77,8 @@ def test_a_common_test(self): 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 diff --git a/test/python/transpiler/test_sabre_swap.py b/test/python/transpiler/test_sabre_swap.py new file mode 100644 index 000000000000..4e45a853fb66 --- /dev/null +++ b/test/python/transpiler/test_sabre_swap.py @@ -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()