Skip to content

Commit

Permalink
General routing (#3762)
Browse files Browse the repository at this point in the history
* Add files for implementing general routing

This implements the Token Swapping approximation algorithm

* Implement LayoutTransform pass

This pass just calls the ApproximateTokenSwapper code to compute which
swaps needs to be performed and appends that to the circuit.

* Restructure case+break (lint)

* Change indentation (lint)

* Fix some bugs suggested by Ali

Co-Authored-By: Ali Javadi-Abhari <[email protected]>

* Bugfixes in the LayoutTransformation pass

Co-Authored-By: Ali Javadi-Abhari <[email protected]>

* Moving LayoutTransformation to passes folder

Also add init imports

* Removed unused imports

They were only used in comments

* Resolve circular import (lint)

Co-Authored-By: Kevin Krsulich <[email protected]>

* Use relative import

Co-Authored-By: Kevin Krsulich <[email protected]>

* Fix references to Swap with relative import

* Add seed to ApproximateTokenSwapper and its pass

* Fix bug assuming dag.qubits() order is given

Added basic test for pass
Some types added

* Reformat imports for test

* Keep things DRY ;)

* Add doc for test

* move directory to avoid cyclic import

* rename general.py to token_swapper.py

* decrease graph size to make the test faster

* expose PermutationCircuit

* rename to from/to_layout

* remove unused function

* allow string layouts

* allow string layouts

* fix test

* lint

* Doc grammar

Co-Authored-By: Ali Javadi-Abhari <[email protected]>

Co-authored-by: Ali Javadi-Abhari <[email protected]>
Co-authored-by: Kevin Krsulich <[email protected]>
Co-authored-by: itoko <[email protected]>
Co-authored-by: Matthew Treinish <[email protected]>
  • Loading branch information
5 people authored Feb 24, 2020
1 parent d47f827 commit 346f8d4
Show file tree
Hide file tree
Showing 9 changed files with 710 additions and 0 deletions.
1 change: 1 addition & 0 deletions qiskit/transpiler/passes/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@

# routing
from .routing import BasicSwap
from .routing import LayoutTransformation
from .routing import LookaheadSwap
from .routing import StochasticSwap

Expand Down
1 change: 1 addition & 0 deletions qiskit/transpiler/passes/routing/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,6 @@
"""Module containing transpiler mapping passes."""

from .basic_swap import BasicSwap
from .layout_transformation import LayoutTransformation
from .lookahead_swap import LookaheadSwap
from .stochastic_swap import StochasticSwap
35 changes: 35 additions & 0 deletions qiskit/transpiler/passes/routing/algorithms/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# -*- coding: utf-8 -*-

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

# Copyright 2019 Andrew M. Childs, Eddie Schoute, Cem M. Unsal
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""The permutation modules contains some functions for permuting on architectures given a mapping.
A permutation function takes in a graph and a permutation of graph nodes,
and returns a sequence of SWAPs that implements that permutation on the graph.
"""

from .token_swapper import ApproximateTokenSwapper
241 changes: 241 additions & 0 deletions qiskit/transpiler/passes/routing/algorithms/token_swapper.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,241 @@
# -*- coding: utf-8 -*-

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

# Copyright 2019 Andrew M. Childs, Eddie Schoute, Cem M. Unsal
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Permutation algorithms for general graphs."""

import logging
from typing import TypeVar, Iterator, Mapping, Generic, MutableMapping, MutableSet, List, \
Iterable, Optional, Union

import networkx as nx
import numpy as np

from .types import Swap, Permutation
from .util import PermutationCircuit, permutation_circuit

_V = TypeVar('_V')
_T = TypeVar('_T')

logger = logging.getLogger(__name__)


class ApproximateTokenSwapper(Generic[_V]):
"""A class for computing approximate solutions to the Token Swapping problem.
Internally caches the graph and associated datastructures for re-use.
"""

