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

Use retworkx for CouplingMap #5183

Merged
merged 46 commits into from
Dec 4, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
39ec74c
WIP: Use retworkx for CouplingMap
mtreinish Sep 30, 2020
3540355
DNM: Install retworkx from source
mtreinish Oct 5, 2020
665bb84
DNM: Add retworkx custom branch to travis
mtreinish Oct 5, 2020
be14661
Fix draw() copy paste error
mtreinish Oct 5, 2020
ffd500e
Fix typo
mtreinish Oct 5, 2020
7086c0a
DNK: Add source retworkx to docs build
mtreinish Oct 5, 2020
64ebd2b
Fix lint
mtreinish Oct 5, 2020
f995dc3
Use graph_distance_matrix instead of floyd warshall
mtreinish Oct 6, 2020
a7ad519
Merge branch 'master' into test-rx
mtreinish Oct 7, 2020
fcdd03d
DNM also add retworkx from source for image test job
mtreinish Oct 7, 2020
a8d10e1
Merge branch 'master' into test-rx
mtreinish Oct 9, 2020
569b1fa
Merge branch 'master' into test-rx
mtreinish Oct 11, 2020
1d28041
Use Gnm random function from retworkx in token swapper tests
mtreinish Oct 19, 2020
f64315f
Merge branch 'master' into test-rx
mtreinish Oct 19, 2020
a5e8ca8
Remove nx import from token swapper tests
mtreinish Oct 19, 2020
8499ffd
Fix lint
mtreinish Oct 20, 2020
31ae819
Merge branch 'master' into test-rx
mtreinish Oct 29, 2020
da99fa5
Merge branch 'master' into test-rx
mtreinish Nov 12, 2020
22f565c
Remove install from git from CI config since retworkx 0.6.0 is released
mtreinish Nov 12, 2020
77e2a90
Fix api call
mtreinish Nov 12, 2020
5d29b57
Use retworkx generators where possible for constructors
mtreinish Nov 12, 2020
2393949
Remvoe source install from travis config
mtreinish Nov 12, 2020
e4ca28c
Bump version in setup.py too
mtreinish Nov 12, 2020
ae916bc
Merge branch 'master' into test-rx
mtreinish Nov 13, 2020
042d2f2
Use extend_from_edge_list for from_full
mtreinish Nov 16, 2020
37d9fe0
Merge branch 'master' into test-rx
mtreinish Nov 16, 2020
1ce4f8f
Merge branch 'master' into test-rx
mtreinish Nov 16, 2020
7a2ab3a
Update qiskit/transpiler/coupling.py
mtreinish Nov 18, 2020
8538497
Merge branch 'master' into test-rx
mtreinish Nov 18, 2020
3c5557b
Merge branch 'master' into test-rx
mtreinish Nov 19, 2020
bd0297e
Merge branch 'master' into test-rx
mtreinish Nov 20, 2020
67c9b46
Use edge_list() return and has_edge() in sabre
mtreinish Nov 23, 2020
e03b56b
Merge branch 'master' into test-rx
mtreinish Nov 23, 2020
7b9f704
Merge branch 'master' into test-rx
mtreinish Nov 24, 2020
79eec8d
Merge branch 'master' into test-rx
mtreinish Nov 25, 2020
1b5857e
Merge branch 'master' into test-rx
mtreinish Nov 26, 2020
53c739e
Fix issue in layout_transformation pass
mtreinish Nov 30, 2020
b3a4cdc
Merge branch 'master' into test-rx
mtreinish Nov 30, 2020
b722db7
Merge branch 'master' into test-rx
Cryoris Dec 2, 2020
808c647
Update qiskit/transpiler/passes/routing/algorithms/token_swapper.py
mtreinish Dec 2, 2020
3265ba3
Merge branch 'master' into test-rx
mtreinish Dec 3, 2020
b6304ea
Fix Lint
mtreinish Dec 3, 2020
6c38480
Merge branch 'master' into test-rx
Cryoris Dec 3, 2020
7af8f86
Merge branch 'master' into test-rx
kdk Dec 3, 2020
4da7ae6
Merge branch 'master' into test-rx
mtreinish Dec 4, 2020
3643a49
Merge branch 'master' into test-rx
mergify[bot] Dec 4, 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
77 changes: 32 additions & 45 deletions qiskit/transpiler/coupling.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand All @@ -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
Expand All @@ -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):
"""
Expand All @@ -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.
Expand Down Expand Up @@ -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

Expand All @@ -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):
Expand All @@ -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):
Expand All @@ -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.
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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."""
Expand Down Expand Up @@ -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))
16 changes: 7 additions & 9 deletions qiskit/transpiler/passes/routing/algorithms/token_swapper.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -48,27 +49,24 @@ 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:
graph (nx.Graph): Undirected graph represented a coupling map.
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:
self.seed = np.random.default_rng(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]]
return self.shortest_paths[vertex0, vertex1]

def permutation_circuit(self, permutation: Permutation,
trials: int = 4) -> PermutationCircuit:
Expand Down Expand Up @@ -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(),
Expand Down Expand Up @@ -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]

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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.
Expand All @@ -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.

Expand Down
2 changes: 1 addition & 1 deletion qiskit/transpiler/passes/routing/layout_transformation.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
4 changes: 2 additions & 2 deletions qiskit/transpiler/passes/routing/sabre_swap.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
29 changes: 17 additions & 12 deletions test/python/transpiler/test_token_swapper.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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]

Expand All @@ -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]

Expand All @@ -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]

Expand All @@ -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))
Expand All @@ -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]

Expand All @@ -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)])
Comment on lines +113 to +114
Copy link
Contributor

Choose a reason for hiding this comment

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

What's the difference to extend_from_edge_list?

Copy link
Member Author

Choose a reason for hiding this comment

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

extend_from_edge_list() will add nodes to the graph if there is a node index in the edge tuple that doesn't exist. While add_edges_from_no_data() will error if there is an index in the edge tuple that doesn't exist.

https://retworkx.readthedocs.io/en/stable/stubs/retworkx.PyGraph.extend_from_edge_list.html#retworkx.PyGraph.extend_from_edge_list

swapper = ApproximateTokenSwapper(graph) # type: ApproximateTokenSwapper[int]

# Generate a randomized permutation.
Expand Down