Repository: Qiskit/qiskit

Issue:
Schedule Call inlining has a bug
<!-- ⚠️ If you do not respect this template, your issue will be closed -->
<!-- ⚠️ Make sure to browse the opened and closed issues -->

### Information

- **Qiskit Terra version**: commit 3c979ebdacecbbd213757a8149bddc309bfa58c0
- **Python version**: 3.8.5
- **Operating system**: Ubuntu 20.04

### What is the current behavior?

Inlining the call instructions of a schedule does not remove all call instructions.

### Steps to reproduce the problem

The following code should reproduce this behavior:
```python
from qiskit.pulse.transforms import inline_subroutines
from qiskit.circuit import Parameter
import qiskit.pulse as pulse
from qiskit.pulse import Drag, DriveChannel, Gaussian


amp = Parameter("amp")
d0_ = DriveChannel(Parameter('ch0'))
sigma = Parameter("σ")

with pulse.build(name="xp") as xp:
    pulse.play(Gaussian(160, amp, sigma), d0_)

with pulse.build(name="xp2") as xp2:
    with pulse.align_sequential():
        pulse.call(xp)
        
print(xp2)
print(inline_subroutines(xp2))
```

The output is
```
ScheduleBlock(ScheduleBlock(Call(ScheduleBlock(Play(Gaussian(duration=160, amp=amp, sigma=σ), DriveChannel(ch0)), name="xp", transform=AlignLeft()), name='call_xp'), name="block2", transform=AlignSequential()), name="xp2", transform=AlignLeft())
ScheduleBlock(ScheduleBlock(Call(ScheduleBlock(Play(Gaussian(duration=160, amp=amp, sigma=σ), DriveChannel(ch0)), name="xp", transform=AlignLeft()), name='call_xp'), name="block2", transform=AlignSequential()), name="xp2", transform=AlignLeft())
```

### What is the expected behavior?

All call instructions should have been removed by the inline.

### Suggested solutions

There might be an issue with the inline function.



Discussion:

Resolving patch:
diff --git a/qiskit/pulse/schedule.py b/qiskit/pulse/schedule.py
--- a/qiskit/pulse/schedule.py
+++ b/qiskit/pulse/schedule.py
@@ -202,9 +202,32 @@ def channels(self) -> Tuple[Channel]:
 
     @property
     def _children(self) -> Tuple[Tuple[int, ScheduleComponent], ...]:
-        """Return the child``NamedValues``s of this ``Schedule`` in the
+        """Deprecated. Return the child schedule components of this ``Schedule`` in the
         order they were added to the schedule.
 
+        Notes:
+            Nested schedules are returned as-is. If you want to collect only instructions,
+            use py:meth:`~Schedule.instructions` instead.
+
+        Returns:
+            A tuple, where each element is a two-tuple containing the initial
+            scheduled time of each ``NamedValue`` and the component
+            itself.
+        """
+        warnings.warn('Schedule._children is now available as the public property '
+                      'Schedule.children. Access to this private property is being deprecated.',
+                      DeprecationWarning)
+        return tuple(self.__children)
+
+    @property
+    def children(self) -> Tuple[Tuple[int, ScheduleComponent], ...]:
+        """Return the child schedule components of this ``Schedule`` in the
+        order they were added to the schedule.
+
+        Notes:
+            Nested schedules are returned as-is. If you want to collect only instructions,
+            use py:meth:`~Schedule.instructions` instead.
+
         Returns:
             A tuple, where each element is a two-tuple containing the initial
             scheduled time of each ``NamedValue`` and the component
@@ -272,7 +295,7 @@ def _instructions(self, time: int = 0):
                 :class:`~qiskit.pulse.Instruction`
                 starts at and the flattened :class:`~qiskit.pulse.Instruction` s.
         """
-        for insert_time, child_sched in self._children:
+        for insert_time, child_sched in self.children:
             yield from child_sched._instructions(time + insert_time)
 
     def shift(self,
@@ -330,8 +353,7 @@ def _mutable_shift(self,
 
         self._duration = self._duration + time
         self._timeslots = timeslots
-        self.__children = [(orig_time + time, child) for
-                           orig_time, child in self._children]
+        self.__children = [(orig_time + time, child) for orig_time, child in self.children]
         return self
 
     def insert(self,
@@ -651,7 +673,7 @@ def replace(self,
         new_children = []
         new_parameters = ParameterManager()
 
-        for time, child in self._children:
+        for time, child in self.children:
             if child == old:
                 new_children.append((time, new))
                 new_parameters.update_parameter_table(new)
diff --git a/qiskit/pulse/transforms/alignments.py b/qiskit/pulse/transforms/alignments.py
--- a/qiskit/pulse/transforms/alignments.py
+++ b/qiskit/pulse/transforms/alignments.py
@@ -81,7 +81,7 @@ def align(self, schedule: Schedule) -> Schedule:
             Schedule with reallocated instructions.
         """
         aligned = Schedule()
