Repository: Qiskit/qiskit

Issue:
Utility function to measure only a subset of qubits using OpenPulse
### What is the expected enhancement?
Currently, measurement in OpenPulse is often done by getting the default measurement schedule from the `CmdDef`:
```
meas = cmd_def.get('measure', qubits=tuple(range(total_number_of_qubits)))
```
This isn't ideal because it will stimulate measurement on all qubits.

More convenient would be to be able to do
```
schedule += measure_qubits(qubits_to_measure: List)
```
where `measure_qubits` is a utility function exposed within `pulse` and `qubits_to_measure` is a subset of all qubits. Only the qubits in `qubits_to_measure` will have a measurement pulse. All qubits will still have to be acquired according to the `meas_map`.

The implementation may be similar to changes in the scheduler which have been started here:
https://github.com/Qiskit/qiskit-terra/pull/3415


Discussion:
This won't work currently. The measurements need to be done all together which is why the backend cmd_def reports the measure command like that. 
@ajavadia -- the Acquire command does need to have all qubits acquired at once, as you say, but not all qubits need a stimulus pulse. this would allow you to measure only a subset of the qubits (although acquisitions will be done according to the meas map, which usually means all qubits)
oh ok if this works then we should do it as it reduces job size substantially.
This is a feature that should exist, but it's use should be configurable during scheduling. In some cases this might cause calibrated discriminators to behave poorly if they were calibrated with measurements on all qubits. In the long run we may have to build the capability to support target dependent scheduling passes.
What behavior do we want for this? My suggestion is to have the following
- Default mode: measure every qubit `schedule += measure_qubits()` in this case `qubits_to_measure` is `None` meaning measure everything.
- User specified mode: the user can explicitly customize which qubits to measure. E.g. `schedule += measure_qubits(qubits_to_measure=[0,1,3])`

We could also think of a mechanism that simplifies the measurement schedule. For instance, measure only the qubits that are involved in the schedule (aside from `Acquire` commands) as long as it does not, for instance, mess up the discriminator that will be used.
This sounds good to me.
As noted by @SooluThomas this is related to #3572.

Resolving patch:
diff --git a/qiskit/scheduler/__init__.py b/qiskit/scheduler/__init__.py
--- a/qiskit/scheduler/__init__.py
+++ b/qiskit/scheduler/__init__.py
@@ -30,3 +30,4 @@
 
 from qiskit.scheduler.config import ScheduleConfig
 from qiskit.scheduler.schedule_circuit import schedule_circuit
+from qiskit.scheduler.utils import measure, measure_all
diff --git a/qiskit/scheduler/methods/basic.py b/qiskit/scheduler/methods/basic.py
--- a/qiskit/scheduler/methods/basic.py
+++ b/qiskit/scheduler/methods/basic.py
@@ -12,11 +12,7 @@
 # copyright notice, and modified files need to carry a notice indicating
 # that they have been altered from the originals.
 
-"""The most straightforward scheduling methods: scheduling as early or as late as possible.
-
-Warning: Currently for both of these methods, the MemorySlots in circuit Measures are ignored.
-Qubits will be measured into the MemorySlot which matches the measured qubit's index. (Issue #2704)
-"""
+"""The most straightforward scheduling methods: scheduling as early or as late as possible."""
 
 from collections import defaultdict, namedtuple
 from typing import List
@@ -27,8 +23,8 @@
 from qiskit.extensions.standard.barrier import Barrier
 from qiskit.pulse.exceptions import PulseError
 from qiskit.pulse.schedule import Schedule
-from qiskit.pulse.channels import MemorySlot
-from qiskit.pulse.commands import AcquireInstruction
+from qiskit.pulse.channels import AcquireChannel
+from qiskit.scheduler.utils import measure
 
 from qiskit.scheduler.config import ScheduleConfig
 
