Repository: Qiskit/qiskit

Issue:
Bug in Timeslot Ordering
<!-- ⚠️ 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**:
- **Python version**:
- **Operating system**:

### What is the current behavior?
A bug in the ordering of timeslots has led to a problem with `ch_stop_time` this was not caught as timeslot tests were deleted with the old implementation and the `Schedule` method didn't have any `ch_stop_time` tests directly.


### Steps to reproduce the problem
```python
def test_ch_stop_time(self):
        reference_sched = Schedule()
        reference_sched = reference_sched.insert(
            10, Delay(10, DriveChannel(0)))
        reference_sched = reference_sched.insert(
            10, ShiftPhase(0.1, DriveChannel(0)))
        self.assertEqual(
            reference_sched.timeslots[DriveChannel(0)], [(10, 10,), (10, 20)])
        self.assertEqual(reference_sched.duration, 20)
        self.assertEqual(reference_sched.ch_stop_time(DriveChannel(0)), 20)
```


### What is the expected behavior?



### Suggested solutions





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
@@ -128,7 +128,7 @@ def ch_duration(self, *channels: List[Channel]) -> int:
         Args:
             *channels: Channels within ``self`` to include.
         """
-        return self.ch_stop_time(channels)
+        return self.ch_stop_time(*channels)
 
     def ch_start_time(self, *channels: List[Channel]) -> int:
         """Return the time of the start of the first instruction over the supplied channels.