def __init__(self, graph: nx.Graph, seed: Union[int, np.random.RandomState] = None) -> None:
"""Construct an ApproximateTokenSwapping object.
Args:
graph (nx.Graph): Undirected graph represented a coupling map.
seed (Union[int, np.random.RandomState]): Seed to use for random trials.
"""
self.graph = graph
# We need to fix the mapping from nodes in graph to nodes in shortest_paths.
# The nodes in graph don't have to integer nor contiguous, but those in a NumPy array are.
nodelist = list(graph.nodes())
self.node_map = {node: i for i, node in enumerate(nodelist)}
self.shortest_paths = nx.floyd_warshall_numpy(graph, nodelist=nodelist)
if isinstance(seed, np.random.RandomState):
self.seed = seed
else:
self.seed = np.random.RandomState(seed)

def distance(self, vertex0: _V, vertex1: _V) -> int:
"""Compute the distance between two nodes in `graph`."""
return self.shortest_paths[self.node_map[vertex0], self.node_map[vertex1]]

def permutation_circuit(self, permutation: Permutation,
trials: int = 4) -> PermutationCircuit:
"""Perform an approximately optimal Token Swapping algorithm to implement the permutation.
Args:
permutation: The partial mapping to implement in swaps.
trials: The number of trials to try to perform the mapping. Minimize over the trials.
Returns:
The circuit to implement the permutation
"""
sequential_swaps = self.map(permutation, trials=trials)
parallel_swaps = [[swap] for swap in sequential_swaps]
return permutation_circuit(parallel_swaps)

def map(self, mapping: Mapping[_V, _V],
trials: int = 4) -> List[Swap[_V]]:
"""Perform an approximately optimal Token Swapping algorithm to implement the permutation.
Supports partial mappings (i.e. not-permutations) for graphs with missing tokens.
Based on the paper: Approximation and Hardness for Token Swapping by Miltzow et al. (2016)
ArXiV: https://arxiv.org/abs/1602.05150
and generalization based on our own work.
Args:
mapping: The partial mapping to implement in swaps.
trials: The number of trials to try to perform the mapping. Minimize over the trials.
Returns:
The swaps to implement the mapping
"""
tokens = dict(mapping)
digraph = nx.DiGraph()
sub_digraph = nx.DiGraph() # Excludes self-loops in digraph.
todo_nodes = {node for node, destination in tokens.items() if node != destination}
for node in self.graph.nodes:
self._add_token_edges(node, tokens, digraph, sub_digraph)

trial_results = iter(list(self._trial_map(digraph.copy(),
sub_digraph.copy(),
todo_nodes.copy(),
tokens.copy()))
for _ in range(trials))

# Once we find a zero solution we stop.
def take_until_zero(results: Iterable[List[_T]]) -> Iterator[List[_T]]:
"""Take results until one is emitted of length zero (and also emit that)."""
for result in results:
yield result
if not result:
break

trial_results = take_until_zero(trial_results)
return min(trial_results, key=len)

def _trial_map(self,
digraph: nx.DiGraph,
sub_digraph: nx.DiGraph,
todo_nodes: MutableSet[_V],
tokens: MutableMapping[_V, _V]) -> Iterator[Swap[_V]]:
"""Try to map the tokens to their destinations and minimize the number of swaps."""

def swap(node0: _V, node1: _V) -> None:
"""Swap two nodes, maintaining datastructures.
Args:
node0: _V:
node1: _V:
Returns:
"""
self._swap(node0, node1, tokens, digraph, sub_digraph, todo_nodes)

# Can't just iterate over todo_nodes, since it may change during iteration.
steps = 0
while todo_nodes and steps <= 4 * self.graph.number_of_nodes() ** 2:
todo_node_id = self.seed.randint(0, len(todo_nodes))
todo_node = tuple(todo_nodes)[todo_node_id]

