Repository: Qiskit/qiskit

Issue:
TimeSlotCollection is_mergeable_with method causes channels to be added to instruction
<!-- ⚠️ 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**: 0.8.1
- **Python version**: 3.7
- **Operating system**: Osx

### What is the current behavior?
The calling TimeslotCollection is being extended when this method is called.


### Steps to reproduce the problem
```python
from qiskit.pulse.channels import DriveChannel, AcquireChannel, MemorySlot
from qiskit.pulse.commands import SamplePulse, Acquire

drive_ch0 = DriveChannel(0)
acquire_ch0 = AcquireChannel(0)
memory_slot0 = MemorySlot(0)

acquire = Acquire(100)

sine_pulse = SamplePulse([0, 1])
sine_instr = sine_pulse.to_instruction(drive_ch0)
acquire_instr = acquire(acquire_ch0, memory_slot0)

print(sine_instr.channels)
sine_instr.timeslots.is_mergeable_with(acquire_instr.timeslots)
print(sine_instr.channels)
print(acquire_instr.channels)
```
Output
```python
(DriveChannel(0),)
(DriveChannel(0), AcquireChannel(0), MemorySlot(0))
(AcquireChannel(0), MemorySlot(0))
```

### What is the expected behavior?
The TimeslotCollection should not be mutated and only `True`/`False` should be returned.


### Suggested solutions





Discussion:
This line `for interval in self._table[slot.channel]` in `is_mergeable_with()` seems to be what is causing this. Since `_table` is a `deafultdict()`, it automatically creates an entry with `key= slot.channel` and `value=[]` if the key provided is not found in the dictionary. 

I would like to try and work on this issue, I just wanted to confirm what expected behavior of the code inside of `is_mergeable_with()` is. 

My guess is that is retrieves the `interval`s of the `timeslot`s within the `TimeslotCollection` given, and checks those against the `interval`s of the `timeslot`s within the `TimeslotCollection` that is `self` by using `has_overlap()`. But it only looks like the check happens if the `timeslot.channel` given is the same as the `timeslot.channel` in `self`.

I'm not super knowledgeable about this space though, so my assumption could be off. 
Thank you for taking this on @Matt-Stypulkoski. Yes, this interpretation makes sense to make. Intervals only overlap if they share a channel.
Ok, just want to make sure I understand. Then an `AquireChannel` with an arbitrary `Interval` with `begin`: 1 and `end`: 2 would return `True` for `has_overlap()` if compared to another `AquireChannel` with conflicting `begin`/`end` times, but would return `False` if compared to a `DriveChannel` with an arbitrary `Interval` with `begin`: 1 and `end`: 2? 
Two channels will only overlap if they are the same channel, ie., they have the same type (in this case `AcquireChannel`) and index (`AcquireChannel.index`). Equality can be checked with `==`.

Resolving patch:
diff --git a/qiskit/pulse/timeslots.py b/qiskit/pulse/timeslots.py
--- a/qiskit/pulse/timeslots.py
+++ b/qiskit/pulse/timeslots.py
@@ -220,9 +220,10 @@ def is_mergeable_with(self, timeslots: 'TimeslotCollection') -> bool:
             timeslots: TimeslotCollection to be checked
         """
         for slot in timeslots.timeslots:
-            for interval in self._table[slot.channel]:
-                if slot.interval.has_overlap(interval):
-                    return False
+            if slot.channel in self.channels:
+                for interval in self._table[slot.channel]:
+                    if slot.interval.has_overlap(interval):
+                        return False
         return True
 
     def merged(self, timeslots: 'TimeslotCollection') -> 'TimeslotCollection':


Test patch:
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
@@ -120,6 +120,23 @@ def test_shift(self):
         expected = TimeslotCollection(Timeslot(Interval(11, 13), AcquireChannel(0)))
         self.assertEqual(expected, actual)
 
+    def test_is_mergeable_does_not_mutate_TimeslotCollection(self):
+        """Test that is_mergeable_with does not mutate the given TimeslotCollection"""
+
+        # Different channel but different interval
+        col1 = TimeslotCollection(Timeslot(Interval(1, 2), AcquireChannel(0)))
+        expected_channels = col1.channels
+        col2 = TimeslotCollection(Timeslot(Interval(3, 4), AcquireChannel(1)))
+        col1.is_mergeable_with(col2)
+        self.assertEqual(col1.channels, expected_channels)
+
+        # Different channel but same interval
+        col1 = TimeslotCollection(Timeslot(Interval(1, 2), AcquireChannel(0)))
+        expected_channels = col1.channels
+        col2 = TimeslotCollection(Timeslot(Interval(1, 2), AcquireChannel(1)))
+        col1.is_mergeable_with(col2)
+        self.assertEqual(col1.channels, expected_channels)
+
 
 if __name__ == '__main__':
     unittest.main()