diff --git a/qiskit/transpiler/coupling.py b/qiskit/transpiler/coupling.py index 6a0bd7d6de8..6abc5657dee 100644 --- a/qiskit/transpiler/coupling.py +++ b/qiskit/transpiler/coupling.py @@ -22,7 +22,7 @@ import numpy as np import scipy.sparse as sp import scipy.sparse.csgraph as cs -import networkx as nx +import retworkx as rx from qiskit.transpiler.exceptions import CouplingError @@ -45,7 +45,7 @@ def __init__(self, couplinglist=None, description=None): """ self.description = description # the coupling map graph - self.graph = nx.DiGraph() + self.graph = rx.PyDiGraph() # a dict of dicts from node pairs to distances self._dist_matrix = None # a sorted list of physical qubits (integers) in this coupling map @@ -54,12 +54,11 @@ def __init__(self, couplinglist=None, description=None): self._is_symmetric = None if couplinglist is not None: - for source, target in couplinglist: - self.add_edge(source, target) + self.graph.extend_from_edge_list([tuple(x) for x in couplinglist]) def size(self): """Return the number of physical qubits in this graph.""" - return len(self.graph.nodes) + return len(self.graph) def get_edges(self): """ @@ -68,7 +67,7 @@ def get_edges(self): Returns: Tuple(int,int): Each edge is a pair of physical qubits. """ - return list(self.graph.edges()) + return self.graph.edge_list() def add_physical_qubit(self, physical_qubit): """Add a physical qubit to the coupling graph as a node. @@ -98,7 +97,7 @@ def add_edge(self, src, dst): self.add_physical_qubit(src) if dst not in self.physical_qubits: self.add_physical_qubit(dst) - self.graph.add_edge(src, dst) + self.graph.add_edge(src, dst, None) self._dist_matrix = None # invalidate self._is_symmetric = None # invalidate @@ -118,7 +117,7 @@ def subgraph(self, nodelist): def physical_qubits(self): """Returns a sorted list of physical_qubits""" if self._qubit_list is None: - self._qubit_list = sorted(self.graph.nodes) + self._qubit_list = self.graph.node_indexes() return self._qubit_list def is_connected(self): @@ -128,8 +127,8 @@ def is_connected(self): Return True if connected, False otherwise """ try: - return nx.is_weakly_connected(self.graph) - except nx.exception.NetworkXException: + return rx.is_weakly_connected(self.graph) + except rx.NullGraph: return False def neighbors(self, physical_qubit): @@ -155,14 +154,7 @@ def _compute_distance_matrix(self): """ if not self.is_connected(): raise CouplingError("coupling graph not connected") - lengths = nx.all_pairs_shortest_path_length(self.graph.to_undirected(as_view=True)) - lengths = dict(lengths) - size = len(lengths) - cmap = np.zeros((size, size)) - for idx in range(size): - cmap[idx, np.fromiter(lengths[idx].keys(), dtype=int)] = np.fromiter( - lengths[idx].values(), dtype=int) - self._dist_matrix = cmap + self._dist_matrix = rx.digraph_distance_matrix(self.graph, as_undirected=True) def distance(self, physical_qubit1, physical_qubit2): """Returns the undirected distance between physical_qubit1 and physical_qubit2. @@ -196,12 +188,12 @@ def shortest_undirected_path(self, physical_qubit1, physical_qubit2): Raises: CouplingError: When there is no path between physical_qubit1, physical_qubit2. """ - try: - return nx.shortest_path(self.graph.to_undirected(as_view=True), source=physical_qubit1, - target=physical_qubit2) - except nx.exception.NetworkXNoPath: + paths = rx.digraph_dijkstra_shortest_paths( + self.graph, source=physical_qubit1, target=physical_qubit2, as_undirected=True) + if not paths: raise CouplingError( "Nodes %s and %s are not connected" % (str(physical_qubit1), str(physical_qubit2))) + return paths[physical_qubit2] @property def is_symmetric(self): @@ -232,8 +224,7 @@ def _check_symmetry(self): Returns: Bool: True if symmetric, False otherwise """ - mat = nx.adjacency_matrix(self.graph) - return (mat - mat.T).nnz == 0 + return self.graph.is_symmetric() def reduce(self, mapping): """Returns a reduced coupling map that @@ -278,41 +269,36 @@ def reduce(self, mapping): def from_full(cls, num_qubits, bidirectional=True): """Return a fully connected coupling map on n qubits.""" cmap = cls(description='full') + edge_list = [] for i in range(num_qubits): for j in range(i): - cmap.add_edge(j, i) + edge_list.append((j, i)) if bidirectional: - cmap.add_edge(i, j) + edge_list.append((i, j)) + cmap.graph.extend_from_edge_list(edge_list) return cmap @classmethod def from_line(cls, num_qubits, bidirectional=True): """Return a fully connected coupling map on n qubits.""" cmap = cls(description='line') - for i in range(num_qubits-1): - cmap.add_edge(i, i+1) - if bidirectional: - cmap.add_edge(i+1, i) + cmap.graph = rx.generators.directed_path_graph( + num_qubits, bidirectional=bidirectional) return cmap @classmethod def from_ring(cls, num_qubits, bidirectional=True): """Return a fully connected coupling map on n qubits.""" cmap = cls(description='ring') - for i in range(num_qubits): - if i == num_qubits - 1: - k = 0 - else: - k = i + 1 - cmap.add_edge(i, k) - if bidirectional: - cmap.add_edge(k, i) + cmap.graph = rx.generators.directed_cycle_graph( + num_qubits, bidirectional=bidirectional) return cmap @classmethod def from_grid(cls, num_rows, num_columns, bidirectional=True): """Return qubits connected on a grid of num_rows x num_columns.""" cmap = cls(description='grid') + edge_list = [] for i in range(num_rows): for j in range(num_columns): node = i * num_columns + j @@ -323,18 +309,19 @@ def from_grid(cls, num_rows, num_columns, bidirectional=True): right = (node+1) if j < num_columns-1 else None if up is not None and bidirectional: - cmap.add_edge(node, up) + edge_list.append((node, up)) if left is not None and bidirectional: - cmap.add_edge(node, left) + edge_list.append((node, left)) if down is not None: - cmap.add_edge(node, down) + edge_list.append((node, down)) if right is not None: - cmap.add_edge(node, right) + edge_list.append((node, right)) + cmap.graph.extend_from_edge_list(edge_list) return cmap def largest_connected_component(self): """Return a set of qubits in the largest connected component.""" - return max(nx.strongly_connected_components(self.graph), key=len) + return max(rx.strongly_connected_components(self.graph), key=len) def __str__(self): """Return a string representation of the coupling graph.""" @@ -366,8 +353,8 @@ def draw(self): except ImportError: raise ImportError("CouplingMap.draw requires pydot and pillow. " "Run 'pip install pydot pillow'.") - - dot = nx.drawing.nx_pydot.to_pydot(self.graph) + dot_str = self.graph.to_dot() + dot = pydot.graph_from_dot_data(dot_str)[0] png = dot.create_png(prog='neato') return Image.open(io.BytesIO(png)) diff --git a/qiskit/transpiler/passes/routing/algorithms/token_swapper.py b/qiskit/transpiler/passes/routing/algorithms/token_swapper.py index 7cc54861744..ecc0c67469d 100644 --- a/qiskit/transpiler/passes/routing/algorithms/token_swapper.py +++ b/qiskit/transpiler/passes/routing/algorithms/token_swapper.py @@ -32,6 +32,7 @@ import networkx as nx import numpy as np +import retworkx as rx from .types import Swap, Permutation from .util import PermutationCircuit, permutation_circuit @@ -48,7 +49,8 @@ class ApproximateTokenSwapper(Generic[_V]): Internally caches the graph and associated datastructures for re-use. """ - def __init__(self, graph: nx.Graph, seed: Union[int, np.random.Generator] = None) -> None: + def __init__(self, graph: rx.PyGraph, + seed: Union[int, np.random.Generator, None] = None) -> None: """Construct an ApproximateTokenSwapping object. Args: @@ -56,11 +58,7 @@ def __init__(self, graph: nx.Graph, seed: Union[int, np.random.Generator] = None seed (Union[int, np.random.default_rng]): 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) + self.shortest_paths = rx.graph_floyd_warshall_numpy(graph) if isinstance(seed, np.random.Generator): self.seed = seed else: @@ -68,7 +66,7 @@ def __init__(self, graph: nx.Graph, seed: Union[int, np.random.Generator] = None 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]] + return self.shortest_paths[vertex0, vertex1] def permutation_circuit(self, permutation: Permutation, trials: int = 4) -> PermutationCircuit: @@ -106,7 +104,7 @@ def map(self, mapping: Mapping[_V, _V], 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: + for node in self.graph.node_indexes(): self._add_token_edges(node, tokens, digraph, sub_digraph) trial_results = iter(list(self._trial_map(digraph.copy(), @@ -147,7 +145,7 @@ def swap(node0: _V, node1: _V) -> None: # 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: + while todo_nodes and steps <= 4 * len(self.graph) ** 2: todo_node_id = self.seed.integers(0, len(todo_nodes)) todo_node = tuple(todo_nodes)[todo_node_id] diff --git a/qiskit/transpiler/passes/routing/cython/stochastic_swap/swap_trial.pyx b/qiskit/transpiler/passes/routing/cython/stochastic_swap/swap_trial.pyx index 719a6ed6c8b..0647cfbd094 100644 --- a/qiskit/transpiler/passes/routing/cython/stochastic_swap/swap_trial.pyx +++ b/qiskit/transpiler/passes/routing/cython/stochastic_swap/swap_trial.pyx @@ -20,8 +20,9 @@ from .utils cimport NLayout, EdgeCollection @cython.boundscheck(False) @cython.wraparound(False) -cdef double compute_cost(double[:, ::1] dist, unsigned int * logic_to_phys, - int[::1] gates, unsigned int num_gates) nogil: +cdef double compute_cost(const double[:, ::1] dist, + unsigned int * logic_to_phys, + int[::1] gates, unsigned int num_gates) nogil: """ Computes the cost (distance) of a logical to physical mapping. Args: @@ -44,7 +45,7 @@ cdef double compute_cost(double[:, ::1] dist, unsigned int * logic_to_phys, @cython.nonecheck(False) @cython.boundscheck(False) @cython.wraparound(False) -cdef compute_random_scaling(double[:, ::1] scale, double[:, ::1] cdist2, +cdef compute_random_scaling(double[:, ::1] scale, const double[:, ::1] cdist2, double * rand, unsigned int num_qubits): """ Computes the symmetric random scaling (perturbation) matrix, and places the values in the 'scale' array. @@ -67,7 +68,8 @@ cdef compute_random_scaling(double[:, ::1] scale, double[:, ::1] cdist2, @cython.boundscheck(False) @cython.wraparound(False) def swap_trial(int num_qubits, NLayout int_layout, int[::1] int_qubit_subset, - int[::1] gates, double[:, ::1] cdist2, double[:, ::1] cdist, + int[::1] gates, const double[:, ::1] cdist2, + const double[:, ::1] cdist, int[::1] edges, double[:, ::1] scale, object rng): """ A single iteration of the tchastic swap mapping routine. diff --git a/qiskit/transpiler/passes/routing/layout_transformation.py b/qiskit/transpiler/passes/routing/layout_transformation.py index 2f1af94bcfa..ce7c88af1ba 100644 --- a/qiskit/transpiler/passes/routing/layout_transformation.py +++ b/qiskit/transpiler/passes/routing/layout_transformation.py @@ -61,7 +61,7 @@ def __init__(self, coupling_map: CouplingMap, graph = coupling_map.graph.to_undirected() else: self.coupling_map = CouplingMap.from_full(len(to_layout)) - graph = self.coupling_map.graph + graph = self.coupling_map.graph.to_undirected() self.token_swapper = ApproximateTokenSwapper(graph, seed) self.trials = trials diff --git a/qiskit/transpiler/passes/routing/sabre_swap.py b/qiskit/transpiler/passes/routing/sabre_swap.py index 1fff8edc3f8..52b10f14cb8 100644 --- a/qiskit/transpiler/passes/routing/sabre_swap.py +++ b/qiskit/transpiler/passes/routing/sabre_swap.py @@ -167,8 +167,8 @@ def run(self, dag): 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(): + if self.coupling_map.graph.has_edge(current_layout[v0], + current_layout[v1]): execute_gate_list.append(node) else: # Single-qubit gates as well as barriers are free execute_gate_list.append(node) diff --git a/test/python/transpiler/test_token_swapper.py b/test/python/transpiler/test_token_swapper.py index b8523de5b13..38cac8972bf 100644 --- a/test/python/transpiler/test_token_swapper.py +++ b/test/python/transpiler/test_token_swapper.py @@ -28,7 +28,7 @@ import itertools -import networkx as nx +import retworkx as rx from numpy import random from qiskit.transpiler.passes.routing.algorithms import ApproximateTokenSwapper from qiskit.transpiler.passes.routing.algorithms import util @@ -46,7 +46,7 @@ def setUp(self) -> None: def test_simple(self) -> None: """Test a simple permutation on a path graph of size 4.""" - graph = nx.path_graph(4) + graph = rx.generators.path_graph(4) permutation = {0: 0, 1: 3, 3: 1, 2: 2} swapper = ApproximateTokenSwapper(graph) # type: ApproximateTokenSwapper[int] @@ -57,7 +57,7 @@ def test_simple(self) -> None: def test_small(self) -> None: """Test an inverting permutation on a small path graph of size 8""" - graph = nx.path_graph(8) + graph = rx.generators.path_graph(8) permutation = {i: 7 - i for i in range(8)} swapper = ApproximateTokenSwapper(graph) # type: ApproximateTokenSwapper[int] @@ -67,9 +67,10 @@ def test_small(self) -> None: 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)]) + graph = rx.PyGraph() + graph.extend_from_edge_list([ + (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] @@ -79,7 +80,7 @@ def test_bug1(self) -> None: def test_partial_simple(self) -> None: """Test a partial mapping on a small graph.""" - graph = nx.path_graph(4) + graph = rx.generators.path_graph(4) mapping = {0: 3} swapper = ApproximateTokenSwapper(graph) # type: ApproximateTokenSwapper[int] out = list(swapper.map(mapping)) @@ -89,7 +90,7 @@ def test_partial_simple(self) -> None: def test_partial_small(self) -> None: """Test an partial inverting permutation on a small path graph of size 5""" - graph = nx.path_graph(4) + graph = rx.generators.path_graph(4) permutation = {i: 3 - i for i in range(2)} swapper = ApproximateTokenSwapper(graph) # type: ApproximateTokenSwapper[int] @@ -102,11 +103,15 @@ 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. + graph = rx.undirected_gnm_random_graph(size, size ** 2 // 10) + for i in graph.node_indexes(): + try: + graph.remove_edge(i, i) # Remove self-loops. + except rx.NoEdgeBetweenNodes: + continue # 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)) + graph.add_edges_from_no_data( + [(i, i + 1) for i in range(len(graph) - 1)]) swapper = ApproximateTokenSwapper(graph) # type: ApproximateTokenSwapper[int] # Generate a randomized permutation.