Repository: Qiskit/qiskit

Issue:
RecursionError in Schedule.instructions (flatten)
<!-- ⚠️ If you do not respect this template, your issue will be closed -->
<!-- ⚠️ Make sure to browse the opened and closed issues -->

### What is the current behavior?
RecursionError raises when trying to get instructions in a schedule with 1000 instructions.
```
RecursionError: maximum recursion depth exceeded
```

### Steps to reproduce the problem
```
n_instrs_by_channel, n_channels = 100, 10
channels = [pulse.DriveChannel(i) for i in range(n_channels)]
instrs = []
for i in range(n_instrs_by_channel):
    command = pulse.SamplePulse(np.random.random(10))
    for chan in channels:
        instrs.append(command(chan))
schedule = pulse.Schedule()
for instr in instrs:
    schedule += instr
_ = schedule.instructions
```

### Suggested solutions
This is caused by a rapid growth of the depth of schedule tree. It can be improved by modifying the implementation of `union` not to increase the tree depth.


RecursionError in Schedule.instructions (flatten)
<!-- ⚠️ If you do not respect this template, your issue will be closed -->
<!-- ⚠️ Make sure to browse the opened and closed issues -->

### What is the current behavior?
RecursionError raises when trying to get instructions in a schedule with 1000 instructions.
```
RecursionError: maximum recursion depth exceeded
```

### Steps to reproduce the problem
```
n_instrs_by_channel, n_channels = 100, 10
channels = [pulse.DriveChannel(i) for i in range(n_channels)]
instrs = []
for i in range(n_instrs_by_channel):
    command = pulse.SamplePulse(np.random.random(10))
    for chan in channels:
        instrs.append(command(chan))
schedule = pulse.Schedule()
for instr in instrs:
    schedule += instr
_ = schedule.instructions
```

### Suggested solutions
This is caused by a rapid growth of the depth of schedule tree. It can be improved by modifying the implementation of `union` not to increase the tree depth.




Discussion:
We will address this by implementing a flat representation of a schedule in which each schedule only tracks a set of time-ordered instructions, removing the tree-based schedule description. When two schedules are combined their instructions will be copied into a new schedule. I realize this is what you initially proposed 😄.
We will address this by implementing a flat representation of a schedule in which each schedule only tracks a set of time-ordered instructions, removing the tree-based schedule description. When two schedules are combined their instructions will be copied into a new schedule. I realize this is what you initially proposed 😄.

Resolving patch:
diff --git a/qiskit/pulse/schedule.py b/qiskit/pulse/schedule.py
--- a/qiskit/pulse/schedule.py
+++ b/qiskit/pulse/schedule.py
@@ -147,9 +147,9 @@ def _instructions(self, time: int = 0) -> Iterable[Tuple[int, 'Instruction']]:
         for insert_time, child_sched in self._children:
             yield from child_sched._instructions(time + insert_time)
 
-    def union(self, *schedules: List[ScheduleComponent],
+    def union(self, *schedules: Union[ScheduleComponent, Tuple[int, ScheduleComponent]],
               name: Optional[str] = None) -> 'Schedule':
-        """Return a new schedule which is the union of `self` and `schedule`.
+        """Return a new schedule which is the union of both `self` and `schedules`.
 
         Args:
             *schedules: Schedules to be take the union with this `Schedule`.
@@ -157,7 +157,32 @@ def union(self, *schedules: List[ScheduleComponent],
         """
         if name is None:
             name = self.name
-        return Schedule(self, *schedules, name=name)
+        new_sched = Schedule(name=name)
+        new_sched._union((0, self))
+        for sched_pair in schedules:
+            if not isinstance(sched_pair, tuple):
+                sched_pair = (0, sched_pair)
+            new_sched._union(sched_pair)
+        return new_sched
+
+    def _union(self, other: Tuple[int, ScheduleComponent]) -> 'Schedule':
+        """Mutably union `self` and `other` Schedule with shift time.
+
+        Args:
+            other: Schedule with shift time to be take the union with this `Schedule`.
+        """
+        shift_time, sched = other
+        if isinstance(sched, Schedule):
+            shifted_children = sched._children
+            if shift_time != 0:
+                shifted_children = tuple((t + shift_time, child) for t, child in shifted_children)
+            self.__children += shifted_children
+        else:  # isinstance(sched, Instruction)
+            self.__children += (other,)
+
+        sched_timeslots = sched.timeslots if shift_time == 0 else sched.timeslots.shift(shift_time)
+        self._timeslots = self.timeslots.merge(sched_timeslots)
+        self._buffer = max(self.buffer, sched.buffer)
 
     def shift(self, time: int, name: Optional[str] = None) -> 'Schedule':
         """Return a new schedule shifted forward by `time`.


Test patch: