Repository: Qiskit/qiskit

Issue:
Use `contract_nodes` from retworkx for ConsolidateBlocks pass
### What is the expected enhancement?

For improved performance and cleaner code, we should use `PyDiGraph.contract_nodes` to replace a bulk of the logic in `DAGCircuit.replace_block_with_op`, once it becomes available in a future retworkx release.

Contract nodes PR: https://github.com/Qiskit/retworkx/pull/487


Discussion:
Removing on hold because retworkx 0.11.0 was released which included the `contract_nodes()` method. So this can be implemented

Resolving patch:
diff --git a/qiskit/dagcircuit/dagcircuit.py b/qiskit/dagcircuit/dagcircuit.py
--- a/qiskit/dagcircuit/dagcircuit.py
+++ b/qiskit/dagcircuit/dagcircuit.py
@@ -1031,13 +1031,13 @@ def replace_block_with_op(self, node_block, op, wire_pos_map, cycle_check=True):
                 block with
             wire_pos_map (Dict[Qubit, int]): The dictionary mapping the qarg to
                 the position. This is necessary to reconstruct the qarg order
-                over multiple gates in the combined singe op node.
+                over multiple gates in the combined single op node.
             cycle_check (bool): When set to True this method will check that
                 replacing the provided ``node_block`` with a single node
                 would introduce a a cycle (which would invalidate the
                 ``DAGCircuit``) and will raise a ``DAGCircuitError`` if a cycle
                 would be introduced. This checking comes with a run time
-                penalty, if you can guarantee that your input ``node_block`` is
+                penalty. If you can guarantee that your input ``node_block`` is
                 a contiguous block and won't introduce a cycle when it's
                 contracted to a single node, this can be set to ``False`` to
                 improve the runtime performance of this method.
@@ -1047,56 +1047,37 @@ def replace_block_with_op(self, node_block, op, wire_pos_map, cycle_check=True):
                 the specified block introduces a cycle or if ``node_block`` is
                 empty.
         """
-        # TODO: Replace this with a function in retworkx to do this operation in
-        # the graph
-        block_preds = defaultdict(set)
-        block_succs = defaultdict(set)
         block_qargs = set()
         block_cargs = set()
-        block_ids = {x._node_id for x in node_block}
+        block_ids = [x._node_id for x in node_block]
 
         # If node block is empty return early
         if not node_block:
             raise DAGCircuitError("Can't replace an empty node_block")
 
         for nd in node_block:
-            for parent_id, _, edge in self._multi_graph.in_edges(nd._node_id):
-                if parent_id not in block_ids:
-                    block_preds[parent_id].add(edge)
-            for _, child_id, edge in self._multi_graph.out_edges(nd._node_id):
-                if child_id not in block_ids:
-                    block_succs[child_id].add(edge)
             block_qargs |= set(nd.qargs)
             if isinstance(nd, DAGOpNode) and nd.op.condition:
                 block_cargs |= set(nd.cargs)
-        if cycle_check:
-            # If we're cycle checking copy the graph to ensure we don't create
-            # invalid DAG when we encounter a cycle
-            backup_graph = self._multi_graph.copy()
-        # Add node and wire it into graph
-        new_index = self._add_op_node(
+
+        # Create replacement node
+        new_node = DAGOpNode(
             op,
             sorted(block_qargs, key=lambda x: wire_pos_map[x]),
             sorted(block_cargs, key=lambda x: wire_pos_map[x]),
         )
-        for node_id, edges in block_preds.items():
-            for edge in edges:
-                self._multi_graph.add_edge(node_id, new_index, edge)
-        for node_id, edges in block_succs.items():
-            for edge in edges:
-                self._multi_graph.add_edge(new_index, node_id, edge)
-        for nd in node_block:
-            self._multi_graph.remove_node(nd._node_id)
-        # If enabled ensure block won't introduce a cycle when node_block is
-        # contracted
-        if cycle_check:
-            # If a cycle was introduced remove new op node and raise error
-            if not rx.is_directed_acyclic_graph(self._multi_graph):
-                # If a cycle was encountered restore the graph to the
-                # original valid state
-                self._multi_graph = backup_graph
-                self._decrement_op(op)
-                raise DAGCircuitError("Replacing the specified node block would introduce a cycle")
+
+        try:
+            new_node._node_id = self._multi_graph.contract_nodes(
+                block_ids, new_node, check_cycle=cycle_check
+            )
+        except rx.DAGWouldCycle as ex:
+            raise DAGCircuitError(
+                "Replacing the specified node block would introduce a cycle"
+            ) from ex
+
+        self._increment_op(op)
+
         for nd in node_block:
             self._decrement_op(nd.op)
 


Test patch:
diff --git a/test/python/dagcircuit/test_dagcircuit.py b/test/python/dagcircuit/test_dagcircuit.py
--- a/test/python/dagcircuit/test_dagcircuit.py
+++ b/test/python/dagcircuit/test_dagcircuit.py
@@ -1605,6 +1605,7 @@ def test_single_node_block(self):
         expected_dag.apply_operation_back(XGate(), [qr[0]])
 
         self.assertEqual(expected_dag, dag)
+        self.assertEqual(expected_dag.count_ops(), dag.count_ops())
 
 
 class TestDagProperties(QiskitTestCase):