Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

General routing #3762

Merged
merged 30 commits into from
Feb 24, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
f9f5301
Add files for implementing general routing
eddieschoute Jan 29, 2020
69897a6
Implement LayoutTransform pass
eddieschoute Jan 29, 2020
f71459a
Restructure case+break (lint)
eddieschoute Jan 29, 2020
155be99
Change indentation (lint)
eddieschoute Jan 29, 2020
ffc2d10
Fix some bugs suggested by Ali
eddieschoute Jan 30, 2020
5dc2cbc
Bugfixes in the LayoutTransformation pass
eddieschoute Jan 31, 2020
e26f765
Moving LayoutTransformation to passes folder
eddieschoute Jan 31, 2020
3fff7e6
Removed unused imports
eddieschoute Jan 31, 2020
d5f67b0
Resolve circular import (lint)
eddieschoute Jan 31, 2020
c62fc0d
Use relative import
eddieschoute Feb 7, 2020
e53eb15
Fix references to Swap with relative import
eddieschoute Feb 7, 2020
5035d07
Add seed to ApproximateTokenSwapper and its pass
eddieschoute Feb 7, 2020
0f1aeb0
Fix bug assuming dag.qubits() order is given
eddieschoute Feb 7, 2020
e6ff517
Reformat imports for test
eddieschoute Feb 7, 2020
a2496ec
Keep things DRY ;)
eddieschoute Feb 7, 2020
31cac28
Add doc for test
eddieschoute Feb 7, 2020
9e8fd5e
move directory to avoid cyclic import
itoko Feb 12, 2020
cef8133
rename general.py to token_swapper.py
itoko Feb 12, 2020
15fea51
decrease graph size to make the test faster
itoko Feb 12, 2020
92522b8
expose PermutationCircuit
itoko Feb 12, 2020
7daf732
rename to from/to_layout
itoko Feb 12, 2020
9aaf192
remove unused function
itoko Feb 12, 2020
cc8920d
allow string layouts
itoko Feb 12, 2020
1298182
allow string layouts
itoko Feb 12, 2020
e3ffd91
fix a bug
itoko Feb 12, 2020
6253572
fix test
itoko Feb 12, 2020
00ad95e
lint
itoko Feb 12, 2020
446af3d
Doc grammar
eddieschoute Feb 19, 2020
117aa2f
Merge branch 'master' into general-routing
ajavadia Feb 24, 2020
33116e4
Merge branch 'master' into general-routing
mtreinish Feb 24, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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."""
ajavadia marked this conversation as resolved.
Show resolved Hide resolved

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

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