Repository: Qiskit/qiskit

Issue:
Add support for mapping `Expr` values in DAG/QC converters
### What should we add?

Once `Expr` values are valid in some `.condition` and `.target` attributes in `QuantumCircuit` instructions, they need to be supported through the `circuit_to_dag` and `dag_to_circuit` converters.  Especially in `circuit_to_dag`, we must ensure that any implied classical dependencies between instructions are represented in the wires in the `DAGCircuit`.

The work here will mostly be in ensuring that `DAGCircuit.apply_operation_back` has this handling.  We should also ensure that `DAGCircuit.apply_operation_front` has the same handling.

Depends on #10223 and #10224.


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
@@ -30,7 +30,7 @@
 import rustworkx as rx
 
 from qiskit.circuit import ControlFlowOp, ForLoopOp, IfElseOp, WhileLoopOp, SwitchCaseOp
-from qiskit.circuit.controlflow import condition_resources
+from qiskit.circuit.controlflow import condition_resources, node_resources
 from qiskit.circuit.quantumregister import QuantumRegister, Qubit
 from qiskit.circuit.classicalregister import ClassicalRegister, Clbit
 from qiskit.circuit.gate import Gate
@@ -476,17 +476,26 @@ def _check_bits(self, args, amap):
                 raise DAGCircuitError(f"(qu)bit {wire} not found in {amap}")
 
     @staticmethod
-    def _bits_in_condition(cond):
-        """Return a list of bits in the given condition.
+    def _bits_in_operation(operation):
+        """Return an iterable over the classical bits that are inherent to an instruction.  This
+        includes a `condition`, or the `target` of a :class:`.ControlFlowOp`.
 
         Args:
-            cond (tuple or expr.Expr or None): optional condition in any form that the control-flow
-                operations accept.
+            instruction: the :class:`~.circuit.Instruction` instance for a node.
 
         Returns:
-            list[Clbit]: list of classical bits
+            Iterable[Clbit]: the :class:`.Clbit`\\ s involved.
         """
-        return [] if cond is None else list(condition_resources(cond).clbits)
+        if (condition := getattr(operation, "condition", None)) is not None:
+            yield from condition_resources(condition).clbits
+        if isinstance(operation, SwitchCaseOp):
+            target = operation.target
+            if isinstance(target, Clbit):
+                yield target
+            elif isinstance(target, ClassicalRegister):
+                yield from target
+            else:
+                yield from node_resources(target).clbits
 
     def _increment_op(self, op):
         if op.name in self._op_names:
@@ -571,8 +580,7 @@ def apply_operation_back(self, op, qargs=(), cargs=()):
         qargs = tuple(qargs) if qargs is not None else ()
         cargs = tuple(cargs) if cargs is not None else ()
 
-        all_cbits = self._bits_in_condition(getattr(op, "condition", None))
-        all_cbits = set(all_cbits).union(cargs)
+        all_cbits = set(self._bits_in_operation(op)).union(cargs)
 
         self._check_condition(op.name, getattr(op, "condition", None))
         self._check_bits(qargs, self.output_map)
@@ -603,8 +611,7 @@ def apply_operation_front(self, op, qargs=(), cargs=()):
         Raises:
             DAGCircuitError: if initial nodes connected to multiple out edges
         """
-        all_cbits = self._bits_in_condition(getattr(op, "condition", None))
-        all_cbits.extend(cargs)
+        all_cbits = set(self._bits_in_operation(op)).union(cargs)
 
         self._check_condition(op.name, getattr(op, "condition", None))
         self._check_bits(qargs, self.input_map)
