Repository: Qiskit/qiskit

Issue:
Nested flow control for Passmanagers
<!-- ⚠️ If you do not respect this template, your issue will be closed -->
<!-- ⚠️ Make sure to browse the opened and closed issues to confirm this idea does not exist. -->

### What is the expected enhancement?

Skimming through the pass manager code and reading the documentation (which is quite lacking) for building pass managers I do not see an option to nest flow control. The `FlowController` class is only attached to a set of passes which makes it impossible to apply an additional flow controller on a subset of those passes. For example the use case I have in mind is to have a condition in a do while loop, for example something like:

```python
while not fixed_point:
    pass_a()
    pass_b()
    check_pass()
    if not check_pass_result:
        pass_d()
    pass_e()
pass_f()
```

which at least I couldn't find a way to represent in the current pass manager structure. It would be very useful as we make the pass manager more sophisticated to have nested flow control like this. If I just missed it we should improve the documentation around this (which honestly we need regardless).


Discussion:
Do you think this will be a good issue to get familiar with the transpiler code? If yes, I would like to try it.
This seems related to https://github.com/Qiskit/qiskit-terra/pull/5772 . Let me have a look first, it might be more complicated than expected. 

Resolving patch:
diff --git a/qiskit/transpiler/passmanager.py b/qiskit/transpiler/passmanager.py
--- a/qiskit/transpiler/passmanager.py
+++ b/qiskit/transpiler/passmanager.py
@@ -167,8 +167,13 @@ def _normalize_passes(
         if isinstance(passes, BasePass):
             passes = [passes]
         for pass_ in passes:
-            if not isinstance(pass_, BasePass):
-                raise TranspilerError("%s is not a pass instance" % pass_.__class__)
+            if isinstance(pass_, FlowController):
+                # Normalize passes in nested FlowController
+                PassManager._normalize_passes(pass_.passes)
+            elif not isinstance(pass_, BasePass):
+                raise TranspilerError(
+                    "%s is not a BasePass or FlowController instance " % pass_.__class__
+                )
         return passes
 
     def run(
diff --git a/qiskit/transpiler/runningpassmanager.py b/qiskit/transpiler/runningpassmanager.py
--- a/qiskit/transpiler/runningpassmanager.py
+++ b/qiskit/transpiler/runningpassmanager.py
@@ -20,6 +20,7 @@
 
 from qiskit.dagcircuit import DAGCircuit
 from qiskit.converters import circuit_to_dag, dag_to_circuit
+from qiskit.transpiler.basepasses import BasePass
 from .propertyset import PropertySet
 from .fencedobjs import FencedPropertySet, FencedDAGCircuit
 from .exceptions import TranspilerError
@@ -132,10 +133,10 @@ def run(self, circuit, output_name=None, callback=None):
         return circuit
 
     def _do_pass(self, pass_, dag, options):
-        """Do a pass and its "requires".
+        """Do either a pass and its "requires" or FlowController.
 
         Args:
-            pass_ (BasePass): Pass to do.
+            pass_ (BasePass or FlowController): Pass to do.
             dag (DAGCircuit): The dag on which the pass is ran.
             options (dict): PassManager options.
         Returns:
@@ -144,18 +145,35 @@ def _do_pass(self, pass_, dag, options):
         Raises:
             TranspilerError: If the pass is not a proper pass instance.
         """
+        if isinstance(pass_, BasePass):
+            # First, do the requires of pass_
+            for required_pass in pass_.requires:
+                dag = self._do_pass(required_pass, dag, options)
 
-        # First, do the requires of pass_
-        for required_pass in pass_.requires:
-            dag = self._do_pass(required_pass, dag, options)
+            # Run the pass itself, if not already run
+            if pass_ not in self.valid_passes:
+                dag = self._run_this_pass(pass_, dag)
 
-        # Run the pass itself, if not already run
-        if pass_ not in self.valid_passes:
-            dag = self._run_this_pass(pass_, dag)
+                # update the valid_passes property
+                self._update_valid_passes(pass_)
 
-            # update the valid_passes property
-            self._update_valid_passes(pass_)
+        # if provided a nested flow controller
+        elif isinstance(pass_, FlowController):
 
+            if isinstance(pass_, ConditionalController) and not isinstance(
+                pass_.condition, partial
+            ):
+                pass_.condition = partial(pass_.condition, self.fenced_property_set)
+
+            elif isinstance(pass_, DoWhileController) and not isinstance(pass_.do_while, partial):
+                pass_.do_while = partial(pass_.do_while, self.fenced_property_set)
+
+            for _pass in pass_:
+                self._do_pass(_pass, dag, pass_.options)
+        else:
+            raise TranspilerError(
+                "Expecting type BasePass or FlowController, got %s." % type(pass_)
+            )
         return dag
 
     def _run_this_pass(self, pass_, dag):


Test patch:
diff --git a/test/python/transpiler/test_pass_scheduler.py b/test/python/transpiler/test_pass_scheduler.py
--- a/test/python/transpiler/test_pass_scheduler.py
+++ b/test/python/transpiler/test_pass_scheduler.py
@@ -550,6 +550,61 @@ def test_fresh_initial_state(self):
             ],
         )
 
+    def test_nested_conditional_in_loop(self):
+        """Run a loop with a nested conditional."""
+        nested_conditional = [
+            ConditionalController(
+                [PassA_TP_NR_NP()], condition=lambda property_set: property_set["property"] >= 5
+            )
+        ]
+        self.passmanager.append(
+            [PassK_check_fixed_point_property()]
+            + nested_conditional
+            + [PassF_reduce_dag_property()],
+            do_while=lambda property_set: not property_set["property_fixed_point"],
+        )
+        expected = [
+            "run analysis pass PassG_calculates_dag_property",
+            "set property as 8 (from dag.property)",
+            "run analysis pass PassK_check_fixed_point_property",
+            "run transformation pass PassA_TP_NR_NP",
+            "run transformation pass PassF_reduce_dag_property",
+            "dag property = 6",
+            "run analysis pass PassG_calculates_dag_property",
+            "set property as 6 (from dag.property)",
+            "run analysis pass PassK_check_fixed_point_property",
+            "run transformation pass PassA_TP_NR_NP",
+            "run transformation pass PassF_reduce_dag_property",
+            "dag property = 5",
+            "run analysis pass PassG_calculates_dag_property",
+            "set property as 5 (from dag.property)",
+            "run analysis pass PassK_check_fixed_point_property",
+            "run transformation pass PassA_TP_NR_NP",
+            "run transformation pass PassF_reduce_dag_property",
+            "dag property = 4",
+            "run analysis pass PassG_calculates_dag_property",
+            "set property as 4 (from dag.property)",
+            "run analysis pass PassK_check_fixed_point_property",
+            "run transformation pass PassF_reduce_dag_property",
+            "dag property = 3",
+            "run analysis pass PassG_calculates_dag_property",
+            "set property as 3 (from dag.property)",
+            "run analysis pass PassK_check_fixed_point_property",
+            "run transformation pass PassF_reduce_dag_property",
+            "dag property = 2",
+            "run analysis pass PassG_calculates_dag_property",
+            "set property as 2 (from dag.property)",
+            "run analysis pass PassK_check_fixed_point_property",
+            "run transformation pass PassF_reduce_dag_property",
+            "dag property = 2",
+            "run analysis pass PassG_calculates_dag_property",
+            "set property as 2 (from dag.property)",
+            "run analysis pass PassK_check_fixed_point_property",
+            "run transformation pass PassF_reduce_dag_property",
+            "dag property = 2",
+        ]
+        self.assertScheduler(self.circuit, self.passmanager, expected)
+
 
 class DoXTimesController(FlowController):
     """A control-flow plugin for running a set of passes an X amount of times."""