Repository: Qiskit/qiskit

Issue:
Enable control flow in `optimization_level=3`
### What should we add?

### What should we add?

Sub-epic of #9417.

This is the list of issues needed to support control flow at optimisation level 3 in `transpile`.

Depends on:
- [x] #9419
- [x] #9421 
- [x] #9423 
- [x] #9425 
- [x] #9426

Once all the dependants are satisfied (add more to the list as they're found), the error-on-control-flow pass in the run initialisation for optimisation level 3 should be dropped to close this issue: https://github.com/Qiskit/qiskit-terra/blob/e981b7e8ba073388edc3544b30d908b731dc6d7d/qiskit/transpiler/preset_passmanagers/level3.py#L177-L179


Discussion:

Resolving patch:
diff --git a/qiskit/transpiler/preset_passmanagers/common.py b/qiskit/transpiler/preset_passmanagers/common.py
--- a/qiskit/transpiler/preset_passmanagers/common.py
+++ b/qiskit/transpiler/preset_passmanagers/common.py
@@ -66,11 +66,9 @@
     "routing_method": _ControlFlowState(
         working={"none", "stochastic", "sabre"}, not_working={"lookahead", "basic"}
     ),
-    # 'synthesis' is not a supported translation method because of the block-collection passes
-    # involved; we currently don't have a neat way to pass the information about nested blocks - the
-    # `UnitarySynthesis` pass itself is control-flow aware.
     "translation_method": _ControlFlowState(
-        working={"translator", "unroller"}, not_working={"synthesis"}
+        working={"translator", "synthesis", "unroller"},
+        not_working=set(),
     ),
     "optimization_method": _ControlFlowState(working=set(), not_working=set()),
     "scheduling_method": _ControlFlowState(working=set(), not_working={"alap", "asap"}),
diff --git a/qiskit/transpiler/preset_passmanagers/level2.py b/qiskit/transpiler/preset_passmanagers/level2.py
--- a/qiskit/transpiler/preset_passmanagers/level2.py
+++ b/qiskit/transpiler/preset_passmanagers/level2.py
@@ -254,8 +254,14 @@ def _unroll_condition(property_set):
         sched = plugin_manager.get_passmanager_stage(
             "scheduling", scheduling_method, pass_manager_config, optimization_level=2
         )
-    init = common.generate_error_on_control_flow(
-        "The optimizations in optimization_level=2 do not yet support control flow."
+    init = common.generate_control_flow_options_check(
+        layout_method=layout_method,
+        routing_method=routing_method,
+        translation_method=translation_method,
+        optimization_method=optimization_method,
+        scheduling_method=scheduling_method,
+        basis_gates=basis_gates,
+        target=target,
     )
     if init_method is not None:
         init += plugin_manager.get_passmanager_stage(
diff --git a/qiskit/transpiler/preset_passmanagers/level3.py b/qiskit/transpiler/preset_passmanagers/level3.py
--- a/qiskit/transpiler/preset_passmanagers/level3.py
+++ b/qiskit/transpiler/preset_passmanagers/level3.py
@@ -189,8 +189,14 @@ def _opt_control(property_set):
     ]
 
     # Build pass manager
-    init = common.generate_error_on_control_flow(
-        "The optimizations in optimization_level=3 do not yet support control flow."
+    init = common.generate_control_flow_options_check(
+        layout_method=layout_method,
+        routing_method=routing_method,
+        translation_method=translation_method,
+        optimization_method=optimization_method,
+        scheduling_method=scheduling_method,
+        basis_gates=basis_gates,
+        target=target,
     )
     if init_method is not None:
         init += plugin_manager.get_passmanager_stage(


Test patch:
diff --git a/test/python/compiler/test_transpiler.py b/test/python/compiler/test_transpiler.py
--- a/test/python/compiler/test_transpiler.py
+++ b/test/python/compiler/test_transpiler.py
@@ -1525,9 +1525,7 @@ def test_target_ideal_gates(self, opt_level):
 
         self.assertEqual(Operator.from_circuit(result), Operator.from_circuit(qc))
 
-    # TODO: Add optimization level 2 and 3 after they support control flow
-    # compilation
-    @data(0, 1)
+    @data(0, 1, 2, 3)
     def test_transpile_with_custom_control_flow_target(self, opt_level):
         """Test transpile() with a target and constrol flow ops."""
         target = FakeMumbaiV2().target
@@ -1759,10 +1757,15 @@ def test_qpy_roundtrip_backendv2(self, optimization_level):
 
         self.assertEqual(round_tripped, transpiled)
 
-    @data(0, 1)
+    @data(0, 1, 2, 3)
     def test_qpy_roundtrip_control_flow(self, optimization_level):
         """Test that the output of a transpiled circuit with control flow can be round-tripped
         through QPY."""
+        if optimization_level == 3 and sys.platform == "win32":
+            self.skipTest(
+                "This test case triggers a bug in the eigensolver routine on windows. "
+                "See #10345 for more details."
+            )
 
         backend = FakeMelbourne()
         transpiled = transpile(
@@ -1781,7 +1784,7 @@ def test_qpy_roundtrip_control_flow(self, optimization_level):
         round_tripped = qpy.load(buffer)[0]
         self.assertEqual(round_tripped, transpiled)
 
-    @data(0, 1)
+    @data(0, 1, 2, 3)
     def test_qpy_roundtrip_control_flow_backendv2(self, optimization_level):
         """Test that the output of a transpiled circuit with control flow can be round-tripped
         through QPY."""
@@ -1818,7 +1821,7 @@ def test_qasm3_output(self, optimization_level):
         # itself doesn't throw an error, though.
         self.assertIsInstance(qasm3.dumps(transpiled).strip(), str)
 
-    @data(0, 1)
+    @data(0, 1, 2, 3)
     def test_qasm3_output_control_flow(self, optimization_level):
         """Test that the output of a transpiled circuit with control flow can be dumped into
         OpenQASM 3."""
diff --git a/test/python/transpiler/test_preset_passmanagers.py b/test/python/transpiler/test_preset_passmanagers.py
--- a/test/python/transpiler/test_preset_passmanagers.py
+++ b/test/python/transpiler/test_preset_passmanagers.py
@@ -1427,7 +1427,7 @@ def get_translation_stage_plugin(self):
 class TestIntegrationControlFlow(QiskitTestCase):
     """Integration tests for control-flow circuits through the preset pass managers."""
 
-    @data(0, 1)
+    @data(0, 1, 2, 3)
     def test_default_compilation(self, optimization_level):
         """Test that a simple circuit with each type of control-flow passes a full transpilation
         pipeline with the defaults."""
@@ -1503,7 +1503,7 @@ def _visit_block(circuit, stack=None):
         # Assert routing ran.
         _visit_block(transpiled)
 
-    @data(0, 1)
+    @data(0, 1, 2, 3)
     def test_allow_overriding_defaults(self, optimization_level):
         """Test that the method options can be overridden."""
         circuit = QuantumCircuit(3, 1)
@@ -1541,7 +1541,7 @@ def callback(pass_, **_):
         self.assertNotIn("SabreLayout", calls)
         self.assertNotIn("BasisTranslator", calls)
 
-    @data(0, 1)
+    @data(0, 1, 2, 3)
     def test_invalid_methods_raise_on_control_flow(self, optimization_level):
         """Test that trying to use an invalid method with control flow fails."""
         qc = QuantumCircuit(1)
@@ -1550,22 +1550,10 @@ def test_invalid_methods_raise_on_control_flow(self, optimization_level):
 
         with self.assertRaisesRegex(TranspilerError, "Got routing_method="):
             transpile(qc, routing_method="lookahead", optimization_level=optimization_level)
-        with self.assertRaisesRegex(TranspilerError, "Got translation_method="):
-            transpile(qc, translation_method="synthesis", optimization_level=optimization_level)
         with self.assertRaisesRegex(TranspilerError, "Got scheduling_method="):
             transpile(qc, scheduling_method="alap", optimization_level=optimization_level)
 
-    @data(2, 3)
-    def test_unsupported_levels_raise(self, optimization_level):
-        """Test that trying to use an invalid method with control flow fails."""
-        qc = QuantumCircuit(1)
-        with qc.for_loop((1,)):
-            qc.x(0)
-
-        with self.assertRaisesRegex(TranspilerError, "The optimizations in optimization_level="):
-            transpile(qc, optimization_level=optimization_level)
-
-    @data(0, 1)
+    @data(0, 1, 2, 3)
     def test_unsupported_basis_gates_raise(self, optimization_level):
         """Test that trying to transpile a control-flow circuit for a backend that doesn't support
         the necessary operations in its `basis_gates` will raise a sensible error."""
@@ -1591,7 +1579,7 @@ def test_unsupported_basis_gates_raise(self, optimization_level):
         with self.assertRaisesRegex(TranspilerError, "The control-flow construct.*not supported"):
             transpile(qc, backend, optimization_level=optimization_level)
 
-    @data(0, 1)
+    @data(0, 1, 2, 3)
     def test_unsupported_targets_raise(self, optimization_level):
         """Test that trying to transpile a control-flow circuit for a backend that doesn't support
         the necessary operations in its `Target` will raise a more sensible error."""