Repository: Qiskit/qiskit

Issue:
Improved error message for overlapping pulses
<!-- ⚠️ 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?
The current pulse overlap error message reads: `PulseError: 'Child schedules <Schedule 0>, <Schedule 1> overlap.'`

For a user building pulse schedules, it would be nice for the error to give more information about how they overlapped, so the user could more easily debug their mistake.

(For example, in my case, schedule 0 involved only qubit 0, and I thought schedule 1 involved only qubit 1, but I was actually measuring qubit 1 into mem slot 0, causing the conflict.)



Discussion:
@lcapelluto 

Looks like exception logged here -> [schedule.py#L65](https://github.com/Qiskit/qiskit-terra/blob/master/qiskit/pulse/schedule.py#L65)
```python
except PulseError as ts_err:
    raise PulseError('Child schedules {0} overlap.'.format(schedules)) from ts_err
```
which is raised by merging timeslots here -> [timeslots.py#L284](https://github.com/Qiskit/qiskit-terra/blob/master/qiskit/pulse/timeslots.py#L284)
```python
elif interval.has_overlap(ch_interval):
    raise PulseError("Timeslot: {0} overlaps with existing"
                     "Timeslot: {1}".format(timeslot, ch_timeslot))
```
which already have information regarding how timeslots are overlaped. 
Does this means that we can just append `ts_err` message to Schedule PulseError? like
```python
except PulseError as ts_err:
    raise PulseError('Child schedules {0} overlap: {1}'.format(schedules, ts_err.message)) from ts_err
```
And this will be rendered nicely as [timeslots.py#L187](https://github.com/Qiskit/qiskit-terra/blob/master/qiskit/pulse/timeslots.py#L187).

Am I missing something? If it is simple as that I can create a PR and jump on other issues.
Anyway I can work on that if you tell me what I'm missing :) 

Best wishes,
Iskandar



This is the code to reproduce what @lcapelluto pointed as an example case:
```python
system = pulse.PulseChannelSpec.from_backend(backend)

sched0 = pulse.Schedule()
sched0 |= pulse.Acquire(10)(qubits=system.qubits[0], mem_slots=system.memoryslots[0])
sched1 = pulse.Schedule()
sched1 |= pulse.Acquire(10)(qubits=system.qubits[1], mem_slots=system.memoryslots[0])

sched0 |= sched1 # this raises an error
```

I got an error message below and I think error message has been improved.
```
PulseError: 'Timeslot: Timeslot(MemorySlot(0), (0, 10)) overlaps with existingTimeslot: Timeslot(MemorySlot(0), (0, 10))'
```
But this is still not sufficient for novice users, especially who don't know `__repr__` of `TimeSlot`. This is because error message just reuse representation of TimeSlot object. We can rewrite error message to be like
```
Timeslot: MemorySlot(0) t=[0, 10] overlaps with existing Timeslot: MemorySlot(0) t=[0, 10]
```

We can get more confusing case by rewriting `sched1` as 
```python
sched1 |= pulse.Acquire(100)(qubits=system.qubits[1], mem_slots=system.memoryslots[0])  << 5
```

The error message is now
```
PulseError: 'Timeslot: Timeslot(MemorySlot(0), (5, 105)) overlaps with existingTimeslot: Timeslot(MemorySlot(0), (0, 10))'
```

Actually, in above schedule, a section where two schedules are overlapped is [5, 10], but error message says whole section of sched1 is overlapped. In this case, error message should be
```
PulseError: 'Input schedules has overlap in MemorySlot(0) t=[5, 10]'
```
Sorry, I noticed we have another option to reproduce the example case.
```python
sched = pulse.Schedule(sched0, sched1)
```

Then I got an error
```
PulseError: 'Child schedules (Schedule(0, (0, Instruction(Acquire(acq14, duration=10, kernel=None, discriminator=None), AcquireChannel(0), MemorySlot(0)))), Schedule(5, (5, Instruction(Acquire(acq15, duration=100, kernel=None, discriminator=None), AcquireChannel(1), MemorySlot(0))))) overlap.'
```
You're right. This is not readable.

@IceKhan13 thanks for pointing out simple solution. we have two things to be done.

1. Rewrite below error message
https://github.com/Qiskit/qiskit-terra/blob/277407c0550cf8e4cfc0e1844113594933efa903/qiskit/pulse/timeslots.py#L284

2. throw the message to Schedule. Representation object of schedule should be replaced by more readable one.

Resolving patch:
diff --git a/qiskit/pulse/schedule.py b/qiskit/pulse/schedule.py
--- a/qiskit/pulse/schedule.py
+++ b/qiskit/pulse/schedule.py
@@ -62,7 +62,15 @@ def __init__(self, *schedules: List[Union[ScheduleComponent, Tuple[int, Schedule
         try:
             self._timeslots = TimeslotCollection(*timeslots)
         except PulseError as ts_err:
-            raise PulseError('Child schedules {0} overlap.'.format(schedules)) from ts_err
+            formatted_schedules = []
+            for sched_pair in schedules:
+                sched = sched_pair[1] if isinstance(sched_pair, (List, tuple)) else sched_pair
+                formatted_sched = 'Schedule(name="{0}", duration={1})'.format(sched.name,
+                                                                              sched.duration)
+                formatted_schedules.append(formatted_sched)
+            formatted_schedules = ", ".join(formatted_schedules)
+            raise PulseError('Schedules overlap: {0} for {1}'
+                             ''.format(ts_err.message, formatted_schedules)) from ts_err
 
         self.__children = tuple(_children)
 
@@ -440,13 +448,11 @@ def __lshift__(self, time: int) -> 'Schedule':
         return self.shift(time)
 
     def __repr__(self):
-        res = 'Schedule("name=%s", ' % self._name if self._name else 'Schedule('
-        res += '%d, ' % self.start_time
-        instructions = [repr(instr) for instr in self.instructions]
-        res += ', '.join([str(i) for i in instructions[:50]])
-        if len(instructions) > 50:
-            return res + ', ...)'
-        return res + ')'
+        name = "name={}, ".format(self._name) if self._name else ""
+        instructions = ", ".join([repr(instr) for instr in self.instructions[:50]])
+        if len(self.instructions) > 50:
+            instructions += ", ..."
+        return "Schedule({0}{1} {2})".format(name, self.start_time, instructions)
 
 
 class ParameterizedSchedule:
diff --git a/qiskit/pulse/timeslots.py b/qiskit/pulse/timeslots.py
--- a/qiskit/pulse/timeslots.py
+++ b/qiskit/pulse/timeslots.py
@@ -281,8 +281,12 @@ def _merge_timeslot(self, timeslot: Timeslot) -> int:
             if interval.start >= ch_interval.stop:
                 break
             elif interval.has_overlap(ch_interval):
-                raise PulseError("Timeslot: {0} overlaps with existing"
-                                 "Timeslot: {1}".format(timeslot, ch_timeslot))
+                overlap_start = interval.start if interval.start > ch_interval.start \
+                    else ch_interval.start
+                overlap_end = ch_interval.stop if interval.stop > ch_interval.stop \
+                    else interval.stop
+                raise PulseError("Overlap on channel {0} over time range [{1}, {2}]"
+                                 "".format(timeslot.channel, overlap_start, overlap_end))
 
             insert_idx -= 1
 


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
@@ -175,6 +175,24 @@ def test_empty_schedule(self):
         self.assertEqual(TimeslotCollection(), sched.timeslots)
         self.assertEqual([], list(sched.instructions))
 
+    def test_overlapping_schedules(self):
+        """Test overlapping schedules."""
+
+        def my_test_make_schedule(acquire: int, memoryslot: int, shift: int):
+            op = Acquire(acquire)
+            sched1 = op(AcquireChannel(0), MemorySlot(memoryslot))
+            sched2 = op(AcquireChannel(1), MemorySlot(memoryslot)).shift(shift)
+
+            return Schedule(sched1, sched2)
+
+        self.assertIsInstance(my_test_make_schedule(4, 0, 4), Schedule)
+        self.assertRaisesRegex(PulseError,
+                               r".* MemorySlot\(0\) over time range \[2, 4\] .*",
+                               my_test_make_schedule, 4, 0, 2)
+        self.assertRaisesRegex(PulseError,
+                               r".* MemorySlot\(1\) over time range \[0, 4\] .*",
+                               my_test_make_schedule, 4, 1, 0)
+
     def test_flat_instruction_sequence_returns_instructions(self):
         """Test if `flat_instruction_sequence` returns `Instruction`s."""
         lp0 = self.linear(duration=3, slope=0.2, intercept=0.1)