Repository: Qiskit/qiskit

Issue:
Pulse schedule filtering methods
<!-- ⚠️ 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?
There should exist a set of methods to search through and filter a pulse schedule by certain characteristics such as `channels`, `intervals`, and instruction types and return a new pulse schedule containing only the filtered schedules.

Any idea for how this might be implemented is a generic private filter method 

`schedule._filter(*filter_funcs)`

that takes a set of filter functions (its also possible that `_instructions` could be augmented with this capability as well). This method would then traverse the tree of schedules/instructions and only return valid entries.

Finally, a public filtering method could be exposed with often used builtins such as 
`schedule.filter(*filter_funcs, channels=None, instructions=None, Intervals=None)`




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
@@ -15,9 +15,11 @@
 """Schedule."""
 
 import itertools
-from typing import List, Tuple, Iterable, Union, Dict, Callable
+import abc
+from typing import List, Tuple, Iterable, Union, Dict, Callable, Set, Optional, Type
 
-from qiskit.pulse import ops
+from . import ops
+from .timeslots import Interval
 from .channels import Channel
 from .interfaces import ScheduleComponent
 from .timeslots import TimeslotCollection
@@ -95,7 +97,7 @@ def channels(self) -> Tuple[Channel]:
         return self.timeslots.channels
 
     @property
-    def _children(self) -> Tuple[ScheduleComponent]:
+    def _children(self) -> Tuple[Tuple[int, ScheduleComponent], ...]:
         return self.__children
 
     @property
@@ -186,6 +188,93 @@ def flatten(self) -> 'ScheduleComponent':
         """Return a new schedule which is the flattened schedule contained all `instructions`."""
         return ops.flatten(self)
 
