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 9 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
76 changes: 76 additions & 0 deletions qiskit/transpiler/passes/routing/layout_transformation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
# -*- coding: utf-8 -*-

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

"""Map (with minimum effort) a DAGCircuit onto a `coupling_map` adding swap gates."""

from qiskit.transpiler.basepasses import TransformationPass
from qiskit.transpiler.exceptions import TranspilerError
from qiskit.transpiler.routing import util
from qiskit.transpiler.routing.general import ApproximateTokenSwapper


class LayoutTransformation(TransformationPass):
""" Adds a Swap circuit for a given (partial) permutation to the circuit.

This circuit is found by a 4-approximation algorithm for Token Swapping.
More details are available in the routing code.
"""

def __init__(self, coupling_map, initial_layout, final_layout, trials=4):
eddieschoute marked this conversation as resolved.
Show resolved Hide resolved
"""LayoutTransformation initializer.

Args:
coupling_map (CouplingMap): Directed graph represented a coupling map.
initial_layout (Layout): The starting layout of qubits onto physical qubits.
final_layout (Layout): The final layout of qubits on phyiscal qubits.
trials (int): How many randomized trials to perform, taking the best circuit as output.
"""
super().__init__()
self.coupling_map = coupling_map
itoko marked this conversation as resolved.
Show resolved Hide resolved
self.initial_layout = initial_layout
self.final_layout = final_layout
graph = coupling_map.graph.to_undirected()
token_swapper = ApproximateTokenSwapper(graph)

# Find the permutation that between the initial physical qubits and final physical qubits.
permutation = {pqubit: final_layout.get_virtual_bits()[vqubit]
for vqubit, pqubit in initial_layout.get_virtual_bits().items()}
swaps = token_swapper.map(permutation, trials)
# None of the swaps are guaranteed to be disjoint so we perform one swap every layer.
parallel_swaps = [[swap] for swap in swaps]
self.permutation_circuit = util.circuit(parallel_swaps)

def run(self, dag):
"""Apply the specified partial permutation to the circuit.

Args:
dag (DAGCircuit): DAG to transform the layout of.

Returns:
DAGCircuit: The DAG with transformed layout.

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('LayoutTransform runs on physical circuits only')

if len(dag.qubits()) > len(self.coupling_map.physical_qubits):
raise TranspilerError('The layout does not match the amount of qubits in the DAG')

edge_map = {vqubit: self.initial_layout.get_physical_bits()[pqubit]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

edge_map = {right_q: dag.qubits()[left_q] 
            for (left_q, right_q) in self.permutation_circuit.inputmap.items()}

would work to return a physical-to-physical qubit map. (Note: Both of left_q and right_q are physical qubits.)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure why you can assume the same ordering of the qubits by using dag.qubits()[left_q]. Why would the physical qubit have the same index as the qubit register in the dag? Is that guaranteed somehow?

E.g., if I get a Layout with a DagCircuit which say that qubit v[15] → 35, then that is guaranteed to mean that virtual qubit v15 is mapped onto dag.qubits()[35]?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ApplyLayout guarantees that the physical qubit have the same index as the qubit register in the dag. It assures that the dag has only one register 'q' and its index is the same as that of physical qubits, i.e. the dag never has virtual qubits. So as long as LayoutTransformation runs after ApplyLayout, dag.qubits()[left_q] is valid. If we want to handle circuits with virtual qubits (in future), we need more lines here to care the case you suggested.

for (pqubit, vqubit) in self.permutation_circuit.inputmap.items()}
dag.compose_back(self.permutation_circuit.circuit, edge_map=edge_map)
return dag
35 changes: 35 additions & 0 deletions qiskit/transpiler/routing/__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 qiskit.transpiler.routing.types import Permutation, Swap
228 changes: 228 additions & 0 deletions qiskit/transpiler/routing/general.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,228 @@
# -*- 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
import random
from typing import TypeVar, Iterator, Mapping, Generic, MutableMapping, MutableSet, List, \
Iterable, Optional

import networkx as nx

from qiskit.transpiler.routing import Swap

_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) -> None:
"""Construct an ApproximateTokenSwapping object."""
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)

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 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 = random.sample(todo_nodes, 1)[0]

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

def parallel_map(self, mapping: Mapping[_V, _V],
trials: Optional[int] = None) -> List[List[Swap[_V]]]:
"""A convenience function to wrap each swap in a list.

Useful for code that expects a parallel sequence of swaps.
"""
if trials is not None:
sequential_swaps = self.map(mapping, trials=trials)
else:
sequential_swaps = self.map(mapping)
return [[swap] for swap in sequential_swaps]
35 changes: 35 additions & 0 deletions qiskit/transpiler/routing/types.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.

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

from typing import TypeVar, Dict, Tuple

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