Repository: Qiskit/qiskit

Issue:
Add a pad method to pulse schedule
<!-- ⚠️ 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?
With the introduction of a delay instruction (#2846) we should add a method `pad` (or another name) which will take a pulse schedule and return a new schedule with delays inserted in all available timeslots. This will allow one to fix the behaviour of a given schedule by preventing additional pulses to be inserted and allow easier scheduling.




Discussion:

Resolving patch:
diff --git a/qiskit/pulse/commands/instruction.py b/qiskit/pulse/commands/instruction.py
--- a/qiskit/pulse/commands/instruction.py
+++ b/qiskit/pulse/commands/instruction.py
@@ -252,4 +252,4 @@ def __lshift__(self, time: int) -> 'Schedule':
         return self.shift(time)
 
     def __repr__(self):
-        return "%s" % (self._command)
+        return "Instruction(%s, %s)" % (self._command, ', '.join(str(ch) for ch in self.channels))
diff --git a/qiskit/pulse/timeslots.py b/qiskit/pulse/timeslots.py
--- a/qiskit/pulse/timeslots.py
+++ b/qiskit/pulse/timeslots.py
@@ -17,7 +17,7 @@
 """
 from collections import defaultdict
 import itertools
-from typing import Tuple, Union
+from typing import Tuple, Union, Optional
 
 from .channels import Channel
 from .exceptions import PulseError
@@ -96,6 +96,30 @@ def __eq__(self, other):
         """
         return self.start == other.start and self.stop == other.stop
 
+    def stops_before(self, other):
+        """Whether intervals stops at value less than or equal to the
+        other interval's starting time.
+
+        Args:
+            other (Interval): other Interval
+
+        Returns:
+            bool: are self and other equal.
+        """
+        return self.stop <= other.start
+
+    def starts_after(self, other):
+        """Whether intervals starts at value greater than or equal to the
+        other interval's stopping time.
+
+        Args:
+            other (Interval): other Interval
+
+        Returns:
+            bool: are self and other equal.
+        """
+        return self.start >= other.stop
+
     def __repr__(self):
         """Return a readable representation of Interval Object"""
         return "{}({}, {})".format(self.__class__.__name__, self.start, self.stop)
@@ -349,6 +373,35 @@ def shift(self, time: int) -> 'TimeslotCollection':
         slots = [slot.shift(time) for slot in self.timeslots]
         return TimeslotCollection(*slots)
 
+    def complement(self, stop_time: Optional[int] = None) -> 'TimeslotCollection':
+        """Return a complement TimeSlotCollection containing all unoccupied Timeslots
+        within this TimeSlotCollection.
+
+
+        Args:
+            stop_time: Final time too which complement Timeslot's will be returned.
+                If not set, defaults to last time in this TimeSlotCollection
+        """
+
+        timeslots = []
+
+        stop_time = stop_time or self.stop_time
+
+        for channel in self.channels:
+            curr_time = 0
+            for timeslot in self.ch_timeslots(channel):
+                next_time = timeslot.interval.start
+                if next_time-curr_time > 0:
+                    timeslots.append(Timeslot(Interval(curr_time, next_time), channel))
+
+                curr_time = timeslot.interval.stop
+
+            # pad out channel to stop_time
+            if stop_time-curr_time > 0:
+                timeslots.append(Timeslot(Interval(curr_time, stop_time), channel))
+
+        return TimeslotCollection(*timeslots)
+
     def __eq__(self, other) -> bool:
         """Two time-slot collections are the same if they have the same time-slots.
 
@@ -357,6 +410,7 @@ def __eq__(self, other) -> bool:
         """
         if set(self.channels) != set(other.channels):
             return False
+
         for channel in self.channels:
             if self.ch_timeslots(channel) != self.ch_timeslots(channel):
                 return False
diff --git a/qiskit/pulse/utils.py b/qiskit/pulse/utils.py
--- a/qiskit/pulse/utils.py
+++ b/qiskit/pulse/utils.py
@@ -17,20 +17,20 @@
 """
 import warnings
 
-from typing import List, Optional
+from typing import List, Optional, Iterable
 
 import numpy as np
 
-from .channels import AcquireChannel, MemorySlot
+from .channels import Channel, AcquireChannel, MemorySlot
 from .cmd_def import CmdDef
-from .commands import Acquire, AcquireInstruction
+from .commands import Acquire, AcquireInstruction, Delay
 from .exceptions import PulseError
 from .interfaces import ScheduleComponent
 from .schedule import Schedule
 
 
-def align_measures(schedules: List[ScheduleComponent], cmd_def: CmdDef, cal_gate: str = 'u3',
-                   max_calibration_duration: Optional[int] = None,
+def align_measures(schedules: Iterable[ScheduleComponent], cmd_def: CmdDef,
+                   cal_gate: str = 'u3', max_calibration_duration: Optional[int] = None,
                    align_time: Optional[int] = None) -> Schedule:
     """Return new schedules where measurements occur at the same physical time. Minimum measurement
     wait time (to allow for calibration pulses) is enforced.
@@ -131,3 +131,36 @@ def add_implicit_acquires(schedule: ScheduleComponent, meas_map: List[List[int]]
             new_schedule |= inst << time
 
     return new_schedule
+
+
+def pad(schedule: Schedule, channels: Optional[Iterable[Channel]] = None,
+        until: Optional[int] = None) -> Schedule:
+    """Pad the input Schedule with `Delay`s on all unoccupied timeslots until
+    `schedule.duration` or `until` if not `None`.
+
+    Args:
+        schedule: Schedule to pad.
+        channels: Channels to pad. Defaults to all channels in
+            `schedule` if not provided. If the supplied channel is not a member
+            of `schedule` it will be added.
+        until: Time to pad until. Defaults to `schedule.duration` if not provided.
+    Returns:
+        Schedule: The padded schedule
+    """
+    until = until or schedule.duration
+
+    channels = channels or schedule.channels
+    occupied_channels = schedule.channels
+
+    unoccupied_channels = set(channels) - set(occupied_channels)
+
+    empty_timeslot_collection = schedule.timeslots.complement(until)
+
+    for channel in channels:
+        for timeslot in empty_timeslot_collection.ch_timeslots(channel):
+            schedule |= Delay(timeslot.duration)(timeslot.channel).shift(timeslot.start)
+
+    for channel in unoccupied_channels:
+        schedule |= Delay(until)(channel)
+
+    return schedule


Test patch:
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
@@ -447,7 +447,7 @@ def test_delay_snapshot_channel(self):
 
 
 class TestScheduleFilter(BaseTestSchedule):
-    """Test filtering of schedules."""
+    """Test Schedule filtering methods."""
 
     def test_filter_channels(self):
         """Test filtering over channels."""
@@ -560,41 +560,8 @@ def test_filter(self):
         self.assertEqual(len(sched._filter([lambda x: False]).instructions), 0)
         self.assertEqual(len(sched._filter([lambda x: x[0] < 30]).instructions), 2)
 
-        def my_test_par_sched_one(x, y, z):
-            result = PulseInstruction(
-                SamplePulse(np.array([x, y, z]), name='sample'),
-                device.drives[0]
-            )
-            return 0, result
-
-        def my_test_par_sched_two(x, y, z):
-            result = PulseInstruction(
-                SamplePulse(np.array([x, y, z]), name='sample'),
-                device.drives[0]
-            )
-            return 5, result
-
-        par_sched_in_0 = ParameterizedSchedule(
-            my_test_par_sched_one, parameters={'x': 0, 'y': 1, 'z': 2}
-        )
-        par_sched_in_1 = ParameterizedSchedule(
-            my_test_par_sched_two, parameters={'x': 0, 'y': 1, 'z': 2}
-        )
-        par_sched = ParameterizedSchedule(par_sched_in_0, par_sched_in_1)
-
-        cmd_def = CmdDef()
-        cmd_def.add('test', 0, par_sched)
-
-        actual = cmd_def.get('test', 0, 0.01, 0.02, 0.03)
-        expected = par_sched_in_0.bind_parameters(0.01, 0.02, 0.03) |\
-            par_sched_in_1.bind_parameters(0.01, 0.02, 0.03)
-        self.assertEqual(actual.start_time, expected.start_time)
-        self.assertEqual(actual.stop_time, expected.stop_time)
-
-        self.assertEqual(cmd_def.get_parameters('test', 0), ('x', 'y', 'z'))
-
 
-class TestScheduleEquality(QiskitTestCase):
+class TestScheduleEquality(BaseTestSchedule):
     """Test equality of schedules."""
 
     def test_different_channels(self):
diff --git a/test/python/pulse/test_timeslots.py b/test/python/pulse/test_timeslots.py
--- a/test/python/pulse/test_timeslots.py
+++ b/test/python/pulse/test_timeslots.py
@@ -225,6 +225,37 @@ def test_zero_duration_timeslot(self):
             TimeslotCollection(Timeslot(Interval(0, 10), AcquireChannel(0)),
                                Timeslot(Interval(5, 5), AcquireChannel(0)))
 
+    def test_complement(self):
+        """Test complement of TimeslotCollection is properly created."""
+        test_collection = TimeslotCollection(
+            Timeslot(Interval(5, 7), DriveChannel(0)),
+            Timeslot(Interval(10, 12), DriveChannel(0)),
+            Timeslot(Interval(0, 3), DriveChannel(1)))
+        ref_complement = TimeslotCollection(
+            Timeslot(Interval(0, 5), DriveChannel(0)),
+            Timeslot(Interval(7, 10), DriveChannel(0)),
+            Timeslot(Interval(3, 12), DriveChannel(1)))
+        complement_collection = test_collection.complement()
+
+        self.assertEqual(ref_complement, complement_collection)
+
+    def test_complement_set_stop_time(self):
+        """Test complement of TimeslotCollection obeys stop_time."""
+        stop_time = 20
+
+        test_collection = TimeslotCollection(
+            Timeslot(Interval(5, 7), DriveChannel(0)),
+            Timeslot(Interval(10, 12), DriveChannel(0)),
+            Timeslot(Interval(0, 3), DriveChannel(1)))
+        ref_complement = TimeslotCollection(
+            Timeslot(Interval(0, 5), DriveChannel(0)),
+            Timeslot(Interval(7, 10), DriveChannel(0)),
+            Timeslot(Interval(12, stop_time), DriveChannel(0)),
+            Timeslot(Interval(3, stop_time), DriveChannel(1)))
+        complement_collection = test_collection.complement(stop_time=stop_time)
+
+        self.assertEqual(ref_complement, complement_collection)
+
 
 if __name__ == '__main__':
     unittest.main()
diff --git a/test/python/pulse/test_utils.py b/test/python/pulse/test_utils.py
--- a/test/python/pulse/test_utils.py
+++ b/test/python/pulse/test_utils.py
@@ -24,7 +24,7 @@
 from qiskit.test import QiskitTestCase
 from qiskit.test.mock import FakeOpenPulse2Q
 
-from qiskit.pulse.utils import add_implicit_acquires, align_measures
+from qiskit.pulse.utils import add_implicit_acquires, align_measures, pad
 
 
 class TestAutoMerge(QiskitTestCase):
@@ -158,5 +158,94 @@ def test_dont_add_all(self):
         self.assertEqual(acquired_qubits, {0, 1, 2, 3})
 
 
+class TestPad(QiskitTestCase):
+    """Test padding of schedule with delays."""
+
+    def test_padding_empty_schedule(self):
+        """Test padding of empty schedule."""
+        self.assertEqual(pulse.Schedule(), pad(pulse.Schedule()))
+
+    def test_padding_schedule(self):
+        """Test padding schedule."""
+        delay = pulse.Delay(10)
+        double_delay = pulse.Delay(20)
+
+        sched = (delay(pulse.DriveChannel(0)).shift(10) +
+                 delay(pulse.DriveChannel(0)).shift(10) +
+                 delay(pulse.DriveChannel(1)).shift(10))
+
+        ref_sched = (sched |
+                     delay(pulse.DriveChannel(0)) |
+                     delay(pulse.DriveChannel(0)).shift(20) |
+                     delay(pulse.DriveChannel(1)) |
+                     double_delay(pulse.DriveChannel(1)).shift(20))
+
+        self.assertEqual(pad(sched), ref_sched)
+
+    def test_padding_schedule_inverse_order(self):
+        """Test padding schedule is insensitive to order in which commands were added.
+
+        This test is the same as `test_adding_schedule` but the order by channel
+        in which commands were added to the schedule to be padded has been reversed.
+        """
+        delay = pulse.Delay(10)
+        double_delay = pulse.Delay(20)
+
+        sched = (delay(pulse.DriveChannel(1)).shift(10) +
+                 delay(pulse.DriveChannel(0)).shift(10) +
+                 delay(pulse.DriveChannel(0)).shift(10))
+
+        ref_sched = (sched |
+                     delay(pulse.DriveChannel(0)) |
+                     delay(pulse.DriveChannel(0)).shift(20) |
+                     delay(pulse.DriveChannel(1)) |
+                     double_delay(pulse.DriveChannel(1)).shift(20))
+
+        self.assertEqual(pad(sched), ref_sched)
+
+    def test_padding_until_less(self):
+        """Test padding until time that is less than schedule duration."""
+        delay = pulse.Delay(10)
+
+        sched = (delay(pulse.DriveChannel(0)).shift(10) +
+                 delay(pulse.DriveChannel(1)))
+
+        ref_sched = (sched |
+                     delay(pulse.DriveChannel(0)) |
+                     pulse.Delay(5)(pulse.DriveChannel(1)).shift(10))
+
+        self.assertEqual(pad(sched, until=15), ref_sched)
+
+    def test_padding_until_greater(self):
+        """Test padding until time that is greater than schedule duration."""
+        delay = pulse.Delay(10)
+
+        sched = (delay(pulse.DriveChannel(0)).shift(10) +
+                 delay(pulse.DriveChannel(1)))
+
+        ref_sched = (sched |
+                     delay(pulse.DriveChannel(0)) |
+                     pulse.Delay(30)(pulse.DriveChannel(0)).shift(20) |
+                     pulse.Delay(40)(pulse.DriveChannel(1)).shift(10))
+
+        self.assertEqual(pad(sched, until=50), ref_sched)
+
+    def test_padding_supplied_channels(self):
+        """Test padding of only specified channels."""
+        delay = pulse.Delay(10)
+        double_delay = pulse.Delay(20)
+
+        sched = (delay(pulse.DriveChannel(0)).shift(10) +
+                 delay(pulse.DriveChannel(1)))
+
+        ref_sched = (sched |
+                     delay(pulse.DriveChannel(0)) |
+                     double_delay(pulse.DriveChannel(2)))
+
+        channels = [pulse.DriveChannel(0), pulse.DriveChannel(2)]
+
+        self.assertEqual(pad(sched, channels=channels), ref_sched)
+
+
 if __name__ == '__main__':
     unittest.main()