-        for _, child in schedule._children:
+        for _, child in schedule.children:
             self._push_left_append(aligned, child)
 
         return aligned
@@ -139,7 +139,7 @@ def align(self, schedule: Schedule) -> Schedule:
             Schedule with reallocated instructions.
         """
         aligned = Schedule()
-        for _, child in reversed(schedule._children):
+        for _, child in reversed(schedule.children):
             aligned = self._push_right_prepend(aligned, child)
 
         return aligned
@@ -200,7 +200,7 @@ def align(self, schedule: Schedule) -> Schedule:
             Schedule with reallocated instructions.
         """
         aligned = Schedule()
-        for _, child in schedule._children:
+        for _, child in schedule.children:
             aligned.insert(aligned.duration, child, inplace=True)
 
         return aligned
@@ -247,17 +247,17 @@ def align(self, schedule: Schedule) -> Schedule:
         """
         instruction_duration_validation(self.duration)
 
-        total_duration = sum([child.duration for _, child in schedule._children])
+        total_duration = sum([child.duration for _, child in schedule.children])
         if self.duration < total_duration:
             return schedule
 
         total_delay = self.duration - total_duration
 
-        if len(schedule._children) > 1:
+        if len(schedule.children) > 1:
             # Calculate the interval in between sub-schedules.
             # If the duration cannot be divided by the number of sub-schedules,
             # the modulo is appended and prepended to the input schedule.
-            interval, mod = np.divmod(total_delay, len(schedule._children) - 1)
+            interval, mod = np.divmod(total_delay, len(schedule.children) - 1)
         else:
             interval = 0
             mod = total_delay
@@ -268,7 +268,7 @@ def align(self, schedule: Schedule) -> Schedule:
         aligned = Schedule()
         # Insert sub-schedules with interval
         _t0 = int(aligned.stop_time + delay + mod)
-        for _, child in schedule._children:
+        for _, child in schedule.children:
             aligned.insert(_t0, child, inplace=True)
             _t0 = int(aligned.stop_time + interval)
 
@@ -338,7 +338,7 @@ def align(self, schedule: Schedule) -> Schedule:
             return schedule
 
         aligned = Schedule()
-        for ind, (_, child) in enumerate(schedule._children):
+        for ind, (_, child) in enumerate(schedule.children):
             _t_center = self.duration * self._func(ind + 1)
             _t0 = int(_t_center - 0.5 * child.duration)
             if _t0 < 0 or _t0 > self.duration:
diff --git a/qiskit/pulse/transforms/canonicalization.py b/qiskit/pulse/transforms/canonicalization.py
--- a/qiskit/pulse/transforms/canonicalization.py
+++ b/qiskit/pulse/transforms/canonicalization.py
@@ -157,7 +157,9 @@ def _inline_schedule(schedule: Schedule) -> Schedule:
     """
     ret_schedule = Schedule(name=schedule.name,
                             metadata=schedule.metadata)
-    for t0, inst in schedule.instructions:
+    for t0, inst in schedule.children:
+        # note that schedule.instructions unintentionally flatten the nested schedule.
+        # this should be performed by another transformer node.
         if isinstance(inst, instructions.Call):
             # bind parameter
             subroutine = inst.assigned_subroutine()
@@ -167,6 +169,10 @@ def _inline_schedule(schedule: Schedule) -> Schedule:
             # recursively inline the program
             inline_schedule = _inline_schedule(subroutine)
             ret_schedule.insert(t0, inline_schedule, inplace=True)
+        elif isinstance(inst, Schedule):
+            # recursively inline the program
+            inline_schedule = _inline_schedule(inst)
+            ret_schedule.insert(t0, inline_schedule, inplace=True)
         else:
             ret_schedule.insert(t0, inst, inplace=True)
     return ret_schedule
@@ -191,6 +197,10 @@ def _inline_block(block: ScheduleBlock) -> ScheduleBlock:
             # recursively inline the program
             inline_block = _inline_block(subroutine)
             ret_block.append(inline_block, inplace=True)