@@ -134,8 +130,10 @@ def translate_gates_to_pulse_defs(circuit: QuantumCircuit,
     Args:
         circuit: The quantum circuit to translate
         schedule_config: Backend specific parameters used for building the Schedule
+
     Returns:
         A list of CircuitPulseDefs: the pulse definition for each circuit element
+
     Raises:
         QiskitError: If circuit uses a command that isn't defined in config.inst_map
     """
@@ -146,31 +144,15 @@ def translate_gates_to_pulse_defs(circuit: QuantumCircuit,
 
     def get_measure_schedule() -> CircuitPulseDef:
         """Create a schedule to measure the qubits queued for measuring."""
-        measures = set()
-        all_qubits = set()
         sched = Schedule()
-        for qubit in qubit_mem_slots:
-            measures.add(tuple(schedule_config.meas_map[qubit]))
-        for qubits in measures:
-            all_qubits.update(qubits)
-            unused_mem_slots = set(qubits) - set(qubit_mem_slots.values())
-            default_sched = inst_map.get('measure', qubits)
-            for time, inst in default_sched.instructions:
-                if isinstance(inst, AcquireInstruction):
-                    for channel in inst.acquires:
-                        if channel.index in qubit_mem_slots.keys():
-                            mem_slot = MemorySlot(qubit_mem_slots[channel.index])
-                        else:
-                            mem_slot = MemorySlot(unused_mem_slots.pop())
-                        sched._union((time, AcquireInstruction(command=inst.command,
-                                                               acquire=channel,
-                                                               mem_slot=mem_slot)))
-
-                # Measurement pulses should only be added if its qubit was measured by the user
-                elif inst.channels[0].index in qubit_mem_slots.keys():
-                    sched._union((time, inst))
+        sched += measure(qubits=list(qubit_mem_slots.keys()),
+                         inst_map=inst_map,
+                         meas_map=schedule_config.meas_map,
+                         qubit_mem_slots=qubit_mem_slots)
         qubit_mem_slots.clear()
-        return CircuitPulseDef(schedule=sched, qubits=list(all_qubits))
+        return CircuitPulseDef(schedule=sched,
+                               qubits=[chan.index for chan in sched.channels
+                                       if isinstance(chan, AcquireChannel)])
 
     for inst, qubits, clbits in circuit.data:
         inst_qubits = [qubit.index for qubit in qubits]  # We want only the indices of the qubits
diff --git a/qiskit/scheduler/utils.py b/qiskit/scheduler/utils.py
--- a/qiskit/scheduler/utils.py
+++ b/qiskit/scheduler/utils.py
@@ -14,7 +14,12 @@
 
 """Scheduling utility functions."""
 
-from typing import Dict, List
+from typing import Dict, List, Optional, Union
+from qiskit.pulse.instruction_schedule_map import InstructionScheduleMap
+from qiskit.pulse.schedule import Schedule
+from qiskit.pulse.channels import MemorySlot
+from qiskit.pulse.commands import AcquireInstruction
+from qiskit.pulse.exceptions import PulseError
 
 
 def format_meas_map(meas_map: List[List[int]]) -> Dict[int, List[int]]:
@@ -34,3 +39,89 @@ def format_meas_map(meas_map: List[List[int]]) -> Dict[int, List[int]]:
         for q in sublist:
             qubit_mapping[q] = sublist
     return qubit_mapping
+
+
+def measure(qubits: List[int],
+            backend: Optional['BaseBackend'] = None,
+            inst_map: Optional[InstructionScheduleMap] = None,
+            meas_map: Optional[Union[List[List[int]], Dict[int, List[int]]]] = None,
+            qubit_mem_slots: Optional[Dict[int, int]] = None,
+            measure_name: str = 'measure') -> Schedule:
+    """
+    Return a schedule which measures the requested qubits according to the given
+    instruction mapping and measure map, or by using the defaults provided by the backend.
+
+    By default, the measurement results for each qubit are trivially mapped to the qubit
+    index. This behavior is overridden by qubit_mem_slots. For instance, to measure
+    qubit 0 into MemorySlot(1), qubit_mem_slots can be provided as {0: 1}.
+
+    Args:
+        qubits: List of qubits to be measured.
+        backend: A backend instance, which contains hardware-specific data required for scheduling.
+        inst_map: Mapping of circuit operations to pulse schedules. If None, defaults to the
+                  ``instruction_schedule_map`` of ``backend``.
+        meas_map: List of sets of qubits that must be measured together. If None, defaults to
+                  the ``meas_map`` of ``backend``.
+        qubit_mem_slots: Mapping of measured qubit index to classical bit index.
+        measure_name: Name of the measurement schedule.
+
+    Returns:
+        A measurement schedule corresponding to the inputs provided.
+
+    Raises:
+        PulseError: If both ``inst_map`` or ``meas_map``, and ``backend`` is None.
+    """
+    schedule = Schedule(name="Default measurement schedule for qubits {}".format(qubits))
+    try:
+        inst_map = inst_map or backend.defaults().instruction_schedule_map
+        meas_map = meas_map or backend.configuration().meas_map
+    except AttributeError:
+        raise PulseError('inst_map or meas_map, and backend cannot be None simultaneously')
+    if isinstance(meas_map, List):
+        meas_map = format_meas_map(meas_map)
+
+    measure_groups = set()
+    for qubit in qubits:
+        measure_groups.add(tuple(meas_map[qubit]))
+    for measure_group_qubits in measure_groups:
+        if qubit_mem_slots is not None:
+            unused_mem_slots = set(measure_group_qubits) - set(qubit_mem_slots.values())
+        try:
+            default_sched = inst_map.get(measure_name, measure_group_qubits)
+        except PulseError:
+            raise PulseError("We could not find a default measurement schedule called '{}'. "
+                             "Please provide another name using the 'measure_name' keyword "
+                             "argument. For assistance, the instructions which are defined are: "
+                             "{}".format(measure_name, inst_map.instructions))
+
+        for time, inst in default_sched.instructions:
+            if qubit_mem_slots and isinstance(inst, AcquireInstruction):
+                for channel in inst.acquires:
+                    if channel.index in qubit_mem_slots:
+                        mem_slot = MemorySlot(qubit_mem_slots[channel.index])
+                    else:
+                        mem_slot = MemorySlot(unused_mem_slots.pop())
+                    schedule = schedule.insert(time, AcquireInstruction(command=inst.command,
+                                                                        acquire=channel,
+                                                                        mem_slot=mem_slot))
+            elif qubit_mem_slots is None and isinstance(inst, AcquireInstruction):
+                schedule = schedule.insert(time, inst)
+            # Measurement pulses should only be added if its qubit was measured by the user
+            elif inst.channels[0].index in qubits:
+                schedule = schedule.insert(time, inst)
+
+    return schedule
+
+
+def measure_all(backend: 'BaseBackend') -> Schedule:
+    """
+    Return a Schedule which measures all qubits of the given backend.
+
+    Args:
+        backend: A backend instance, which contains hardware-specific data required for scheduling.
+
+    Returns:
+        A schedule corresponding to the inputs provided.
+    """
+    return measure(qubits=list(range(backend.configuration().n_qubits)),
+                   backend=backend)


Test patch:
diff --git a/test/python/scheduler/test_utils.py b/test/python/scheduler/test_utils.py
new file mode 100644
--- /dev/null
+++ b/test/python/scheduler/test_utils.py
@@ -0,0 +1,102 @@
+# -*- coding: utf-8 -*-
+
+# This code is part of Qiskit.
+#
+# (C) Copyright IBM 2019.
+#
+# This code is licensed under the Apache License, Version 2.0. You may
+# obtain a copy of this license in the LICENSE.txt file in the root directory
+# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.
+#
+# Any modifications or derivative works of this code must retain this
+# copyright notice, and modified files need to carry a notice indicating
+# that they have been altered from the originals.
+
+"""Test cases for Scheduler Utility functions."""
+
+from qiskit.pulse import (Schedule, AcquireChannel, Acquire, InstructionScheduleMap,
+                          MeasureChannel, MemorySlot, GaussianSquare)
+from qiskit.scheduler import measure, measure_all
+from qiskit.pulse.exceptions import PulseError
+from qiskit.test.mock import FakeOpenPulse2Q
+from qiskit.test import QiskitTestCase
+
+
+class TestUtils(QiskitTestCase):
+    """Utils tests."""
+
+    def setUp(self):
+        self.backend = FakeOpenPulse2Q()
+        self.inst_map = self.backend.defaults().instruction_schedule_map
+
+    def test_measure(self):
+        """Test utility function - measure."""
+        acquire = Acquire(duration=10)
+        sched = measure(qubits=[0],
+                        backend=self.backend)
+        expected = Schedule(
+            self.inst_map.get('measure', [0, 1]).filter(channels=[MeasureChannel(0)]),
+            acquire(AcquireChannel(0), MemorySlot(0)),
+            acquire(AcquireChannel(1), MemorySlot(1)))
+        self.assertEqual(sched.instructions, expected.instructions)
+
+    def test_measure_sched_with_qubit_mem_slots(self):
+        """Test measure with custom qubit_mem_slots."""
+        acquire = Acquire(duration=10)
+        sched = measure(qubits=[0],
+                        backend=self.backend,
+                        qubit_mem_slots={0: 1})
+        expected = Schedule(
+            self.inst_map.get('measure', [0, 1]).filter(channels=[MeasureChannel(0)]),
+            acquire(AcquireChannel(0), MemorySlot(1)),
+            acquire(AcquireChannel(1), MemorySlot(0)))
+        self.assertEqual(sched.instructions, expected.instructions)
+
+    def test_measure_sched_with_meas_map(self):
+        """Test measure with custom meas_map as list and dict."""
+        acquire = Acquire(duration=10)
+        sched_with_meas_map_list = measure(qubits=[0],
+                                           backend=self.backend,
+                                           meas_map=[[0, 1]])
+        sched_with_meas_map_dict = measure(qubits=[0],
+                                           backend=self.backend,
+                                           meas_map={0: [0, 1], 1: [0, 1]})
+        expected = Schedule(
+            self.inst_map.get('measure', [0, 1]).filter(channels=[MeasureChannel(0)]),
+            acquire(AcquireChannel(0), MemorySlot(0)),
+            acquire(AcquireChannel(1), MemorySlot(1)))
+        self.assertEqual(sched_with_meas_map_list.instructions, expected.instructions)
+        self.assertEqual(sched_with_meas_map_dict.instructions, expected.instructions)
+
+    def test_measure_with_custom_inst_map(self):
+        """Test measure with custom inst_map, meas_map with measure_name."""
+        q0_sched = GaussianSquare(1200, 1, 0.4, 1150)(MeasureChannel(0))
+        q0_sched += Acquire(1200)(AcquireChannel(0), MemorySlot(0))
+        inst_map = InstructionScheduleMap()
+        inst_map.add('my_sched', 0, q0_sched)
+        sched = measure(qubits=[0],
+                        measure_name='my_sched',
+                        inst_map=inst_map,
+                        meas_map=[[0]])
+        self.assertEqual(sched.instructions, q0_sched.instructions)
+
+        with self.assertRaises(PulseError):
+            measure(qubits=[0],
+                    measure_name="name",
+                    inst_map=inst_map,
+                    meas_map=[[0]])
+
+    def test_fail_measure(self):
+        """Test failing measure."""
+        with self.assertRaises(PulseError):
+            measure(qubits=[0],
+                    meas_map=self.backend.configuration().meas_map)
+        with self.assertRaises(PulseError):
+            measure(qubits=[0],
+                    inst_map=self.inst_map)
+
+    def test_measure_all(self):
+        """Test measure_all function."""
+        sched = measure_all(self.backend)
+        expected = Schedule(self.inst_map.get('measure', [0, 1]))
+        self.assertEqual(sched.instructions, expected.instructions)