Repository: Qiskit/qiskit

Issue:
Transpiler pass for converting `c_if` to `if_test`
### What should we add?

As developers, we want to transition away from `Instruction.condition` (via the `Instruction{,Set}.c_if` methods) to remove unnecessary state from `Instruction` subclasses.  Part of the backwards compatibility for this support can be a transpiler pass that converts instructions with non-`None` conditions to instances `IfElseBlock`.

This has two main benefits as a temporary measure:
- we will have a path for converting old-style `Instruction` instances within the transpiler, without immediately needing to change the behaviour of `Instruction.c_if`, which is very hard to do within the deprecation policy.
- the dynamic-circuits paths in the IBM provider prefer to have conditionals in this form, in order to more easily apply dynamic decoupling against blocks of instructions.

The first point is the justification for why this is useful to be in Terra (albeit not yet in the preset pass managers - the rest of the support for `IfElseBlock` is too shallow right now), and the second point is why we want this sooner rather than later.

Potential further work: complex conditionals (e.g. `custom_gate.c_if()`) are likely to be expanded by the basis translator into many conditional gates all with the same condition.  Ideally these would be transformed by this pass _before_ basis translation so that they all appear in the same `IfElseBlock`, but right now the support for the control-flow operations in the rest of the transpiler (especially routing) is still being worked on.  One could have a separate classical optimisation pass that runs during optimisation that collects runs of conditionals on the same condition into a single `IfElseBlock`.  This might be moving Terra too far in the direction of classical optimisation, however, and may be better suited for other libraries.

cc: @taalexander.


Discussion:

Resolving patch:
diff --git a/qiskit/transpiler/passes/__init__.py b/qiskit/transpiler/passes/__init__.py
--- a/qiskit/transpiler/passes/__init__.py
+++ b/qiskit/transpiler/passes/__init__.py
@@ -170,6 +170,7 @@
    FixedPoint
    ContainsInstruction
    GatesInBasis