+        elif isinstance(inst, ScheduleBlock):
+            # recursively inline the program
+            inline_block = _inline_block(inst)
+            ret_block.append(inline_block, inplace=True)
         else:
             ret_block.append(inst, inplace=True)
     return ret_block


Test patch:
diff --git a/test/python/pulse/test_parameter_manager.py b/test/python/pulse/test_parameter_manager.py
--- a/test/python/pulse/test_parameter_manager.py
+++ b/test/python/pulse/test_parameter_manager.py
@@ -249,7 +249,7 @@ def test_set_parameter_to_alignment_context(self):
 
         self.assertEqual(assigned, ref_obj)
 
-    def test_nested_assigment_partial_bind(self):
+    def test_nested_assignment_partial_bind(self):
         """Test nested schedule with call instruction.
         Inline the schedule and partially bind parameters."""
         context = AlignEquispaced(duration=self.context_dur)
@@ -271,14 +271,14 @@ def test_nested_assigment_partial_bind(self):
 
         ref_context = AlignEquispaced(duration=1000)
         ref_subroutine = pulse.ScheduleBlock(alignment_context=ref_context)
-        ref_subroutine += pulse.Play(pulse.Gaussian(200, self.amp1_1 + self.amp1_2, 25),
+        ref_subroutine += pulse.Play(pulse.Gaussian(200, self.amp1_1 + self.amp1_2, 50),
                                      pulse.DriveChannel(1))
 
         ref_nested_block = pulse.ScheduleBlock()
         ref_nested_block += ref_subroutine
 
         ref_obj = pulse.ScheduleBlock()
-        ref_obj += nested_block
+        ref_obj += ref_nested_block
 
         self.assertEqual(assigned, ref_obj)
 
diff --git a/test/python/pulse/test_schedule.py b/test/python/pulse/test_schedule.py
--- a/test/python/pulse/test_schedule.py
+++ b/test/python/pulse/test_schedule.py
@@ -191,7 +191,7 @@ def test_empty_schedule(self):
         self.assertEqual(0, sched.stop_time)
         self.assertEqual(0, sched.duration)
         self.assertEqual(0, len(sched))
-        self.assertEqual((), sched._children)
+        self.assertEqual((), sched.children)
         self.assertEqual({}, sched.timeslots)
         self.assertEqual([], list(sched.instructions))
         self.assertFalse(sched)
diff --git a/test/python/pulse/test_transforms.py b/test/python/pulse/test_transforms.py
--- a/test/python/pulse/test_transforms.py
+++ b/test/python/pulse/test_transforms.py
@@ -885,6 +885,54 @@ def test_remove_subroutines(self):
 
         self.assertEqual(target, reference)
 
+    def test_call_in_nested_schedule(self):
+        """Test that subroutines in nested schedule."""
+        d0 = pulse.DriveChannel(0)
+
+        subroutine = pulse.Schedule()
+        subroutine.insert(10, pulse.Delay(10, d0), inplace=True)
+
+        nested_sched = pulse.Schedule()
+        nested_sched.insert(0, pulse.instructions.Call(subroutine), inplace=True)
+
+        main_sched = pulse.Schedule()
+        main_sched.insert(0, nested_sched, inplace=True)
+
+        target = transforms.inline_subroutines(main_sched)
+
+        # no call instruction
+        reference_nested = pulse.Schedule()
+        reference_nested.insert(0, subroutine, inplace=True)
+
+        reference = pulse.Schedule()
+        reference.insert(0, reference_nested, inplace=True)
+
+        self.assertEqual(target, reference)
+
+    def test_call_in_nested_block(self):
+        """Test that subroutines in nested schedule."""
+        d0 = pulse.DriveChannel(0)
+
+        subroutine = pulse.ScheduleBlock()
+        subroutine.append(pulse.Delay(10, d0), inplace=True)
+
+        nested_block = pulse.ScheduleBlock()
+        nested_block.append(pulse.instructions.Call(subroutine), inplace=True)
+
+        main_block = pulse.ScheduleBlock()
+        main_block.append(nested_block, inplace=True)
+
+        target = transforms.inline_subroutines(main_block)
+
+        # no call instruction
+        reference_nested = pulse.ScheduleBlock()
+        reference_nested.append(subroutine, inplace=True)
+
+        reference = pulse.ScheduleBlock()
+        reference.append(reference_nested, inplace=True)
+
+        self.assertEqual(target, reference)
+
 
 if __name__ == '__main__':
     unittest.main()