Performance improvements for collect_2q_blocks (#6433)

* Use find_successors_by_edge in quantum_successors

* Add is_successors to DAGCircuit and collect_2q

* Replace qargs with _qargs in collect_2q_blocks

* Replace op with _op in collect_2q_blocks

* Add tests for is_sucessor/is_predecessor

* Modify requirements.txt temporarily to fetch latest retworkx code

* Correct syntax in requirements.txt to temporarily fetch latest retworkx

* Use Qiskit/retworkx@main

* Update requirements.txt

* Add release note about retworkx upgrade

* Update releasenotes/notes/bump-retworkx-59c7ab59ad02c3c0.yaml

Co-authored-by: Matthew Treinish <mtreinish@kortar.org>

* Fix indentation in release note

Co-authored-by: Matthew Treinish <mtreinish@kortar.org>
Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
This commit is contained in:
Ivan Carvalho 2021-06-08 11:42:11 -07:00 committed by GitHub
parent 33e7448a22
commit 0240377262
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 107 additions and 65 deletions

View File

@ -1302,17 +1302,22 @@ class DAGCircuit:
"""Returns iterator of the predecessors of a node as DAGNodes."""
return iter(self._multi_graph.predecessors(node._node_id))
def is_successor(self, node, node_succ):
"""Checks if a second node is in the successors of node."""
return self._multi_graph.has_edge(node._node_id, node_succ._node_id)
def is_predecessor(self, node, node_pred):
"""Checks if a second node is in the predecessors of node."""
return self._multi_graph.has_edge(node_pred._node_id, node._node_id)
def quantum_predecessors(self, node):
"""Returns iterator of the predecessors of a node that are
connected by a quantum edge as DAGNodes."""
for predecessor in self.predecessors(node):
if any(
isinstance(edge_data, Qubit)
for edge_data in self._multi_graph.get_all_edge_data(
predecessor._node_id, node._node_id
)
):
yield predecessor
return iter(
self._multi_graph.find_predecessors_by_edge(
node._node_id, lambda edge_data: isinstance(edge_data, Qubit)
)
)
def ancestors(self, node):
"""Returns set of the ancestors of a node as DAGNodes."""
@ -1332,14 +1337,11 @@ class DAGCircuit:
def quantum_successors(self, node):
"""Returns iterator of the successors of a node that are
connected by a quantum edge as DAGNodes."""
for successor in self.successors(node):
if any(
isinstance(edge_data, Qubit)
for edge_data in self._multi_graph.get_all_edge_data(
node._node_id, successor._node_id
)
):
yield successor
return iter(
self._multi_graph.find_successors_by_edge(
node._node_id, lambda edge_data: isinstance(edge_data, Qubit)
)
)
def remove_op_node(self, node):
"""Remove an operation node n.

View File

@ -58,13 +58,13 @@ class Collect2qBlocks(AnalysisPass):
# Explore predecessors and successors of 2q gates
if ( # pylint: disable=too-many-boolean-expressions
nd.type == "op"
and isinstance(nd.op, Gate)
and len(nd.qargs) == 2
and isinstance(nd._op, Gate)
and len(nd._qargs) == 2
and not nodes_seen[nd]
and nd.op.condition is None
and not nd.op.is_parameterized()
and nd._op.condition is None
and not nd._op.is_parameterized()
):
these_qubits = set(nd.qargs)
these_qubits = set(nd._qargs)
# Explore predecessors of the 2q node
pred = list(dag.quantum_predecessors(nd))
explore = True
@ -75,13 +75,13 @@ class Collect2qBlocks(AnalysisPass):
pnd = pred[0]
if (
pnd.type == "op"
and isinstance(pnd.op, Gate)
and len(pnd.qargs) <= 2
and pnd.op.condition is None
and not pnd.op.is_parameterized()
and isinstance(pnd._op, Gate)
and len(pnd._qargs) <= 2
and pnd._op.condition is None
and not pnd._op.is_parameterized()
):
if (len(pnd.qargs) == 2 and set(pnd.qargs) == these_qubits) or len(
pnd.qargs
if (len(pnd._qargs) == 2 and set(pnd._qargs) == these_qubits) or len(
pnd._qargs
) == 1:
group.append(pnd)
nodes_seen[pnd] = True
@ -89,52 +89,52 @@ class Collect2qBlocks(AnalysisPass):
# If there are two, then we consider cases
elif len(pred) == 2:
# First, check if there is a relationship
if pred[0] in dag.predecessors(pred[1]):
if dag.is_predecessor(pred[1], pred[0]):
sorted_pred = [pred[1]] # was [pred[1], pred[0]]
elif pred[1] in dag.predecessors(pred[0]):
elif dag.is_predecessor(pred[0], pred[1]):
sorted_pred = [pred[0]] # was [pred[0], pred[1]]
else:
# We need to avoid accidentally adding a 2q gate on these_qubits
# since these must have a dependency through the other predecessor
# in this case
if len(pred[0].qargs) == 2 and set(pred[0].qargs) == these_qubits:
if len(pred[0]._qargs) == 2 and set(pred[0]._qargs) == these_qubits:
sorted_pred = [pred[1]]
elif len(pred[1].qargs) == 1 and set(pred[1].qargs) == these_qubits:
elif len(pred[1]._qargs) == 1 and set(pred[1]._qargs) == these_qubits:
sorted_pred = [pred[0]]
else:
sorted_pred = pred
if (
len(sorted_pred) == 2
and len(sorted_pred[0].qargs) == 2
and len(sorted_pred[1].qargs) == 2
and len(sorted_pred[0]._qargs) == 2
and len(sorted_pred[1]._qargs) == 2
):
break # stop immediately if we hit a pair of 2q gates
# Examine each predecessor
for pnd in sorted_pred:
if (
pnd.type != "op"
or not isinstance(pnd.op, Gate)
or len(pnd.qargs) > 2
or pnd.op.condition is not None
or pnd.op.is_parameterized()
or not isinstance(pnd._op, Gate)
or len(pnd._qargs) > 2
or pnd._op.condition is not None
or pnd._op.is_parameterized()
):
# remove any qubits that are interrupted by a gate
# e.g. a measure in the middle of the circuit
these_qubits = list(set(these_qubits) - set(pnd.qargs))
these_qubits = list(set(these_qubits) - set(pnd._qargs))
continue
# If a predecessor is a single qubit gate, add it
if len(pnd.qargs) == 1 and not pnd.op.is_parameterized():
if len(pnd._qargs) == 1 and not pnd._op.is_parameterized():
if not nodes_seen[pnd]:
group.append(pnd)
nodes_seen[pnd] = True
pred_next.extend(dag.quantum_predecessors(pnd))
# If 2q, check qubits
else:
pred_qubits = set(pnd.qargs)
pred_qubits = set(pnd._qargs)
if (
pred_qubits == these_qubits
and pnd.op.condition is None
and not pnd.op.is_parameterized()
and pnd._op.condition is None
and not pnd._op.is_parameterized()
):
# add if on same qubits
if not nodes_seen[pnd]:
@ -154,7 +154,7 @@ class Collect2qBlocks(AnalysisPass):
group.append(nd)
nodes_seen[nd] = True
# Reset these_qubits
these_qubits = set(nd.qargs)
these_qubits = set(nd._qargs)
# Explore successors of the 2q node
succ = list(dag.quantum_successors(nd))
explore = True
@ -165,13 +165,13 @@ class Collect2qBlocks(AnalysisPass):
snd = succ[0]
if (
snd.type == "op"
and isinstance(snd.op, Gate)
and len(snd.qargs) <= 2
and snd.op.condition is None
and not snd.op.is_parameterized()
and isinstance(snd._op, Gate)
and len(snd._qargs) <= 2
and snd._op.condition is None
and not snd._op.is_parameterized()
):
if (len(snd.qargs) == 2 and set(snd.qargs) == these_qubits) or len(
snd.qargs
if (len(snd._qargs) == 2 and set(snd._qargs) == these_qubits) or len(
snd._qargs
) == 1:
group.append(snd)
nodes_seen[snd] = True
@ -179,55 +179,55 @@ class Collect2qBlocks(AnalysisPass):
# If there are two, then we consider cases
elif len(succ) == 2:
# First, check if there is a relationship
if succ[0] in dag.successors(succ[1]):
if dag.is_successor(succ[1], succ[0]):
sorted_succ = [succ[1]] # was [succ[1], succ[0]]
elif succ[1] in dag.successors(succ[0]):
elif dag.is_successor(succ[0], succ[1]):
sorted_succ = [succ[0]] # was [succ[0], succ[1]]
else:
# We need to avoid accidentally adding a 2q gate on these_qubits
# since these must have a dependency through the other successor
# in this case
if len(succ[0].qargs) == 2 and set(succ[0].qargs) == these_qubits:
if len(succ[0]._qargs) == 2 and set(succ[0]._qargs) == these_qubits:
sorted_succ = [succ[1]]
elif len(succ[1].qargs) == 2 and set(succ[1].qargs) == these_qubits:
elif len(succ[1]._qargs) == 2 and set(succ[1]._qargs) == these_qubits:
sorted_succ = [succ[0]]
else:
sorted_succ = succ
if (
len(sorted_succ) == 2
and len(sorted_succ[0].qargs) == 2
and len(sorted_succ[1].qargs) == 2
and len(sorted_succ[0]._qargs) == 2
and len(sorted_succ[1]._qargs) == 2
):
break # stop immediately if we hit a pair of 2q gates
# Examine each successor
for snd in sorted_succ:
if (
snd.type != "op"
or not isinstance(snd.op, Gate)
or len(snd.qargs) > 2
or snd.op.condition is not None
or snd.op.is_parameterized()
or not isinstance(snd._op, Gate)
or len(snd._qargs) > 2
or snd._op.condition is not None
or snd._op.is_parameterized()
):
# remove qubits from consideration if interrupted
# by a gate e.g. a measure in the middle of the circuit
these_qubits = list(set(these_qubits) - set(snd.qargs))
these_qubits = list(set(these_qubits) - set(snd._qargs))
continue
# If a successor is a single qubit gate, add it
# NB as we have eliminated all gates with names not in
# good_names, this check guarantees they are single qubit
if len(snd.qargs) == 1 and not snd.op.is_parameterized():
if len(snd._qargs) == 1 and not snd._op.is_parameterized():
if not nodes_seen[snd]:
group.append(snd)
nodes_seen[snd] = True
succ_next.extend(dag.quantum_successors(snd))
else:
# If 2q, check qubits
succ_qubits = set(snd.qargs)
succ_qubits = set(snd._qargs)
if (
succ_qubits == these_qubits
and snd.op.condition is None
and not snd.op.is_parameterized()
and snd._op.condition is None
and not snd._op.is_parameterized()
):
# add if on same qubits
if not nodes_seen[snd]:

View File

@ -0,0 +1,11 @@
features:
- |
Added two new methods, :meth:`~qiskit.dagcircuit.DAGCircuit.is_successor` and
:meth:`~qiskit.dagcircuit.DAGCircuit.is_predecessor`, to the
:class:`~qiskit.dagcircuit.DAGCircuit` class. These functions are used to check if a node
is either a successor or predecessor of another node on the :class:`~qiskit.dagcircuit.DAGCircuit`.
upgrade:
- |
The minimum version of the `retworkx <https://pypi.org/project/retworkx/>`_ dependency
was increased to version `0.9.0`. This was done to use new APIs introduced in that release
which improved the performance of some transpiler passes.

View File

@ -1,6 +1,6 @@
contextvars>=2.4;python_version<'3.7'
jsonschema>=2.6
retworkx>=0.8.0
retworkx>=0.9.0
numpy>=1.17
ply>=3.10
psutil>=5

View File

@ -515,6 +515,20 @@ class TestDagNodeSelection(QiskitTestCase):
or (successor2.type == "out" and isinstance(successor1.op, Reset))
)
def test_is_successor(self):
"""The method dag.is_successor(A, B) checks if node B is a successor of A"""
self.dag.apply_operation_back(Measure(), [self.qubit1, self.clbit1], [])
self.dag.apply_operation_back(CXGate(), [self.qubit0, self.qubit1], [])
self.dag.apply_operation_back(Reset(), [self.qubit0], [])
measure_node = self.dag.named_nodes("measure")[0]
cx_node = self.dag.named_nodes("cx")[0]
reset_node = self.dag.named_nodes("reset")[0]
self.assertTrue(self.dag.is_successor(measure_node, cx_node))
self.assertFalse(self.dag.is_successor(measure_node, reset_node))
self.assertTrue(self.dag.is_successor(cx_node, reset_node))
def test_quantum_predecessors(self):
"""The method dag.quantum_predecessors() returns predecessors connected by quantum edges"""
@ -550,6 +564,21 @@ class TestDagNodeSelection(QiskitTestCase):
or (predecessor2.type == "in" and isinstance(predecessor1.op, Reset))
)
def test_is_predecessor(self):
"""The method dag.is_predecessor(A, B) checks if node B is a predecessor of A"""
self.dag.apply_operation_back(Measure(), [self.qubit1, self.clbit1], [])
self.dag.apply_operation_back(CXGate(), [self.qubit0, self.qubit1], [])
self.dag.apply_operation_back(Reset(), [self.qubit0], [])
measure_node = self.dag.named_nodes("measure")[0]
cx_node = self.dag.named_nodes("cx")[0]
reset_node = self.dag.named_nodes("reset")[0]
self.assertTrue(self.dag.is_predecessor(cx_node, measure_node))
self.assertFalse(self.dag.is_predecessor(reset_node, measure_node))
self.assertTrue(self.dag.is_predecessor(reset_node, cx_node))
def test_get_gates_nodes(self):
"""The method dag.gate_nodes() returns all gate nodes"""
self.dag.apply_operation_back(HGate(), [self.qubit0], [])