@@ -1183,9 +1190,7 @@ def substitute_node_with_dag(self, node, input_dag, wires=None, propagate_condit
             # condition as well.
             if not propagate_condition:
                 node_wire_order += [
-                    bit
-                    for bit in self._bits_in_condition(getattr(node.op, "condition", None))
-                    if bit not in node_cargs
+                    bit for bit in self._bits_in_operation(node.op) if bit not in node_cargs
                 ]
             if len(wires) != len(node_wire_order):
                 raise DAGCircuitError(
@@ -1729,8 +1734,6 @@ def serial_layers(self):
             op = copy.copy(next_node.op)
             qargs = copy.copy(next_node.qargs)
             cargs = copy.copy(next_node.cargs)
-            condition = copy.copy(getattr(next_node.op, "condition", None))
-            _ = self._bits_in_condition(condition)
 
             # Add node to new_layer
             new_layer.apply_operation_back(op, qargs, cargs)
diff --git a/qiskit/transpiler/passes/layout/sabre_layout.py b/qiskit/transpiler/passes/layout/sabre_layout.py
--- a/qiskit/transpiler/passes/layout/sabre_layout.py
+++ b/qiskit/transpiler/passes/layout/sabre_layout.py
@@ -311,7 +311,7 @@ def _inner_run(self, dag, coupling_map):
         for node in dag.topological_op_nodes():
             cargs = {original_clbit_indices[x] for x in node.cargs}
             if node.op.condition is not None:
-                for clbit in dag._bits_in_condition(node.op.condition):
+                for clbit in dag._bits_in_operation(node.op):
                     cargs.add(original_clbit_indices[clbit])
 
             dag_list.append(
diff --git a/qiskit/transpiler/passes/routing/sabre_swap.py b/qiskit/transpiler/passes/routing/sabre_swap.py
--- a/qiskit/transpiler/passes/routing/sabre_swap.py
+++ b/qiskit/transpiler/passes/routing/sabre_swap.py
@@ -240,7 +240,7 @@ def run(self, dag):
         for node in dag.topological_op_nodes():
             cargs = {self._clbit_indices[x] for x in node.cargs}
             if node.op.condition is not None:
-                for clbit in dag._bits_in_condition(node.op.condition):
+                for clbit in dag._bits_in_operation(node.op):
                     cargs.add(self._clbit_indices[clbit])
 
             dag_list.append(


Test patch:
diff --git a/test/python/converters/test_circuit_to_dag.py b/test/python/converters/test_circuit_to_dag.py
--- a/test/python/converters/test_circuit_to_dag.py
+++ b/test/python/converters/test_circuit_to_dag.py
@@ -14,8 +14,9 @@
 
 import unittest
 
+from qiskit.circuit import QuantumRegister, ClassicalRegister, QuantumCircuit, Clbit
+from qiskit.circuit.classical import expr
 from qiskit.converters import dag_to_circuit, circuit_to_dag
-from qiskit import QuantumRegister, ClassicalRegister, QuantumCircuit
 from qiskit.test import QiskitTestCase
 
 
@@ -51,6 +52,58 @@ def test_calibrations(self):
         circuit_out = dag_to_circuit(dag)
         self.assertEqual(len(circuit_out.calibrations), 1)
 
+    def test_wires_from_expr_nodes_condition(self):
+        """Test that the classical wires implied by an `Expr` node in a control-flow op's
+        `condition` are correctly transferred."""
+        # The control-flow builder interface always includes any classical wires in the blocks of
+        # the operation, so we test by using manually constructed blocks that don't do that.  It's
+        # not required by the `QuantumCircuit` model (just like `c_if` instructions don't expand
+        # their `cargs`).
+        inner = QuantumCircuit(1)
+        inner.x(0)
+        cr1 = ClassicalRegister(2, "a")
+        cr2 = ClassicalRegister(2, "b")
+        clbit = Clbit()
+        outer = QuantumCircuit(QuantumRegister(1), cr1, cr2, [clbit])
+        # Note that 'cr2' is not in the condition.
+        outer.if_test(expr.logic_and(expr.equal(cr1, 3), expr.logic_not(clbit)), inner, [0], [])
+        outer.while_loop(expr.logic_or(expr.less(2, cr1), clbit), inner, [0], [])
+
+        dag = circuit_to_dag(outer)
+        expected_wires = set(outer.qubits) | set(cr1) | {clbit}
+        for node in dag.topological_op_nodes():
+            test_wires = {wire for _source, _dest, wire in dag.edges(node)}
+            self.assertIsInstance(node.op.condition, expr.Expr)
+            self.assertEqual(test_wires, expected_wires)
+
+        roundtripped = dag_to_circuit(dag)
+        for original, test in zip(outer, roundtripped):
+            self.assertEqual(original.operation.condition, test.operation.condition)
+
+    def test_wires_from_expr_nodes_target(self):
+        """Test that the classical wires implied by an `Expr` node in a control-flow op's
+        `target` are correctly transferred."""
+        case_1 = QuantumCircuit(1)
+        case_1.x(0)
+        case_2 = QuantumCircuit(1)
+        case_2.y(0)
+        cr1 = ClassicalRegister(2, "a")
+        cr2 = ClassicalRegister(2, "b")
+        outer = QuantumCircuit(QuantumRegister(1), cr1, cr2)
+        # Note that 'cr2' is not in the condition.
+        outer.switch(expr.bit_and(cr1, 2), [(1, case_1), (2, case_2)], [0], [])
+
+        dag = circuit_to_dag(outer)
+        expected_wires = set(outer.qubits) | set(cr1)
+        for node in dag.topological_op_nodes():
+            test_wires = {wire for _source, _dest, wire in dag.edges(node)}
+            self.assertIsInstance(node.op.target, expr.Expr)
+            self.assertEqual(test_wires, expected_wires)
+
+        roundtripped = dag_to_circuit(dag)
+        for original, test in zip(outer, roundtripped):
+            self.assertEqual(original.operation.target, test.operation.target)
+
 
 if __name__ == "__main__":
     unittest.main(verbosity=2)
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
@@ -20,25 +20,26 @@
 import rustworkx as rx
 from numpy import pi
 
-from qiskit.dagcircuit import DAGCircuit, DAGOpNode, DAGInNode, DAGOutNode
-from qiskit.circuit import QuantumRegister
-from qiskit.circuit import ClassicalRegister, Clbit
-from qiskit.circuit import QuantumCircuit, Qubit
-from qiskit.circuit import Measure
-from qiskit.circuit import Reset
-from qiskit.circuit import Delay
-from qiskit.circuit import Gate, Instruction
-from qiskit.circuit import Parameter
-from qiskit.circuit.library.standard_gates.i import IGate
-from qiskit.circuit.library.standard_gates.h import HGate
-from qiskit.circuit.library.standard_gates.x import CXGate
-from qiskit.circuit.library.standard_gates.z import CZGate
-from qiskit.circuit.library.standard_gates.x import XGate
-from qiskit.circuit.library.standard_gates.y import YGate
-from qiskit.circuit.library.standard_gates.u1 import U1Gate
-from qiskit.circuit.library.standard_gates.rx import RXGate
-from qiskit.circuit.barrier import Barrier
-from qiskit.dagcircuit.exceptions import DAGCircuitError
+from qiskit.dagcircuit import DAGCircuit, DAGOpNode, DAGInNode, DAGOutNode, DAGCircuitError
+from qiskit.circuit import (
+    QuantumCircuit,
+    QuantumRegister,
+    ClassicalRegister,
+    Clbit,
+    Qubit,
+    Measure,
+    Delay,
+    Reset,
+    Gate,
+    Instruction,
+    Parameter,
+    Barrier,
+    SwitchCaseOp,
+    IfElseOp,
+    WhileLoopOp,
+)
+from qiskit.circuit.classical import expr
+from qiskit.circuit.library import IGate, HGate, CXGate, CZGate, XGate, YGate, U1Gate, RXGate
 from qiskit.converters import circuit_to_dag
 from qiskit.test import QiskitTestCase
 
@@ -580,6 +581,68 @@ def test_apply_operation_front(self):
 
         self.assertIn(reset_node, set(self.dag.predecessors(h_node)))
 
+    def test_apply_operation_expr_condition(self):
+        """Test that the operation-applying functions correctly handle wires implied from `Expr`
+        nodes in the `condition` field of `ControlFlowOp` instances."""
+        inner = QuantumCircuit(1)
+        inner.x(0)
+
+        qr = QuantumRegister(1)
+        cr1 = ClassicalRegister(2, "a")
+        cr2 = ClassicalRegister(2, "b")
+        clbit = Clbit()
+
+        dag = DAGCircuit()
+        dag.add_qreg(qr)
+        dag.add_creg(cr1)
+        dag.add_creg(cr2)
+        dag.add_clbits([clbit])
+
+        # Note that 'cr2' is not in either condition.
+        expected_wires = set(qr) | set(cr1) | {clbit}
+
+        op = IfElseOp(expr.logic_and(expr.equal(cr1, 3), expr.logic_not(clbit)), inner, None)
+        node = dag.apply_operation_back(op, qr, ())
+        test_wires = {wire for _source, _dest, wire in dag.edges(node)}
+        self.assertIsInstance(node.op.condition, expr.Expr)
+        self.assertEqual(test_wires, expected_wires)
+
+        op = WhileLoopOp(expr.logic_or(expr.less(2, cr1), clbit), inner)
+        node = dag.apply_operation_front(op, qr, ())
+        test_wires = {wire for _source, _dest, wire in dag.edges(node)}
+        self.assertIsInstance(node.op.condition, expr.Expr)
+        self.assertEqual(test_wires, expected_wires)
+
+    def test_apply_operation_expr_target(self):
+        """Test that the operation-applying functions correctly handle wires implied from `Expr`
+        nodes in the `target` field of `SwitchCaseOp`."""
+        case_1 = QuantumCircuit(1)
+        case_1.x(0)
+        case_2 = QuantumCircuit(1)
+        case_2.y(0)
+        qr = QuantumRegister(1)
+        cr1 = ClassicalRegister(2, "a")
+        cr2 = ClassicalRegister(2, "b")
+        # Note that 'cr2' is not in the condition.
+        op = SwitchCaseOp(expr.bit_and(cr1, 2), [(1, case_1), (2, case_2)])
+
+        expected_wires = set(qr) | set(cr1)
+
+        dag = DAGCircuit()
+        dag.add_qreg(qr)
+        dag.add_creg(cr1)
+        dag.add_creg(cr2)
+
+        node = dag.apply_operation_back(op, qr, ())
+        test_wires = {wire for _source, _dest, wire in dag.edges(node)}
+        self.assertIsInstance(node.op.target, expr.Expr)
+        self.assertEqual(test_wires, expected_wires)
+
+        node = dag.apply_operation_front(op, qr, ())
+        test_wires = {wire for _source, _dest, wire in dag.edges(node)}
+        self.assertIsInstance(node.op.target, expr.Expr)
+        self.assertEqual(test_wires, expected_wires)
+
 
 class TestDagNodeSelection(QiskitTestCase):
     """Test methods that select certain dag nodes"""