+   ConvertConditionsToIfOps
 """
 
 # layout selection (placement)
@@ -278,3 +279,4 @@
 from .utils import RemoveBarriers
 from .utils import ContainsInstruction
 from .utils import GatesInBasis
+from .utils import ConvertConditionsToIfOps
diff --git a/qiskit/transpiler/passes/utils/__init__.py b/qiskit/transpiler/passes/utils/__init__.py
--- a/qiskit/transpiler/passes/utils/__init__.py
+++ b/qiskit/transpiler/passes/utils/__init__.py
@@ -26,6 +26,7 @@
 from .remove_barriers import RemoveBarriers
 from .contains_instruction import ContainsInstruction
 from .gates_basis import GatesInBasis
+from .convert_conditions_to_if_ops import ConvertConditionsToIfOps
 
 # Utility functions
 from . import control_flow
diff --git a/qiskit/transpiler/passes/utils/convert_conditions_to_if_ops.py b/qiskit/transpiler/passes/utils/convert_conditions_to_if_ops.py
new file mode 100644
--- /dev/null
+++ b/qiskit/transpiler/passes/utils/convert_conditions_to_if_ops.py
@@ -0,0 +1,86 @@
+# This code is part of Qiskit.
+#
+# (C) Copyright IBM 2022.
+#
+# 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.
+
+"""Replace conditional instructions with equivalent :class:`.IfElseOp` objects."""
+
+from qiskit.converters import dag_to_circuit, circuit_to_dag
+from qiskit.circuit import (
+    CircuitInstruction,
+    ClassicalRegister,
+    Clbit,
+    ControlFlowOp,
+    IfElseOp,
+    QuantumCircuit,
+)
+from qiskit.dagcircuit import DAGCircuit
+from qiskit.transpiler import TransformationPass
+
+
+class ConvertConditionsToIfOps(TransformationPass):
+    """Convert instructions whose ``condition`` attribute is set to a non-``None`` value into the
+    equivalent single-statement :class:`.IfElseBlock`.
+
+    This is a simple pass aimed at easing the conversion from the old style of using
+    :meth:`.InstructionSet.c_if` into the new style of using more complex conditional logic."""
+
+    def _run_inner(self, dag):
+        """Run the pass on one :class:`.DAGCircuit`, mutating it.  Returns ``True`` if the circuit
+        was modified and ``False`` if not."""
+        modified = False
+        for node in dag.op_nodes():
+            if isinstance(node.op, ControlFlowOp):
+                modified_blocks = False
+                new_dags = []
+                for block in node.op.blocks:
+                    new_dag = circuit_to_dag(block)
+                    modified_blocks |= self._run_inner(new_dag)
+                    new_dags.append(new_dag)
+                if not modified_blocks:
+                    continue
+                dag.substitute_node(
+                    node,
+                    node.op.replace_blocks(dag_to_circuit(block) for block in new_dags),
+                    inplace=True,
+                )
+            elif getattr(node.op, "condition", None) is None:
+                continue
+            else:
+                target, value = node.op.condition
+                clbits = list(node.cargs)
+                condition_clbits = [target] if isinstance(target, Clbit) else list(target)
+                clbits_set = set(clbits)
+                clbits += [bit for bit in condition_clbits if bit not in clbits_set]
+                block_body = QuantumCircuit(list(node.qargs) + clbits)
+                if isinstance(target, ClassicalRegister):
+                    block_body.add_register(target)
+                new_op = node.op.copy()
+                new_op.condition = None
+                block_body._append(CircuitInstruction(new_op, node.qargs, node.cargs))
+                # Despite only being a node-for-node replacement, control-flow ops contain the
+                # condition bits in their cargs, which requires slightly different handling in the
+                # DAGCircuit methods right now.
+                replacement = DAGCircuit()
+                replacement.add_qubits(block_body.qubits)
+                replacement.add_clbits(block_body.clbits)
+                if isinstance(target, ClassicalRegister):
+                    replacement.add_creg(target)
+                replacement.apply_operation_back(
+                    IfElseOp((target, value), block_body), block_body.qubits, block_body.clbits
+                )
+                wire_map = {bit: bit for bit in block_body.qubits + block_body.clbits}
+                dag.substitute_node_with_dag(node, replacement, wire_map, propagate_condition=False)
+            modified = True
+        return modified
+
+    def run(self, dag):
+        self._run_inner(dag)
+        return dag


Test patch:
diff --git a/test/python/transpiler/test_convert_conditions_to_if_ops.py b/test/python/transpiler/test_convert_conditions_to_if_ops.py
new file mode 100644
--- /dev/null
+++ b/test/python/transpiler/test_convert_conditions_to_if_ops.py
@@ -0,0 +1,139 @@
+# This code is part of Qiskit.
+#
+# (C) Copyright IBM 2022.
+#
+# 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.
+
+# pylint: disable=missing-class-docstring,missing-module-docstring
+
+from qiskit.circuit import QuantumCircuit, QuantumRegister, ClassicalRegister, Qubit, Clbit
+from qiskit.test import QiskitTestCase
+from qiskit.test._canonical import canonicalize_control_flow
+from qiskit.transpiler import PassManager
+from qiskit.transpiler.passes import ConvertConditionsToIfOps
+
+
+class TestConvertConditionsToIfOps(QiskitTestCase):
+    def test_simple_loose_bits(self):
+        """Test that basic conversions work when operating on loose classical bits."""
+        bits = [Qubit(), Qubit(), Clbit(), Clbit()]
+
+        base = QuantumCircuit(bits)
+        base.h(0)
+        base.x(0).c_if(0, 1)
+        base.z(1).c_if(1, 0)
+        base.measure(0, 0)
+        base.measure(1, 1)
+        base.h(0)
+        base.x(0).c_if(0, 1)
+        base.cx(0, 1).c_if(1, 0)
+
+        expected = QuantumCircuit(bits)
+        expected.h(0)
+        with expected.if_test((expected.clbits[0], True)):
+            expected.x(0)
+        with expected.if_test((expected.clbits[1], False)):
+            expected.z(1)
+        expected.measure(0, 0)
+        expected.measure(1, 1)
+        expected.h(0)
+        with expected.if_test((expected.clbits[0], True)):
+            expected.x(0)
+        with expected.if_test((expected.clbits[1], False)):
+            expected.cx(0, 1)
+        expected = canonicalize_control_flow(expected)
+
+        output = PassManager([ConvertConditionsToIfOps()]).run(base)
+        self.assertEqual(output, expected)
+
+    def test_simple_registers(self):
+        """Test that basic conversions work when operating on conditions over registers."""
+        registers = [QuantumRegister(2), ClassicalRegister(2), ClassicalRegister(1)]
+
+        base = QuantumCircuit(*registers)
+        base.h(0)
+        base.x(0).c_if(base.cregs[0], 1)
+        base.z(1).c_if(base.cregs[1], 0)
+        base.measure(0, 0)
+        base.measure(1, 2)
+        base.h(0)
+        base.x(0).c_if(base.cregs[0], 1)
+        base.cx(0, 1).c_if(base.cregs[1], 0)
+
+        expected = QuantumCircuit(*registers)
+        expected.h(0)
+        with expected.if_test((expected.cregs[0], 1)):
+            expected.x(0)
+        with expected.if_test((expected.cregs[1], 0)):
+            expected.z(1)
+        expected.measure(0, 0)
+        expected.measure(1, 2)
+        expected.h(0)
+        with expected.if_test((expected.cregs[0], 1)):
+            expected.x(0)
+        with expected.if_test((expected.cregs[1], 0)):
+            expected.cx(0, 1)
+        expected = canonicalize_control_flow(expected)
+
+        output = PassManager([ConvertConditionsToIfOps()]).run(base)
+        self.assertEqual(output, expected)
+
+    def test_nested_control_flow(self):
+        """Test that the pass successfully converts instructions nested within control-flow
+        blocks."""
+        bits = [Clbit()]
+        registers = [QuantumRegister(3), ClassicalRegister(2)]
+
+        base = QuantumCircuit(*registers, bits)
+        base.x(0).c_if(bits[0], False)
+        with base.if_test((base.cregs[0], 0)) as else_:
+            base.z(1).c_if(bits[0], False)
+        with else_:
+            base.z(1).c_if(base.cregs[0], 1)
+        with base.for_loop(range(2)):
+            with base.while_loop((base.cregs[0], 1)):
+                base.cx(1, 2).c_if(base.cregs[0], 1)
+        base = canonicalize_control_flow(base)
+
+        expected = QuantumCircuit(*registers, bits)
+        with expected.if_test((bits[0], False)):
+            expected.x(0)
+        with expected.if_test((expected.cregs[0], 0)) as else_:
+            with expected.if_test((bits[0], False)):
+                expected.z(1)
+        with else_:
+            with expected.if_test((expected.cregs[0], 1)):
+                expected.z(1)
+        with expected.for_loop(range(2)):
+            with expected.while_loop((expected.cregs[0], 1)):
+                with expected.if_test((expected.cregs[0], 1)):
+                    expected.cx(1, 2)
+        expected = canonicalize_control_flow(expected)
+
+        output = PassManager([ConvertConditionsToIfOps()]).run(base)
+        self.assertEqual(output, expected)
+
+    def test_no_op(self):
+        """Test that the pass works when recursing into control-flow structures, but there's nothing
+        that actually needs replacing."""
+        bits = [Clbit()]
+        registers = [QuantumRegister(3), ClassicalRegister(2)]
+
+        base = QuantumCircuit(*registers, bits)
+        base.x(0)
+        with base.if_test((base.cregs[0], 0)) as else_:
+            base.z(1)
+        with else_:
+            base.z(2)
+        with base.for_loop(range(2)):
+            with base.while_loop((base.cregs[0], 1)):
+                base.cx(1, 2)
+        base = canonicalize_control_flow(base)
+        output = PassManager([ConvertConditionsToIfOps()]).run(base)
+        self.assertEqual(output, base)