diff --git a/qiskit/transpiler/passes/__init__.py b/qiskit/transpiler/passes/__init__.py index ccd920595298..352e93aaee3a 100644 --- a/qiskit/transpiler/passes/__init__.py +++ b/qiskit/transpiler/passes/__init__.py @@ -114,6 +114,7 @@ # routing from .routing import BasicSwap +from .routing import LayoutTransformation from .routing import LookaheadSwap from .routing import StochasticSwap diff --git a/qiskit/transpiler/passes/routing/__init__.py b/qiskit/transpiler/passes/routing/__init__.py index 147cb773cef3..c32baa354706 100644 --- a/qiskit/transpiler/passes/routing/__init__.py +++ b/qiskit/transpiler/passes/routing/__init__.py @@ -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 diff --git a/qiskit/transpiler/passes/routing/algorithms/__init__.py b/qiskit/transpiler/passes/routing/algorithms/__init__.py new file mode 100644 index 000000000000..1fa0d98cd131 --- /dev/null +++ b/qiskit/transpiler/passes/routing/algorithms/__init__.py @@ -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 diff --git a/qiskit/transpiler/passes/routing/algorithms/token_swapper.py b/qiskit/transpiler/passes/routing/algorithms/token_swapper.py new file mode 100644 index 000000000000..5a319ba69cce --- /dev/null +++ b/qiskit/transpiler/passes/routing/algorithms/token_swapper.py @@ -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) diff --git a/qiskit/transpiler/passes/routing/algorithms/types.py b/qiskit/transpiler/passes/routing/algorithms/types.py new file mode 100644 index 000000000000..13f6314eac81 --- /dev/null +++ b/qiskit/transpiler/passes/routing/algorithms/types.py @@ -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. + ]) diff --git a/qiskit/transpiler/passes/routing/algorithms/util.py b/qiskit/transpiler/passes/routing/algorithms/util.py new file mode 100644 index 000000000000..1895d7adfdcd --- /dev/null +++ b/qiskit/transpiler/passes/routing/algorithms/util.py @@ -0,0 +1,103 @@ +# -*- 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. + +"""Utility functions shared between permutation functionality.""" + +from typing import List, TypeVar, Iterable, MutableMapping, Optional + +from qiskit.circuit import QuantumRegister +from qiskit.dagcircuit import DAGCircuit +from qiskit.extensions import SwapGate + +from .types import Swap, PermutationCircuit + +_K = TypeVar('_K') +_V = TypeVar('_V') + + +def swap_permutation(swaps: Iterable[Iterable[Swap[_K]]], + mapping: MutableMapping[_K, _V], + allow_missing_keys: bool = False) -> None: + """Given a circuit of swaps, apply them to the permutation (in-place). + + Args: + swaps: param mapping: A mapping of Keys to Values, where the Keys are being swapped. + mapping: The permutation to have swaps applied to. + allow_missing_keys: Whether to allow swaps of missing keys in mapping. + """ + for swap_step in swaps: + for sw1, sw2 in swap_step: + # Take into account non-existent keys. + val1 = None # type: Optional[_V] + val2 = None # type: Optional[_V] + if allow_missing_keys: + val1 = mapping.pop(sw1, None) + val2 = mapping.pop(sw2, None) + else: + # Asserts that both keys exist + val1, val2 = mapping.pop(sw1), mapping.pop(sw2) + + if val1 is not None: + mapping[sw2] = val1 + if val2 is not None: + mapping[sw1] = val2 + + +def permutation_circuit(swaps: Iterable[List[Swap[_V]]]) -> PermutationCircuit: + """Produce a circuit description of a list of swaps. + With a given permutation and permuter you can compute the swaps using the permuter function + then feed it into this circuit function to obtain a circuit description. + Args: + swaps: An iterable of swaps to perform. + Returns: + A MappingCircuit with the circuit and a mapping of node to qubit in the circuit. + """ + # Construct a circuit with each unique node id becoming a quantum register of size 1. + dag = DAGCircuit() + swap_list = list(swaps) + + # Set of unique nodes used in the swaps. + nodes = { + swap_node + for swap_step in swap_list + for swap_nodes in swap_step + for swap_node in swap_nodes + } + + node_qargs = {node: QuantumRegister(1) for node in nodes} + for qubit in node_qargs.values(): + dag.add_qreg(qubit) + + inputmap = {node: q[0] for node, q in node_qargs.items()} + + # Apply swaps to the circuit. + for swap_step in swap_list: + for swap0, swap1 in swap_step: + dag.apply_operation_back(SwapGate(), [inputmap[swap0], inputmap[swap1]]) + + return PermutationCircuit(dag, inputmap) diff --git a/qiskit/transpiler/passes/routing/layout_transformation.py b/qiskit/transpiler/passes/routing/layout_transformation.py new file mode 100644 index 000000000000..fb2b39716f4e --- /dev/null +++ b/qiskit/transpiler/passes/routing/layout_transformation.py @@ -0,0 +1,108 @@ +# -*- 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 typing import Union + +import numpy as np + +from qiskit.transpiler import Layout, CouplingMap +from qiskit.transpiler.basepasses import TransformationPass +from qiskit.transpiler.exceptions import TranspilerError +from qiskit.transpiler.passes.routing.algorithms 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: CouplingMap, + from_layout: Union[Layout, str], + to_layout: Union[Layout, str], + seed: Union[int, np.random.RandomState] = None, + trials=4): + """LayoutTransformation initializer. + + Args: + coupling_map (CouplingMap): + Directed graph representing a coupling map. + + from_layout (Union[Layout, str]): + The starting layout of qubits onto physical qubits. + If the type is str, look up `property_set` when this pass runs. + + to_layout (Union[Layout, str]): + The final layout of qubits on phyiscal qubits. + If the type is str, look up `property_set` when this pass runs. + + seed (Union[int, np.random.RandomState]): + Seed to use for random trials. + + trials (int): + How many randomized trials to perform, taking the best circuit as output. + """ + super().__init__() + self.coupling_map = coupling_map + self.from_layout = from_layout + self.to_layout = to_layout + graph = coupling_map.graph.to_undirected() + self.token_swapper = ApproximateTokenSwapper(graph, seed) + self.trials = trials + + 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. + Or if either of string from/to_layout is not found in `property_set`. + """ + 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') + + from_layout = self.from_layout + if isinstance(from_layout, str): + try: + from_layout = self.property_set[from_layout] + except Exception: + raise TranspilerError('No {} (from_layout) in property_set.'.format(from_layout)) + + to_layout = self.to_layout + if isinstance(to_layout, str): + try: + to_layout = self.property_set[to_layout] + except Exception: + raise TranspilerError('No {} (to_layout) in property_set.'.format(to_layout)) + + # Find the permutation between the initial physical qubits and final physical qubits. + permutation = {pqubit: to_layout.get_virtual_bits()[vqubit] + for vqubit, pqubit in from_layout.get_virtual_bits().items()} + + perm_circ = self.token_swapper.permutation_circuit(permutation, self.trials) + + edge_map = {vqubit: dag.qubits()[pqubit] + for (pqubit, vqubit) in perm_circ.inputmap.items()} + dag.compose_back(perm_circ.circuit, edge_map=edge_map) + return dag diff --git a/test/python/transpiler/test_layout_transformation.py b/test/python/transpiler/test_layout_transformation.py new file mode 100644 index 000000000000..f36a47591f11 --- /dev/null +++ b/test/python/transpiler/test_layout_transformation.py @@ -0,0 +1,56 @@ +# -*- 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. + +"""Test the LayoutTransformation pass""" + +import unittest + +import numpy as np + +from qiskit import QuantumRegister, QuantumCircuit +from qiskit.converters import circuit_to_dag +from qiskit.test import QiskitTestCase +from qiskit.transpiler import CouplingMap, Layout +from qiskit.transpiler.passes import LayoutTransformation + + +class TestLayoutTransformation(QiskitTestCase): + """ + Tests the LayoutTransformation pass. + """ + + def test_three_qubit(self): + """Test if the permutation {0->2,1->0,2->1} is implemented correctly.""" + np.random.seed(0) + v = QuantumRegister(3, 'v') # virtual qubits + coupling = CouplingMap([[0, 1], [1, 2]]) + from_layout = Layout({v[0]: 0, v[1]: 1, v[2]: 2}) + to_layout = Layout({v[0]: 2, v[1]: 0, v[2]: 1}) + ltpass = LayoutTransformation(coupling_map=coupling, + from_layout=from_layout, + to_layout=to_layout) + qc = QuantumCircuit(3) # input (empty) physical circuit + dag = circuit_to_dag(qc) + q = dag.qubits() + output_dag = ltpass.run(dag) + # output_dag.draw() + # Check that only two swaps were performed + self.assertCountEqual(["swap"] * 2, [op.name for op in output_dag.topological_op_nodes()]) + # And check that the swaps were first performed on {q0,q1} then on {q1,q2}. + self.assertEqual([frozenset([q[0], q[1]]), frozenset([q[1], q[2]])], + [frozenset(op.qargs) for op in output_dag.topological_op_nodes()]) + + +if __name__ == '__main__': + unittest.main() diff --git a/test/python/transpiler/test_token_swapper.py b/test/python/transpiler/test_token_swapper.py new file mode 100644 index 000000000000..30194f75b54a --- /dev/null +++ b/test/python/transpiler/test_token_swapper.py @@ -0,0 +1,120 @@ +# -*- 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. + +"""Test cases for the permutation.complete package""" + +import itertools + +import networkx as nx +from numpy import random +from qiskit.transpiler.passes.routing.algorithms import ApproximateTokenSwapper +from qiskit.transpiler.passes.routing.algorithms import util + +from qiskit.test import QiskitTestCase + + +class TestGeneral(QiskitTestCase): + """The test cases""" + + def setUp(self) -> None: + """Set up test cases.""" + random.seed(0) + + def test_simple(self) -> None: + """Test a simple permutation on a path graph of size 4.""" + graph = nx.path_graph(4) + permutation = {0: 0, 1: 3, 3: 1, 2: 2} + swapper = ApproximateTokenSwapper(graph) # type: ApproximateTokenSwapper[int] + + out = list(swapper.map(permutation)) + self.assertEqual(3, len(out)) + util.swap_permutation([out], permutation) + self.assertEqual({i: i for i in range(4)}, permutation) + + def test_small(self) -> None: + """Test an inverting permutation on a small path graph of size 8""" + graph = nx.path_graph(8) + permutation = {i: 7 - i for i in range(8)} + swapper = ApproximateTokenSwapper(graph) # type: ApproximateTokenSwapper[int] + + out = list(swapper.map(permutation)) + util.swap_permutation([out], permutation) + self.assertEqual({i: i for i in range(8)}, permutation) + + def test_bug1(self) -> None: + """Tests for a bug that occured in happy swap chains of length >2.""" + graph = nx.Graph() + graph.add_edges_from([(0, 1), (0, 2), (0, 3), (0, 4), + (1, 2), (1, 3), (1, 4), (2, 3), (2, 4), (3, 4), (3, 6)]) + permutation = {0: 4, 1: 0, 2: 3, 3: 6, 4: 2, 6: 1} + swapper = ApproximateTokenSwapper(graph) # type: ApproximateTokenSwapper[int] + + out = list(swapper.map(permutation)) + util.swap_permutation([out], permutation) + self.assertEqual({i: i for i in permutation}, permutation) + + def test_partial_simple(self) -> None: + """Test a partial mapping on a small graph.""" + graph = nx.path_graph(4) + mapping = {0: 3} + swapper = ApproximateTokenSwapper(graph) # type: ApproximateTokenSwapper[int] + out = list(swapper.map(mapping)) + self.assertEqual(3, len(out)) + util.swap_permutation([out], mapping, allow_missing_keys=True) + self.assertEqual({3: 3}, mapping) + + def test_partial_small(self) -> None: + """Test an partial inverting permutation on a small path graph of size 5""" + graph = nx.path_graph(4) + permutation = {i: 3 - i for i in range(2)} + swapper = ApproximateTokenSwapper(graph) # type: ApproximateTokenSwapper[int] + + out = list(swapper.map(permutation)) + self.assertEqual(5, len(out)) + util.swap_permutation([out], permutation, allow_missing_keys=True) + self.assertEqual({i: i for i in permutation.values()}, permutation) + + def test_large_partial_random(self) -> None: + """Test a random (partial) mapping on a large randomly generated graph""" + size = 100 + # Note that graph may have "gaps" in the node counts, i.e. the numbering is noncontiguous. + graph = nx.dense_gnm_random_graph(size, size ** 2 // 10) + graph.remove_edges_from((i, i) for i in graph.nodes) # Remove self-loops. + # Make sure the graph is connected by adding C_n + nodes = list(graph.nodes) + graph.add_edges_from((node, nodes[(i + 1) % len(nodes)]) for i, node in enumerate(nodes)) + swapper = ApproximateTokenSwapper(graph) # type: ApproximateTokenSwapper[int] + + # Generate a randomized permutation. + rand_perm = random.permutation(graph.nodes()) + permutation = dict(zip(graph.nodes(), rand_perm)) + mapping = dict(itertools.islice(permutation.items(), 0, size, 2)) # Drop every 2nd element. + + out = list(swapper.map(mapping, trials=40)) + util.swap_permutation([out], mapping, allow_missing_keys=True) + self.assertEqual({i: i for i in mapping.values()}, mapping)