Skip to content

Commit

Permalink
Transition DAGCircuit to retworkx (#3916)
Browse files Browse the repository at this point in the history
* Move networkx towards retworkx syntax and consolidate differences.

Co-authored-by: Matthew Treinish <[email protected]>

* Adjust networkx node_id handling to be zero indexed.

Co-authored-by: Matthew Treinish <[email protected]>

* Consolidate edge inspection calls requiring data=True.

Co-authored-by: Matthew Treinish <[email protected]>

* Change networkx multi_graph indexes from DAGNodes to integers.

Co-authored-by: Matthew Treinish <[email protected]>

* Use retworkx if os.environ['USE_RETWORKX'].lower() == 'y'.

Co-authored-by: Matthew Treinish <[email protected]>

* Move networkx/retworkx variants to be DAGCircuit subclasses.

Co-authored-by: Matthew Treinish <[email protected]>

* Add TravisCI job to run with Retworkx DAGCircuit.

Co-authored-by: Matthew Treinish <[email protected]>

* Switch to retworkx as default

This commit switches the default from the networkx implementation to the
retworkx implemenation, retworkx is added to the requirements list
accordingly because it is now a required dependency.

* Fix rebase mistake

Co-authored-by: Matthew Treinish <[email protected]>
  • Loading branch information
kdk and mtreinish authored Mar 11, 2020
1 parent aedf2f7 commit 95328ad
Show file tree
Hide file tree
Showing 12 changed files with 656 additions and 220 deletions.
2 changes: 1 addition & 1 deletion .pylintrc
Original file line number Diff line number Diff line change
Expand Up @@ -298,7 +298,7 @@ ignore-mixin-members=yes
# (useful for modules/projects where namespaces are manipulated during runtime
# and thus existing member attributes cannot be deduced by static analysis. It
# supports qualified module names, as well as Unix pattern matching.
ignored-modules=matplotlib.cm,numpy.random
ignored-modules=matplotlib.cm,numpy.random,retworkx

# List of class names for which member attributes should not be checked (useful
# for classes with dynamically set attributes). This supports the use of
Expand Down
12 changes: 12 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,18 @@ jobs:
- pip install diff-cover || true
- diff-cover --compare-branch master coverage.xml || true

- name: Python 3.6 Tests (Networkx)
python: 3.6
install:
# Install step for jobs that require compilation and qa.
- pip install -U -r requirements.txt -c constraints.txt
- pip install -U -r requirements-dev.txt coveralls -c constraints.txt
- pip install -c constraints.txt -e .
- pip install "qiskit-ibmq-provider" -c constraints.txt
- pip install "retworkx>=0.3.0"
env:
- USE_RETWORKX="N"

# Randomized testing
- name: Randomized tests
cache:
Expand Down
324 changes: 184 additions & 140 deletions qiskit/dagcircuit/dagcircuit.py

Large diffs are not rendered by default.

161 changes: 161 additions & 0 deletions qiskit/dagcircuit/networkx_dagcircuit.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
# -*- coding: utf-8 -*-

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

"""
Object to represent a quantum circuit as a directed acyclic graph (DAG).
The nodes in the graph are either input/output nodes or operation nodes.
The edges correspond to qubits or bits in the circuit. A directed edge
from node A to node B means that the (qu)bit passes from the output of A
to the input of B. The object's methods allow circuits to be constructed,
composed, and modified. Some natural properties like depth can be computed
directly from the graph.
"""

import copy

import networkx as nx

from .dagnode import DAGNode
from .dagcircuit import DAGCircuit


class NetworkxDAGCircuit(DAGCircuit):
"""
Quantum circuit as a directed acyclic graph.
There are 3 types of nodes in the graph: inputs, outputs, and operations.
The nodes are connected by directed edges that correspond to qubits and
bits.
"""

def __init__(self):
super().__init__()

self._USE_RX = False
self._gx = 'nx'
self._multi_graph = nx.MultiDiGraph()

def _add_multi_graph_node(self, node):
# nx: requires manual node id handling.
# rx: provides defined ids for added nodes.
self._max_node_id += 1
node._node_id = self._max_node_id
self._id_to_node[node._node_id] = node

self._multi_graph.add_node(node._node_id)

return node._node_id

def _get_multi_graph_nodes(self):
return (self._id_to_node[node_index]
for node_index in self._multi_graph.nodes())

def _add_multi_graph_edge(self, src_id, dest_id, data):
# nx: accepts edge data as kwargs.
# rx: accepts edge data as a dict arg.
self._multi_graph.add_edge(src_id, dest_id, **data)

def _get_all_multi_graph_edges(self, src_id, dest_id):
# nx: edge enumeration through indexing multigraph
# rx: edge enumeration through method get_all_edge_data
return list(self._multi_graph[src_id][dest_id].values())

def _get_multi_graph_edges(self):
# nx: Includes edge data in return only when data kwarg = True
# rx: Always includes edge data in return
return self._multi_graph.edges(data=True)

def _get_multi_graph_in_edges(self, node_id):
# nx: Includes edge data in return only when data kwarg = True
# rx: Always includes edge data in return
return self._multi_graph.in_edges(node_id, data=True)

def _get_multi_graph_out_edges(self, node_id):
# nx: Includes edge data in return only when data kwarg = True
# rx: Always includes edge data in return
return self._multi_graph.out_edges(node_id, data=True)

def __eq__(self, other):
slf = copy.deepcopy(self._multi_graph)
oth = copy.deepcopy(other._multi_graph)

for node_id in slf.nodes:
slf.nodes[node_id]['node'] = self._id_to_node[node_id]
for node_id in oth.nodes:
oth.nodes[node_id]['node'] = other._id_to_node[node_id]

return nx.is_isomorphic(
slf, oth,
node_match=lambda x, y: DAGNode.semantic_eq(x['node'], y['node']))

def topological_nodes(self):
"""
Yield nodes in topological order.
Returns:
generator(DAGNode): node in topological order
"""
def _key(x):
return str(self._id_to_node[x].qargs)

return (self._id_to_node[idx]
for idx in nx.lexicographical_topological_sort(
self._multi_graph,
key=_key))

def successors(self, node):
"""Returns iterator of the successors of a node as DAGNodes."""
return (self._id_to_node[idx]
for idx in self._multi_graph.successors(node._node_id))

def predecessors(self, node):
"""Returns iterator of the predecessors of a node as DAGNodes."""
return (self._id_to_node[idx]
for idx in self._multi_graph.predecessors(node._node_id))

def bfs_successors(self, node):
"""
Returns an iterator of tuples of (DAGNode, [DAGNodes]) where the DAGNode is the current node
and [DAGNode] is its successors in BFS order.
"""
return ((self._id_to_node[idx], [self._id_to_node[succ] for succ in succ_list])
for idx, succ_list in nx.bfs_successors(self._multi_graph, node._node_id))

def multigraph_layers(self):
"""Yield layers of the multigraph."""
predecessor_count = dict() # Dict[node, predecessors not visited]
cur_layer = self.input_map.values()
yield cur_layer
next_layer = []
while cur_layer:
for node in cur_layer:
# Count multiedges with multiplicity.
for successor in self.successors(node):
multiplicity = self._multi_graph.number_of_edges(
node._node_id,
successor._node_id)
if successor in predecessor_count:
predecessor_count[successor] -= multiplicity
else:
predecessor_count[successor] = \
self._multi_graph.in_degree(successor._node_id) - multiplicity

if predecessor_count[successor] == 0:
next_layer.append(successor)
del predecessor_count[successor]

yield next_layer
cur_layer = next_layer
next_layer = []
134 changes: 134 additions & 0 deletions qiskit/dagcircuit/retworkx_dagcircuit.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
# -*- coding: utf-8 -*-

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

"""
Object to represent a quantum circuit as a directed acyclic graph (DAG).
The nodes in the graph are either input/output nodes or operation nodes.
The edges correspond to qubits or bits in the circuit. A directed edge
from node A to node B means that the (qu)bit passes from the output of A
to the input of B. The object's methods allow circuits to be constructed,
composed, and modified. Some natural properties like depth can be computed
directly from the graph.
"""

import copy

import retworkx as rx

from .dagnode import DAGNode
from .dagcircuit import DAGCircuit


class RetworkxDAGCircuit(DAGCircuit):
"""
Quantum circuit as a directed acyclic graph.
There are 3 types of nodes in the graph: inputs, outputs, and operations.
The nodes are connected by directed edges that correspond to qubits and
bits.
"""

def __init__(self):
super().__init__()

self._USE_RX = True
self._gx = 'rx'
self._multi_graph = rx.PyDAG()

def _add_multi_graph_node(self, node):
# nx: requires manual node id handling.
# rx: provides defined ids for added nodes.

node_id = self._multi_graph.add_node(node)
node._node_id = node_id
self._id_to_node[node_id] = node
return node_id

def _get_multi_graph_nodes(self):
return iter(self._multi_graph.nodes())

def _add_multi_graph_edge(self, src_id, dest_id, data):
# nx: accepts edge data as kwargs.
# rx: accepts edge data as a dict arg.

self._multi_graph.add_edge(src_id, dest_id, data)

def _get_all_multi_graph_edges(self, src_id, dest_id):
# nx: edge enumeration through indexing multigraph
# rx: edge enumeration through method get_all_edge_data

return self._multi_graph.get_all_edge_data(src_id, dest_id)

def _get_multi_graph_edges(self):
# nx: Includes edge data in return only when data kwarg = True
# rx: Always includes edge data in return

return [(src, dest, data)
for src_node in self._multi_graph.nodes()
for (src, dest, data)
in self._multi_graph.out_edges(src_node._node_id)]

def _get_multi_graph_in_edges(self, node_id):
# nx: Includes edge data in return only when data kwarg = True
# rx: Always includes edge data in return
return self._multi_graph.in_edges(node_id)

def _get_multi_graph_out_edges(self, node_id):
# nx: Includes edge data in return only when data kwarg = True
# rx: Always includes edge data in return
return self._multi_graph.out_edges(node_id)

def __eq__(self, other):
# TODO this works but is a horrible way to do this
slf = copy.deepcopy(self._multi_graph)
oth = copy.deepcopy(other._multi_graph)

return rx.is_isomorphic_node_match(
slf, oth,
DAGNode.semantic_eq)

def topological_nodes(self):
"""
Yield nodes in topological order.
Returns:
generator(DAGNode): node in topological order
"""
def _key(x):
return str(x.qargs)

return iter(rx.lexicographical_topological_sort(
self._multi_graph,
key=_key))

def successors(self, node):
"""Returns iterator of the successors of a node as DAGNodes."""
return iter(self._multi_graph.successors(node._node_id))

def predecessors(self, node):
"""Returns iterator of the predecessors of a node as DAGNodes."""
return iter(self._multi_graph.predecessors(node._node_id))

def bfs_successors(self, node):
"""
Returns an iterator of tuples of (DAGNode, [DAGNodes]) where the DAGNode is the current node
and [DAGNode] is its successors in BFS order.
"""
return iter(rx.bfs_successors(self._multi_graph, node._node_id))

def multigraph_layers(self):
"""Yield layers of the multigraph."""
first_layer = [x._node_id for x in self.input_map.values()]
yield from rx.layers(self._multi_graph, first_layer)
3 changes: 2 additions & 1 deletion qiskit/transpiler/passes/utils/merge_adjacent_barriers.py
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,8 @@ def _collect_potential_merges(dag, barriers):
for next_barrier in barriers[1:]:

# Ensure barriers are adjacent before checking if they are mergeable.
if dag._multi_graph.has_edge(end_of_barrier, next_barrier):
if dag._multi_graph.has_edge(end_of_barrier._node_id,
next_barrier._node_id):

# Remove all barriers that have already been included in this new barrier from the
# set of ancestors/descendants as they will be removed from the new DAG when it is
Expand Down
18 changes: 18 additions & 0 deletions releasenotes/notes/retworkx-ccd3019aae7cfb7d.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
---
upgrade:
- |
A new requirement has been added to the requirements list,
`retworkx <https://pypi.org/project/retworkx/>`_. It is an Apache 2.0
licensed graph library that has a similar API to networkx and is being used
to significantly speed up the :class:`qiskit.dagcircuit.DAGCircuit`
operations as part of the transpiler. There are binaries published on PyPI
for all the platforms supported by Qiskit Terra but if you're using a
platform where there aren't precompiled binaries published refer to the
`retworkx documentation
<https://retworkx.readthedocs.io/en/stable/README.html#installing-retworkx>`_
for instructions on pip installing from sdist.
If you encounter any issues with the transpiler or DAGCircuit class as part
of the transition you can switch back to the previous networkx
implementation by setting the environment variable ``USE_RETWORKX`` to
``N``. This option will be removed in the 0.14.0 release.
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ marshmallow>=3,<4
marshmallow_polyfield>=5.7,<6
networkx>=2.2;python_version>'3.5'
networkx>=2.2,<2.4;python_version=='3.5'
retworkx>=0.3.0
numpy>=1.13
ply>=3.10
psutil>=5
Expand Down
1 change: 1 addition & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
"networkx>=2.2;python_version>'3.5'",
# Networkx 2.4 is the final version with python 3.5 support.
"networkx>=2.2,<2.4;python_version=='3.5'",
"retworkx>=0.3.0",
"numpy>=1.13",
"ply>=3.10",
"psutil>=5",
Expand Down
Loading

0 comments on commit 95328ad

Please sign in to comment.