@@ -669,10 +669,10 @@ def _insertion_index(intervals: List[Interval], new_interval: Interval, index: i
     if len(intervals) == 1:
         if _overlaps(intervals[0], new_interval):
             raise PulseError("New interval overlaps with existing.")
-        return index if new_interval[0] < intervals[0][0] else index + 1
+        return index if new_interval[1] <= intervals[0][0] else index + 1
 
     mid_idx = len(intervals) // 2
-    if new_interval[0] < intervals[mid_idx][0]:
+    if new_interval[1] <= intervals[mid_idx][0]:
         return _insertion_index(intervals[:mid_idx], new_interval, index=index)
     else:
         return _insertion_index(intervals[mid_idx:], new_interval, index=index + mid_idx)


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
@@ -446,6 +446,90 @@ def test_shift_unshift(self):
         shifted_sched = reference_sched.shift(10).shift(-10)
         self.assertEqual(shifted_sched, reference_sched)
 
+    def test_duration(self):
+        """Test schedule.duration."""
+        reference_sched = Schedule()
+        reference_sched = reference_sched.insert(
+            10, Delay(10, DriveChannel(0)))
+        reference_sched = reference_sched.insert(
+            10, Delay(50, DriveChannel(1)))
+        reference_sched = reference_sched.insert(
+            10, ShiftPhase(0.1, DriveChannel(0)))
+
+        reference_sched = reference_sched.insert(
+            100, ShiftPhase(0.1, DriveChannel(1)))
+
+        self.assertEqual(reference_sched.duration, 100)
+        self.assertEqual(reference_sched.duration, 100)
+
+    def test_ch_duration(self):
+        """Test schedule.ch_duration."""
+        reference_sched = Schedule()
+        reference_sched = reference_sched.insert(
+            10, Delay(10, DriveChannel(0)))
+        reference_sched = reference_sched.insert(
+            10, Delay(50, DriveChannel(1)))
+        reference_sched = reference_sched.insert(
+            10, ShiftPhase(0.1, DriveChannel(0)))
+
+        reference_sched = reference_sched.insert(
+            100, ShiftPhase(0.1, DriveChannel(1)))
+
+        self.assertEqual(reference_sched.ch_duration(DriveChannel(0)), 20)
+        self.assertEqual(reference_sched.ch_duration(DriveChannel(1)), 100)
+        self.assertEqual(reference_sched.ch_duration(*reference_sched.channels),
+                         reference_sched.duration)
+
+    def test_ch_start_time(self):
+        """Test schedule.ch_start_time."""
+        reference_sched = Schedule()
+        reference_sched = reference_sched.insert(
+            10, Delay(10, DriveChannel(0)))
+        reference_sched = reference_sched.insert(
+            10, Delay(50, DriveChannel(1)))
+        reference_sched = reference_sched.insert(
+            10, ShiftPhase(0.1, DriveChannel(0)))
+
+        reference_sched = reference_sched.insert(
+            100, ShiftPhase(0.1, DriveChannel(1)))
+
+        self.assertEqual(reference_sched.ch_start_time(DriveChannel(0)), 10)
+        self.assertEqual(reference_sched.ch_start_time(DriveChannel(1)), 10)
+
+    def test_ch_stop_time(self):
+        """Test schedule.ch_stop_time."""
+        reference_sched = Schedule()
+        reference_sched = reference_sched.insert(
+            10, Delay(10, DriveChannel(0)))
+        reference_sched = reference_sched.insert(
+            10, Delay(50, DriveChannel(1)))
+        reference_sched = reference_sched.insert(
+            10, ShiftPhase(0.1, DriveChannel(0)))
+
+        reference_sched = reference_sched.insert(
+            100, ShiftPhase(0.1, DriveChannel(1)))
+
+        self.assertEqual(reference_sched.ch_stop_time(DriveChannel(0)), 20)
+        self.assertEqual(reference_sched.ch_stop_time(DriveChannel(1)), 100)
+
+    def test_timeslots(self):
+        """Test schedule.timeslots."""
+        reference_sched = Schedule()
+        reference_sched = reference_sched.insert(
+            10, Delay(10, DriveChannel(0)))
+        reference_sched = reference_sched.insert(
+            10, Delay(50, DriveChannel(1)))
+        reference_sched = reference_sched.insert(
+            10, ShiftPhase(0.1, DriveChannel(0)))
+
+        reference_sched = reference_sched.insert(
+            100, ShiftPhase(0.1, DriveChannel(1)))
+
+        self.assertEqual(
+            reference_sched.timeslots[DriveChannel(0)], [(10, 10), (10, 20)])
+        self.assertEqual(
+            reference_sched.timeslots[DriveChannel(1)], [(10, 60), (100, 100)])
+
 
 class TestDelay(BaseTestSchedule):
     """Test Delay Instruction"""
@@ -866,6 +950,17 @@ def test_insertion_index(self):
         self.assertEqual(_insertion_index(intervals, (5, 6)), 2)
         self.assertEqual(_insertion_index(intervals, (8, 9)), 3)
 
+        longer_intervals = [(1, 2), (2, 3), (4, 5), (5, 6), (7, 9), (11, 11)]
+        self.assertEqual(_insertion_index(longer_intervals, (4, 4)), 2)
+        self.assertEqual(_insertion_index(longer_intervals, (5, 5)), 3)
+        self.assertEqual(_insertion_index(longer_intervals, (3, 4)), 2)
+        self.assertEqual(_insertion_index(longer_intervals, (3, 4)), 2)
+
+        # test when two identical zero duration timeslots are present
+        intervals = [(0, 10), (73, 73), (73, 73), (90, 101)]
+        self.assertEqual(_insertion_index(intervals, (42, 73)), 1)
+        self.assertEqual(_insertion_index(intervals, (73, 81)), 3)
+
     def test_insertion_index_when_overlapping(self):
         """Test that `_insertion_index` raises an error when the new_interval _overlaps."""
         intervals = [(10, 20), (44, 55), (60, 61), (80, 1000)]
@@ -874,6 +969,10 @@ def test_insertion_index_when_overlapping(self):
         with self.assertRaises(PulseError):
             _insertion_index(intervals, (100, 1500))
 
+        intervals = [(0, 1), (10, 15)]
+        with self.assertRaises(PulseError):
+            _insertion_index(intervals, (7, 13))
+
     def test_insertion_index_empty_list(self):
         """Test that the insertion index is properly found for empty lists."""
         self.assertEqual(_insertion_index([], (0, 1)), 0)