Repository: Qiskit/qiskit

Issue:
Create a class method in `DAGCircuit` which returns its disconnected components
JUSTIFICATION:
Circuit cutting requires breaking a quantum circuit up into smaller, disconnected subcircuits and potentially running those subcircuits on different qubit partitions in parallel.

There is currently no easy way to retrieve all of the disconnected subsets of qubits in a given circuit, even though this information is easily accessible in the graph representation of a quantum circuit.

TASK:
Create a class method, `DAGCircuit.separable_circuits`, that returns a list of DAGCircuits -- one for each set of disconnected qubits in the quantum circuit.


Discussion:

Resolving patch:
diff --git a/qiskit/dagcircuit/dagcircuit.py b/qiskit/dagcircuit/dagcircuit.py
--- a/qiskit/dagcircuit/dagcircuit.py
+++ b/qiskit/dagcircuit/dagcircuit.py
@@ -1402,6 +1402,58 @@ def substitute_node(self, node, op, inplace=False):
             self._decrement_op(node.op)
         return new_node
 
+    def separable_circuits(self, remove_idle_qubits=False) -> List["DAGCircuit"]:
+        """Decompose the circuit into sets of qubits with no gates connecting them.
+
+        Args:
+            remove_idle_qubits (bool): Flag denoting whether to remove idle qubits from
+                the separated circuits. If ``False``, each output circuit will contain the
+                same number of qubits as ``self``.
+
+        Returns:
+            List[DAGCircuit]: The circuits resulting from separating ``self`` into sets
+                of disconnected qubits
+
+        Each :class:`~.DAGCircuit` instance returned by this method will contain the same number of
+        clbits as ``self``. The global phase information in ``self`` will not be maintained
+        in the subcircuits returned by this method.
+        """
+        connected_components = rx.weakly_connected_components(self._multi_graph)
+
+        # Collect each disconnected subgraph
+        disconnected_subgraphs = []
+        for components in connected_components:
+            disconnected_subgraphs.append(self._multi_graph.subgraph(list(components)))
+
+        # Helper function for ensuring rustworkx nodes are returned in lexicographical,
+        # topological order
+        def _key(x):
+            return x.sort_key
+
+        # Create new DAGCircuit objects from each of the rustworkx subgraph objects
+        decomposed_dags = []
+        for subgraph in disconnected_subgraphs:
+            new_dag = self.copy_empty_like()
+            new_dag.global_phase = 0
+            subgraph_is_classical = True
+            for node in rx.lexicographical_topological_sort(subgraph, key=_key):
+                if isinstance(node, DAGInNode):
+                    if isinstance(node.wire, Qubit):
+                        subgraph_is_classical = False
+                if not isinstance(node, DAGOpNode):
+                    continue
+                new_dag.apply_operation_back(node.op, node.qargs, node.cargs)
+
+            # Ignore DAGs created for empty clbits
+            if not subgraph_is_classical:
+                decomposed_dags.append(new_dag)
+
+        if remove_idle_qubits:
+            for dag in decomposed_dags:
+                dag.remove_qubits(*(bit for bit in dag.idle_wires() if isinstance(bit, Qubit)))
+
+        return decomposed_dags
+
     def swap_nodes(self, node1, node2):
         """Swap connected nodes e.g. due to commutation.
 


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
@@ -12,6 +12,8 @@
 
 """Test for the DAGCircuit object"""
 
+from __future__ import annotations
+
 from collections import Counter
 import unittest
 
@@ -1300,6 +1302,20 @@ def test_layers_maintains_order(self):
             self.assertEqual(comp, truth)
 
 
+def _sort_key(indices: dict[Qubit, int]):
+    """Return key function for sorting DAGCircuits, given a global qubit mapping."""
+
+    def _min_active_qubit_id(dag):
+        """Transform a DAGCircuit into its minimum active qubit index."""
+        try:
+            first_op = next(dag.topological_op_nodes())
+        except StopIteration:
+            return -1
+        return min(indices[q] for q in first_op.qargs)
+
+    return _min_active_qubit_id
+
+
 class TestCircuitProperties(QiskitTestCase):
     """DAGCircuit properties test."""
 
@@ -1356,6 +1372,125 @@ def test_circuit_factors(self):
         """Test number of separable factors in circuit."""
         self.assertEqual(self.dag.num_tensor_factors(), 2)
 
+    def test_separable_circuits(self):
+        """Test separating disconnected sets of qubits in a circuit."""
+        # Empty case
+        dag = DAGCircuit()
+        self.assertEqual(dag.separable_circuits(), [])
+
+        # 3 disconnected qubits
+        qreg = QuantumRegister(3, "q")
+        creg = ClassicalRegister(2, "c")
+        dag.add_qreg(qreg)
+        dag.add_creg(creg)
+        dag.apply_operation_back(HGate(), [qreg[0]], [])
+        dag.apply_operation_back(HGate(), [qreg[1]], [])
+        dag.apply_operation_back(HGate(), [qreg[2]], [])
+        dag.apply_operation_back(HGate(), [qreg[2]], [])
+
+        comp_dag1 = DAGCircuit()
+        comp_dag1.add_qubits([qreg[0]])
+        comp_dag1.add_creg(creg)
+        comp_dag1.apply_operation_back(HGate(), [qreg[0]], [])
+        comp_dag2 = DAGCircuit()
+        comp_dag2.add_qubits([qreg[1]])
+        comp_dag2.add_creg(creg)
+        comp_dag2.apply_operation_back(HGate(), [qreg[1]], [])
+        comp_dag3 = DAGCircuit()
+        comp_dag3.add_qubits([qreg[2]])
+        comp_dag3.add_creg(creg)
+        comp_dag3.apply_operation_back(HGate(), [qreg[2]], [])
+        comp_dag3.apply_operation_back(HGate(), [qreg[2]], [])
+
+        compare_dags = [comp_dag1, comp_dag2, comp_dag3]
+
+        # Get a mapping from qubit to qubit id in original circuit
+        indices = {bit: i for i, bit in enumerate(dag.qubits)}
+
+        # Don't rely on separable_circuits outputs to be in any order. We sort by min active qubit id
+        dags = sorted(dag.separable_circuits(remove_idle_qubits=True), key=_sort_key(indices))
+
+        self.assertEqual(dags, compare_dags)
+
+        # 2 sets of disconnected qubits
+        dag.apply_operation_back(CXGate(), [qreg[1], qreg[2]], [])
+
+        comp_dag1 = DAGCircuit()
+        comp_dag1.add_qubits([qreg[0]])
+        comp_dag1.add_creg(creg)
+        comp_dag1.apply_operation_back(HGate(), [qreg[0]], [])
+        comp_dag2 = DAGCircuit()
+        comp_dag2.add_qubits([qreg[1], qreg[2]])
+        comp_dag2.add_creg(creg)
+        comp_dag2.apply_operation_back(HGate(), [qreg[1]], [])
+        comp_dag2.apply_operation_back(HGate(), [qreg[2]], [])
+        comp_dag2.apply_operation_back(HGate(), [qreg[2]], [])
+        comp_dag2.apply_operation_back(CXGate(), [qreg[1], qreg[2]], [])
+
+        compare_dags = [comp_dag1, comp_dag2]
+
+        # Don't rely on separable_circuits outputs to be in any order. We sort by min active qubit id
+        dags = sorted(dag.separable_circuits(remove_idle_qubits=True), key=_sort_key(indices))
+
+        self.assertEqual(dags, compare_dags)
+
+        # One connected component
+        dag.apply_operation_back(CXGate(), [qreg[0], qreg[1]], [])
+
+        comp_dag1 = DAGCircuit()
+        comp_dag1.add_qreg(qreg)
+        comp_dag1.add_creg(creg)
+        comp_dag1.apply_operation_back(HGate(), [qreg[0]], [])
+        comp_dag1.apply_operation_back(HGate(), [qreg[1]], [])
+        comp_dag1.apply_operation_back(HGate(), [qreg[2]], [])
+        comp_dag1.apply_operation_back(HGate(), [qreg[2]], [])
+        comp_dag1.apply_operation_back(CXGate(), [qreg[1], qreg[2]], [])
+        comp_dag1.apply_operation_back(CXGate(), [qreg[0], qreg[1]], [])
+
+        compare_dags = [comp_dag1]
+
+        # Don't rely on separable_circuits outputs to be in any order. We sort by min active qubit id
+        dags = sorted(dag.separable_circuits(remove_idle_qubits=True), key=_sort_key(indices))
+
+        self.assertEqual(dags, compare_dags)
+
+    def test_separable_circuits_w_measurements(self):
+        """Test separating disconnected sets of qubits in a circuit with measurements."""
+        # Test circuit ordering with measurements
+        dag = DAGCircuit()
+        qreg = QuantumRegister(3, "q")
+        creg = ClassicalRegister(1, "c")
+        dag.add_qreg(qreg)
+        dag.add_creg(creg)
+        dag.apply_operation_back(HGate(), [qreg[0]], [])
+        dag.apply_operation_back(XGate(), [qreg[0]], [])
+        dag.apply_operation_back(XGate(), [qreg[1]], [])
+        dag.apply_operation_back(HGate(), [qreg[1]], [])
+        dag.apply_operation_back(YGate(), [qreg[2]], [])
+        dag.apply_operation_back(HGate(), [qreg[2]], [])
+        dag.apply_operation_back(Measure(), [qreg[0]], [creg[0]])
+
+        qc1 = QuantumCircuit(3, 1)
+        qc1.h(0)
+        qc1.x(0)
+        qc1.measure(0, 0)
+        qc2 = QuantumCircuit(3, 1)
+        qc2.x(1)
+        qc2.h(1)
+        qc3 = QuantumCircuit(3, 1)
+        qc3.y(2)
+        qc3.h(2)
+        qcs = [qc1, qc2, qc3]
+        compare_dags = [circuit_to_dag(qc) for qc in qcs]
+
+        # Get a mapping from qubit to qubit id in original circuit
+        indices = {bit: i for i, bit in enumerate(dag.qubits)}
+
+        # Don't rely on separable_circuits outputs to be in any order. We sort by min active qubit id
+        dags = sorted(dag.separable_circuits(), key=_sort_key(indices))
+
+        self.assertEqual(dags, compare_dags)
+
     def test_default_metadata_value(self):
         """Test that the default DAGCircuit metadata is valid QuantumCircuit metadata."""
         qc = QuantumCircuit(1)