+    def 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) -> '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.
+
+        If no arguments are provided, this schedule is returned.
+
+        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: Time intervals to keep, e.g. [(0, 5), (6, 10)]
+            intervals: Time intervals to keep, e.g. [Interval(0, 5), Interval(6, 10)]
+        """
+        def only_channels(channels: Set[Channel]) -> Callable:
+            def channel_filter(time_inst: Tuple[int, 'Instruction']) -> bool:
+                return any([chan in channels for chan in time_inst[1].channels])
+            return channel_filter
+
+        def only_instruction_types(types: Iterable[abc.ABCMeta]) -> Callable:
+            def instruction_filter(time_inst: Tuple[int, 'Instruction']) -> bool:
+                return isinstance(time_inst[1], tuple(types))
+            return instruction_filter
+
+        def only_intervals(ranges: Iterable[Interval]) -> Callable:
+            def interval_filter(time_inst: Tuple[int, 'Instruction']) -> bool:
+                for i in ranges:
+                    if all([(i.begin <= ts.interval.shift(time_inst[0]).begin
+                             and ts.interval.shift(time_inst[0]).end <= i.end)
+                            for ts in time_inst[1].timeslots.timeslots]):
+                        return True
+                return False
+            return interval_filter
+
+        filter_funcs = list(filter_funcs)
+        if channels:
+            filter_funcs.append(only_channels(set(channels)))
+        if instruction_types:
+            filter_funcs.append(only_instruction_types(instruction_types))
+        if time_ranges:
+            filter_funcs.append(
+                only_intervals([Interval(start, end) for start, end 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).
+
+        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))
+
     def draw(self, dt: float = 1, style=None,
              filename: str = None, interp_method: Callable = None, scaling: float = 1,
              channels_to_plot: List[Channel] = None, plot_all: bool = False,


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
@@ -20,9 +20,10 @@
 from qiskit.pulse.channels import (DeviceSpecification, Qubit, RegisterSlot, MemorySlot,
                                    DriveChannel, AcquireChannel, ControlChannel, MeasureChannel)
 from qiskit.pulse.commands import (FrameChange, Acquire, PersistentValue, Snapshot,
-                                   functional_pulse, Instruction)
+                                   functional_pulse, Instruction, AcquireInstruction,
+                                   PulseInstruction, FrameChangeInstruction)
 from qiskit.pulse import pulse_lib
-from qiskit.pulse.timeslots import TimeslotCollection
+from qiskit.pulse.timeslots import TimeslotCollection, Interval
 from qiskit.pulse.exceptions import PulseError
 from qiskit.pulse.schedule import Schedule
 from qiskit.test import QiskitTestCase
@@ -307,6 +308,118 @@ def test_flatten(self):
 
         self.assertEqual(flat_sched.instructions, sched.instructions)
 
+    def test_filter_channels(self):
+        """Test filtering over channels."""
+        device = self.two_qubit_device
+        lp0 = self.linear(duration=3, slope=0.2, intercept=0.1)
+        acquire = Acquire(5)
+        sched = Schedule(name='fake_experiment')
+        sched = sched.insert(0, lp0(device.q[0].drive))
+        sched = sched.insert(10, lp0(device.q[1].drive))
+        sched = sched.insert(30, FrameChange(phase=-1.57)(device.q[0].drive))
+        sched = sched.insert(60, acquire(device.q, device.mem))
+        sched = sched.insert(90, lp0(device.q[0].drive))
+
+        self.assertEqual(len(sched.filter(channels=[AcquireChannel(1)]).instructions), 1)
+        channels = [AcquireChannel(1), DriveChannel(1)]
+        has_chan_1 = sched.filter(channels=channels)
+        for _, inst in has_chan_1.instructions:
+            self.assertTrue(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
+        lp0 = self.linear(duration=3, slope=0.2, intercept=0.1)
+        acquire = Acquire(5)
+        sched = Schedule(name='fake_experiment')
+        sched = sched.insert(0, lp0(device.q[0].drive))
+        sched = sched.insert(10, lp0(device.q[1].drive))
+        sched = sched.insert(30, FrameChange(phase=-1.57)(device.q[0].drive))
+        sched = sched.insert(60, acquire(device.q, device.mem))
+        sched = sched.insert(90, lp0(device.q[0].drive))
+
+        only_acquires = sched.filter(instruction_types=[AcquireInstruction])
+        for _, inst in only_acquires.instructions:
+            self.assertIsInstance(inst, AcquireInstruction)
+        only_pulse_and_fc = sched.filter(instruction_types=[PulseInstruction,
+                                                            FrameChangeInstruction])
+        for _, inst in only_pulse_and_fc.instructions:
+            self.assertIsInstance(inst, (PulseInstruction, FrameChangeInstruction))
+        self.assertEqual(len(only_pulse_and_fc.instructions), 4)
+        only_fc = sched.filter(instruction_types={FrameChangeInstruction})
+        self.assertEqual(len(only_fc.instructions), 1)
+
+    def test_filter_intervals(self):
+        """Test filtering on intervals."""
+        device = self.two_qubit_device
+        lp0 = self.linear(duration=3, slope=0.2, intercept=0.1)
+        acquire = Acquire(5)
+        sched = Schedule(name='fake_experiment')
+        sched = sched.insert(0, lp0(device.q[0].drive))
+        sched = sched.insert(10, lp0(device.q[1].drive))
+        sched = sched.insert(30, FrameChange(phase=-1.57)(device.q[0].drive))
+        sched = sched.insert(60, acquire(device.q, device.mem))
+        sched = sched.insert(90, lp0(device.q[0].drive))
+
+        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.end <= 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)
+
+    def test_filter_multiple(self):
+        """Test filter composition."""
+        device = self.two_qubit_device
+        lp0 = self.linear(duration=3, slope=0.2, intercept=0.1)
+        acquire = Acquire(5)
+        sched = Schedule(name='fake_experiment')
+        sched = sched.insert(0, lp0(device.q[0].drive))
+        sched = sched.insert(10, lp0(device.q[1].drive))
+        sched = sched.insert(30, FrameChange(phase=-1.57)(device.q[0].drive))
+        sched = sched.insert(60, acquire(device.q, device.mem))
+        sched = sched.insert(90, lp0(device.q[0].drive))
+
+        filtered = sched.filter(channels={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(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.assertIsInstance(inst, (FrameChangeInstruction, PulseInstruction))
+        self.assertTrue(len(filtered_b.instructions), 4)
+
+    def test__filter(self):
+        """Test _filter method."""
+        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.q[0].drive))
+        sched = sched.insert(10, lp0(device.q[1].drive))
+        sched = sched.insert(30, FrameChange(phase=-1.57)(device.q[0].drive))
+        for i in sched._filter([lambda x: True]).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)
+
     def test_buffering(self):
         """Test channel buffering."""
         buffer_chan = DriveChannel(0, buffer=5)