# Try to find a happy swap chain first by searching for a cycle,
# excluding self-loops.
# Note that if there are only unhappy swaps involving this todo_node,
# then an unhappy swap must be performed at some point.
# So it is not useful to globally search for all happy swap chains first.
try:
cycle = nx.find_cycle(sub_digraph, source=todo_node)
assert len(cycle) > 1, "The cycle was not happy."
# We iterate over the cycle in reversed order, starting at the last edge.
# The first edge is excluded.
for edge in cycle[-1:0:-1]:
yield edge
swap(edge[0], edge[1])
steps += len(cycle) - 1
except nx.NetworkXNoCycle:
# Try to find a node without a token to swap with.
try:
edge = next(edge for edge in nx.dfs_edges(sub_digraph, todo_node)
if edge[1] not in tokens)
# Swap predecessor and successor, because successor does not have a token
yield edge
swap(edge[0], edge[1])
steps += 1
except StopIteration:
# Unhappy swap case
cycle = nx.find_cycle(digraph, source=todo_node)
assert len(cycle) == 1, "The cycle was not unhappy."
unhappy_node = cycle[0][0]
# Find a node that wants to swap with this node.
try:
predecessor = next(
predecessor for predecessor in digraph.predecessors(unhappy_node)
if predecessor != unhappy_node)
except StopIteration:
logger.error("Unexpected StopIteration raised when getting predecessors"
"in unhappy swap case.")
return
yield unhappy_node, predecessor
swap(unhappy_node, predecessor)
steps += 1
if todo_nodes:
raise RuntimeError("Too many iterations while approximating the Token Swaps.")

def _add_token_edges(self,
node: _V,
tokens: Mapping[_V, _V],
digraph: nx.DiGraph,
sub_digraph: nx.DiGraph) -> None:
"""Add diedges to the graph wherever a token can be moved closer to its destination."""
if node not in tokens:
return

if tokens[node] == node:
digraph.add_edge(node, node)
return

for neighbor in self.graph.neighbors(node):
if self.distance(neighbor, tokens[node]) < self.distance(node, tokens[node]):
digraph.add_edge(node, neighbor)
sub_digraph.add_edge(node, neighbor)

def _swap(self, node1: _V, node2: _V,
tokens: MutableMapping[_V, _V],
digraph: nx.DiGraph,
sub_digraph: nx.DiGraph,
todo_nodes: MutableSet[_V]) -> None:
"""Swap two nodes, maintaining the data structures."""
assert self.graph.has_edge(node1,
node2), "The swap is being performed on a non-existent edge."
# Swap the tokens on the nodes, taking into account no-token nodes.
token1 = tokens.pop(node1, None) # type: Optional[_V]
token2 = tokens.pop(node2, None) # type: Optional[_V]
if token2 is not None:
tokens[node1] = token2
if token1 is not None:
tokens[node2] = token1
# Recompute the edges incident to node 1 and 2
for node in [node1, node2]:
digraph.remove_edges_from([(node, successor) for successor in digraph.successors(node)])
sub_digraph.remove_edges_from(
[(node, successor) for successor in sub_digraph.successors(node)])
self._add_token_edges(node, tokens, digraph, sub_digraph)
if node in tokens and tokens[node] != node:
todo_nodes.add(node)
elif node in todo_nodes:
todo_nodes.remove(node)
45 changes: 45 additions & 0 deletions qiskit/transpiler/passes/routing/algorithms/types.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# -*- coding: utf-8 -*-

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

# Copyright 2019 Andrew M. Childs, Eddie Schoute, Cem M. Unsal
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Type definitions used within the permutation package."""

from typing import TypeVar, Dict, Tuple, NamedTuple, Union

from qiskit.circuit import Qubit
from qiskit.dagcircuit import DAGCircuit

PermuteElement = TypeVar('PermuteElement')
Permutation = Dict[PermuteElement, PermuteElement]
Swap = Tuple[PermuteElement, PermuteElement]

# Represents a circuit for permuting to a mapping.
PermutationCircuit = NamedTuple('PermutationCircuit',
[('circuit', DAGCircuit),
('inputmap', Dict[Union[int, Qubit], Qubit])
# A mapping from architecture nodes to circuit registers.
])
Loading

0 comments on commit 346f8d4

Please sign in to comment.