Repository: Qiskit/qiskit

Issue:
Add a not filter for schedules
There is a filter for schedules (https://github.com/Qiskit/qiskit-terra/blob/a3ae434326aefbb19a7861518b4a4f1e05528fb1/qiskit/pulse/schedule.py#L232) It would be convenient if that returned the remainder of the schedule that doesn't satisfy the filter. This makes it easier to, e.g., shift a part of a schedule. 

`filter(schedule, blah) << 100 + filter(schedule, not blah)`

<!-- ⚠️ 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?





Discussion:
Would this be easy to do @lcapelluto?
I think it should be pretty straightforward. For the API, how about:
```
schedule.filter(args)  # what we already have
schedule.exclude(args)  # the new function
```
sure as long as `schedule.filter(args)+schedule.exclude(args) == schedule`
@dcmckayibm it would require `|` not `+`, but otherwise yes 👍 
I meant whatever we are using for concatenating two schedules

Resolving patch:
diff --git a/qiskit/pulse/schedule.py b/qiskit/pulse/schedule.py
--- a/qiskit/pulse/schedule.py
+++ b/qiskit/pulse/schedule.py
@@ -232,12 +232,11 @@ def filter(self, *filter_funcs: List[Callable],
                time_ranges: Optional[Iterable[Tuple[int, int]]] = None,
                intervals: Optional[Iterable[Interval]] = None) -> 'Schedule':
         """
-        Return a new Schedule with only the instructions which pass though the provided filters.
-        Custom filters may be provided. If a list of channel indices is provided, only the
-        instructions that involve that channel (and maybe also others) will be included in the new
-        schedule. Similarly for instruction_types, only the instructions which are instances of the
-        provided types will be included. For intervals, instructions will be retained if their
-        timeslots are all wholly contained within *any* of the given intervals.
+        Return a new Schedule with only the instructions from this Schedule which pass though the
+        provided filters; i.e. an instruction will be retained iff every function in filter_funcs
+        returns True, the instruction occurs on a channel type contained in channels,
+        the instruction type is contained in instruction_types, and the period over which the
+        instruction operates is fully contained in one specified in time_ranges or intervals.
 
         If no arguments are provided, this schedule is returned.
 
@@ -246,8 +245,76 @@ def filter(self, *filter_funcs: List[Callable],
                           return a bool
             channels: For example, [DriveChannel(0), AcquireChannel(0)]
             instruction_types: For example, [PulseInstruction, AcquireInstruction]
-            time_ranges: Time intervals to keep, e.g. [(0, 5), (6, 10)]
-            intervals: Time intervals to keep, e.g. [Interval(0, 5), Interval(6, 10)]
+            time_ranges: For example, [(0, 5), (6, 10)]
+            intervals: For example, [Interval(0, 5), Interval(6, 10)]
+        """
+        composed_filter = self._construct_filter(*filter_funcs,
+                                                 channels=channels,
+                                                 instruction_types=instruction_types,
+                                                 time_ranges=time_ranges,
+                                                 intervals=intervals)
+        return self._apply_filter(composed_filter,
+                                  new_sched_name="{name}-filtered".format(name=self.name))
+
+    def exclude(self, *filter_funcs: List[Callable],
+                channels: Optional[Iterable[Channel]] = None,
+                instruction_types: Optional[Iterable[Type['Instruction']]] = None,
+                time_ranges: Optional[Iterable[Tuple[int, int]]] = None,
+                intervals: Optional[Iterable[Interval]] = None) -> 'Schedule':
+        """
+        Return a Schedule with only the instructions from this Schedule *failing* at least one of
+        the provided filters. This method is the complement of `self.filter`, so that:
+            self.filter(args) | self.exclude(args) == self
+
+        Args:
+            filter_funcs: A list of Callables which take a (int, ScheduleComponent) tuple and
+                          return a bool
+            channels: For example, [DriveChannel(0), AcquireChannel(0)]
+            instruction_types: For example, [PulseInstruction, AcquireInstruction]
+            time_ranges: For example, [(0, 5), (6, 10)]
+            intervals: For example, [Interval(0, 5), Interval(6, 10)]
+        """
+        composed_filter = self._construct_filter(*filter_funcs,
+                                                 channels=channels,
+                                                 instruction_types=instruction_types,
+                                                 time_ranges=time_ranges,
+                                                 intervals=intervals)
+        return self._apply_filter(lambda x: not composed_filter(x),
+                                  new_sched_name="{name}-excluded".format(name=self.name))
+
+    def _apply_filter(self, filter_func: Callable, new_sched_name: str) -> 'Schedule':
+        """
+        Return a Schedule containing only the instructions from this Schedule for which
+        filter_func returns True.
+
+        Args:
+            filter_func: function of the form (int, ScheduleComponent) -> bool
+            new_sched_name: name of the returned Schedule
+        """
+        subschedules = self.flatten()._children
+        valid_subschedules = [sched for sched in subschedules if filter_func(sched)]
+        return Schedule(*valid_subschedules, name=new_sched_name)
+
+    def _construct_filter(self, *filter_funcs: List[Callable],
+                          channels: Optional[Iterable[Channel]] = None,
+                          instruction_types: Optional[Iterable[Type['Instruction']]] = None,
+                          time_ranges: Optional[Iterable[Tuple[int, int]]] = None,
+                          intervals: Optional[Iterable[Interval]] = None) -> Callable:
+        """
+        Returns a boolean-valued function with input type (int, ScheduleComponent) that returns True
+        iff the input satisfies all of the criteria specified by the arguments; i.e. iff every
+        function in filter_funcs returns True, the instruction occurs on a channel type contained
+        in channels, the instruction type is contained in instruction_types, and the period over
+        which the instruction operates is fully contained in one specified in time_ranges or
+        intervals.
+
+        Args:
+            filter_funcs: A list of Callables which take a (int, ScheduleComponent) tuple and
+                          return a bool
+            channels: For example, [DriveChannel(0), AcquireChannel(0)]
+            instruction_types: For example, [PulseInstruction, AcquireInstruction]
+            time_ranges: For example, [(0, 5), (6, 10)]
+            intervals: For example, [Interval(0, 5), Interval(6, 10)]
         """
         def only_channels(channels: Set[Channel]) -> Callable:
             def channel_filter(time_inst: Tuple[int, 'Instruction']) -> bool:
@@ -269,49 +336,19 @@ def interval_filter(time_inst: Tuple[int, 'Instruction']) -> bool:
                 return False
             return interval_filter
 
-        filter_funcs = list(filter_funcs)
+        filter_func_list = list(filter_funcs)
         if channels:
-            filter_funcs.append(only_channels(set(channels)))
+            filter_func_list.append(only_channels(set(channels)))
         if instruction_types:
-            filter_funcs.append(only_instruction_types(instruction_types))
+            filter_func_list.append(only_instruction_types(instruction_types))
         if time_ranges:
-            filter_funcs.append(
+            filter_func_list.append(
                 only_intervals([Interval(start, stop) for start, stop in time_ranges]))
         if intervals:
-            filter_funcs.append(only_intervals(intervals))
-
-        if not filter_funcs:
-            return self
-
-        return self._filter(filter_funcs)
-
-    def _filter(self, filter_funcs: List[Callable]) -> 'Schedule':
-        """
-        Return a new Schedule with only the instructions which pass through every filter in
-        filter_funcs (i.e. when each function is applied to it, as described below, the function
-        returns True).
+            filter_func_list.append(only_intervals(intervals))
 
-        Expected function signature for each function in filter_funcs:
-            function(time_and_inst_tuple: Tuple[int, Instruction]) -> bool
-
-        For example:
-
-            def only_channel_one(time_and_inst_tuple) -> bool:
-                for chan in time_and_inst_tuple[1].channels:
-                    if chan.index == 1:
-                        return True
-                return False
-
-        Note:
-            The new schedule's name is the previous name appended with "-filtered".
-
-        Args:
-            filter_funcs: A list of Callables which follow the above format
-        """
-        valid_subschedules = self.flatten()._children
-        for filter_func in filter_funcs:
-            valid_subschedules = [sched for sched in valid_subschedules if filter_func(sched)]
-        return Schedule(*valid_subschedules, name="{name}-filtered".format(name=self.name))
+        # return function returning true iff all filters are passed
+        return lambda x: all([filter_func(x) for filter_func in filter_func_list])
 
     def draw(self, dt: float = 1, style: Optional['SchedStyle'] = None,
              filename: Optional[str] = None, interp_method: Optional[Callable] = None,


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
@@ -431,7 +431,7 @@ def test_delay_snapshot_channel(self):
 
 
 class TestScheduleFilter(BaseTestSchedule):
-    """Test Schedule filtering methods."""
+    """Test Schedule filtering methods"""
 
     def test_filter_channels(self):
         """Test filtering over channels."""
@@ -445,12 +445,20 @@ def test_filter_channels(self):
         sched = sched.insert(60, acquire(device.acquires, device.memoryslots))
         sched = sched.insert(90, lp0(device.drives[0]))
 
-        self.assertEqual(len(sched.filter(channels=[AcquireChannel(1)]).instructions), 1)
+        # split instructions for those on AcquireChannel(1) and those not
+        filtered, excluded = self._filter_and_test_consistency(sched, channels=[AcquireChannel(1)])
+        self.assertEqual(len(filtered.instructions), 1)
+        self.assertEqual(len(excluded.instructions), 4)
+
+        # Split schedule into the part with channels on 1 and into a part without
         channels = [AcquireChannel(1), DriveChannel(1)]
-        has_chan_1 = sched.filter(channels=channels)
-        for _, inst in has_chan_1.instructions:
+        filtered, excluded = self._filter_and_test_consistency(sched, channels=channels)
+        for _, inst in filtered.instructions:
             self.assertTrue(any([chan in channels for chan in inst.channels]))
 
+        for _, inst in excluded.instructions:
+            self.assertFalse(any([chan in channels for chan in inst.channels]))
+
     def test_filter_inst_types(self):
         """Test filtering on instruction types."""
         device = self.two_qubit_device
@@ -463,16 +471,30 @@ def test_filter_inst_types(self):
         sched = sched.insert(60, acquire(device.acquires, device.memoryslots))
         sched = sched.insert(90, lp0(device.drives[0]))
 
-        only_acquires = sched.filter(instruction_types=[AcquireInstruction])
-        for _, inst in only_acquires.instructions:
+        # test on Acquire
+        only_acquire, no_acquire = \
+            self._filter_and_test_consistency(sched, instruction_types=[AcquireInstruction])
+        for _, inst in only_acquire.instructions:
             self.assertIsInstance(inst, AcquireInstruction)
-        only_pulse_and_fc = sched.filter(instruction_types=[PulseInstruction,
-                                                            FrameChangeInstruction])
+        for _, inst in no_acquire.instructions:
+            self.assertFalse(isinstance(inst, AcquireInstruction))
+
+        # test two instruction types
+        only_pulse_and_fc, no_pulse_and_fc = \
+            self._filter_and_test_consistency(sched, instruction_types=[PulseInstruction,
+                                                                        FrameChangeInstruction])
         for _, inst in only_pulse_and_fc.instructions:
             self.assertIsInstance(inst, (PulseInstruction, FrameChangeInstruction))
+        for _, inst in no_pulse_and_fc.instructions:
+            self.assertFalse(isinstance(inst, (PulseInstruction, FrameChangeInstruction)))
         self.assertEqual(len(only_pulse_and_fc.instructions), 4)
-        only_fc = sched.filter(instruction_types={FrameChangeInstruction})
+        self.assertEqual(len(no_pulse_and_fc.instructions), 1)
+
+        # test on FrameChange
+        only_fc, no_fc = \
+            self._filter_and_test_consistency(sched, instruction_types={FrameChangeInstruction})
         self.assertEqual(len(only_fc.instructions), 1)
+        self.assertEqual(len(no_fc.instructions), 4)
 
     def test_filter_intervals(self):
         """Test filtering on intervals."""
@@ -486,25 +508,43 @@ def test_filter_intervals(self):
         sched = sched.insert(60, acquire(device.acquires, device.memoryslots))
         sched = sched.insert(90, lp0(device.drives[0]))
 
-        intervals_a = sched.filter(time_ranges=((0, 13),))
-        for time, inst in intervals_a.instructions:
-            self.assertTrue(0 <= time <= 13)
-            self.assertTrue(inst.timeslots.timeslots[0].interval.stop <= 13)
-        self.assertEqual(len(intervals_a.instructions), 2)
-
-        intervals_b = sched.filter(time_ranges=[(59, 65)])
-        self.assertEqual(len(intervals_b.instructions), 1)
-        self.assertEqual(intervals_b.instructions[0][0], 60)
-        self.assertIsInstance(intervals_b.instructions[0][1], AcquireInstruction)
-
-        non_full_intervals = sched.filter(time_ranges=[(0, 2), (8, 11), (61, 70)])
-        self.assertEqual(len(non_full_intervals.instructions), 0)
-
-        multi_interval = sched.filter(time_ranges=[(10, 15), (63, 93)])
-        self.assertEqual(len(multi_interval.instructions), 2)
-
-        multi_interval = sched.filter(intervals=[Interval(10, 15), Interval(63, 93)])
-        self.assertEqual(len(multi_interval.instructions), 2)
+        # split schedule into instructions occuring in (0,13), and those outside
+        filtered, excluded = self._filter_and_test_consistency(sched, time_ranges=((0, 13),))
+        for start_time, inst in filtered.instructions:
+            self.assertTrue((start_time >= 0) and (start_time + inst.stop_time <= 13))
+        for start_time, inst in excluded.instructions:
+            self.assertFalse((start_time >= 0) and (start_time + inst.stop_time <= 13))
+        self.assertEqual(len(filtered.instructions), 2)
+        self.assertEqual(len(excluded.instructions), 3)
+
+        # split into schedule occuring in and outside of interval (59,65)
+        filtered, excluded = self._filter_and_test_consistency(sched, time_ranges=[(59, 65)])
+        self.assertEqual(len(filtered.instructions), 1)
+        self.assertEqual(filtered.instructions[0][0], 60)
+        self.assertIsInstance(filtered.instructions[0][1], AcquireInstruction)
+        self.assertEqual(len(excluded.instructions), 4)
+        self.assertEqual(excluded.instructions[3][0], 90)
+        self.assertIsInstance(excluded.instructions[3][1], PulseInstruction)
+
+        # split instructions based on the interval
+        # (none should be, though they have some overlap with some of the instructions)
+        filtered, excluded = \
+            self._filter_and_test_consistency(sched, time_ranges=[(0, 2), (8, 11), (61, 70)])
+        self.assertEqual(len(filtered.instructions), 0)
+        self.assertEqual(len(excluded.instructions), 5)
+
+        # split instructions from multiple non-overlapping intervals, specified
+        # as time ranges
+        filtered, excluded = \
+            self._filter_and_test_consistency(sched, time_ranges=[(10, 15), (63, 93)])
+        self.assertEqual(len(filtered.instructions), 2)
+        self.assertEqual(len(excluded.instructions), 3)
+
+        # split instructions from non-overlapping intervals, specified as Intervals
+        filtered, excluded = \
+            self._filter_and_test_consistency(sched, intervals=[Interval(10, 15), Interval(63, 93)])
+        self.assertEqual(len(filtered.instructions), 2)
+        self.assertEqual(len(excluded.instructions), 3)
 
     def test_filter_multiple(self):
         """Test filter composition."""
@@ -518,31 +558,71 @@ def test_filter_multiple(self):
         sched = sched.insert(60, acquire(device.acquires, device.memoryslots))
         sched = sched.insert(90, lp0(device.drives[0]))
 
-        filtered = sched.filter(channels={0}, instruction_types=[PulseInstruction],
-                                time_ranges=[(25, 100)])
+        # split instructions with filters on channel 0, of type PulseInstruction,
+        # occuring in the time interval (25, 100)
+        filtered, excluded = self._filter_and_test_consistency(sched,
+                                                               channels={device.drives[0]},
+                                                               instruction_types=[PulseInstruction],
+                                                               time_ranges=[(25, 100)])
         for time, inst in filtered.instructions:
             self.assertIsInstance(inst, PulseInstruction)
-            self.assertTrue(any([chan.index == 0 for chan in inst.channels]))
+            self.assertTrue(all([chan.index == 0 for chan in inst.channels]))
             self.assertTrue(25 <= time <= 100)
-
-        filtered_b = sched.filter(instruction_types=[PulseInstruction, FrameChangeInstruction],
-                                  time_ranges=[(25, 100), (0, 30)])
-        for time, inst in filtered_b.instructions:
+        self.assertEqual(len(excluded.instructions), 4)
+        self.assertTrue(excluded.instructions[0][1].channels[0] == DriveChannel(0))
+        self.assertTrue(excluded.instructions[2][0] == 30)
+
+        # split based on PulseInstructions in the specified intervals
+        filtered, excluded = self._filter_and_test_consistency(sched,
+                                                               instruction_types=[PulseInstruction],
+                                                               time_ranges=[(25, 100), (0, 11)])
+        self.assertTrue(len(excluded.instructions), 3)
+        for time, inst in filtered.instructions:
             self.assertIsInstance(inst, (FrameChangeInstruction, PulseInstruction))
-        self.assertTrue(len(filtered_b.instructions), 4)
+        self.assertTrue(len(filtered.instructions), 4)
+        # make sure the PulseInstruction not in the intervals is maintained
+        self.assertIsInstance(excluded.instructions[0][1], PulseInstruction)
 
-    def test_filter(self):
-        """Test _filter method."""
+    def test_custom_filters(self):
+        """Test custom filters."""
         device = self.two_qubit_device
         lp0 = self.linear(duration=3, slope=0.2, intercept=0.1)
         sched = Schedule(name='fake_experiment')
         sched = sched.insert(0, lp0(device.drives[0]))
         sched = sched.insert(10, lp0(device.drives[1]))
         sched = sched.insert(30, FrameChange(phase=-1.57)(device.drives[0]))
-        for i in sched._filter([lambda x: True]).instructions:
+
+        filtered, excluded = self._filter_and_test_consistency(sched, lambda x: True)
+        for i in filtered.instructions:
             self.assertTrue(i in sched.instructions)
-        self.assertEqual(len(sched._filter([lambda x: False]).instructions), 0)
-        self.assertEqual(len(sched._filter([lambda x: x[0] < 30]).instructions), 2)
+        for i in excluded.instructions:
+            self.assertFalse(i in sched.instructions)
+
+        filtered, excluded = self._filter_and_test_consistency(sched, lambda x: False)
+        self.assertEqual(len(filtered.instructions), 0)
+        self.assertEqual(len(excluded.instructions), 3)
+
+        filtered, excluded = self._filter_and_test_consistency(sched, lambda x: x[0] < 30)
+        self.assertEqual(len(filtered.instructions), 2)
+        self.assertEqual(len(excluded.instructions), 1)
+
+        # multiple custom filters
+        filtered, excluded = self._filter_and_test_consistency(sched,
+                                                               lambda x: x[0] > 0,
+                                                               lambda x: x[0] < 30)
+        self.assertEqual(len(filtered.instructions), 1)
+        self.assertEqual(len(excluded.instructions), 2)
+
+    def _filter_and_test_consistency(self, schedule: Schedule, *args, **kwargs):
+        """
+        Returns the tuple
+        (schedule.filter(*args, **kwargs), schedule.exclude(*args, **kwargs)),
+        including a test that schedule.filter | schedule.exclude == schedule
+        """
+        filtered = schedule.filter(*args, **kwargs)
+        excluded = schedule.exclude(*args, **kwargs)
+        self.assertEqual(filtered | excluded, schedule)
+        return filtered, excluded
 
 
 class TestScheduleEquality(BaseTestSchedule):