Repository: Qiskit/qiskit

Issue:
Scheduler needs to respect MemSlots in QuantumCircuit Measures
<!-- ⚠️ If you do not respect this template, your issue will be closed -->
<!-- ⚠️ Make sure to browse the opened and closed issues -->

### Information

Known bug from my PR https://github.com/Qiskit/qiskit-terra/pull/2650

### What is the current behavior?
When passing a circuit through the basic scheduler (`qiskit.scheduler.schedule`), the resulting `Schedule` will always measure qubit results into the `MemorySlot` which matches the qubit index.
For example:
```
qc.measure(q[0], c[3])
```
will measure qubit 0 into `MemorySlot(0)` rather than `MemorySlot(3)` -- it will use the default Acquire from the cmd_def which is probably something like `Acquire([0, 1], [MemSlot(0), MemSlot(1)])`. The correct behaviour is to use the classical reg from the instruction and then modify the `AcquireInstruction` from the `Schedule` returned by the `cmd_def`. It's unclear what we should do with the other qubits that are measured within the same `meas_map` group, if the user doesn't specify those other qubits to be measured.

We need a follow up PR to handle this more carefully. The solution likely needs to update the helper function: `qiskit.scheduler.methods.basic.translate_gates_to_pulse_defs.get_measure_schedule`


### Steps to reproduce the problem
### What is the expected behavior?
### Suggested solutions
see above



Discussion:
Another thing: The `measure` schedule returned by the backend will have pulses on all `MeasureChannel`s for qubits being measured. The user circuit may only be measuring a subset of those qubits. We would like to remove any pulses from `MeasureChannel`s which are being played on qubits that the user did not want to schedule.

Summary:
Given a default `measure` Schedule (from `backend.defaults().build_cmd_def().get('measure', <qubits>)`, make the following modifications before adding to the final `Schedule`:
  1) Remove extra measurement pulses if the circuit doesn't have measures on those qubits
  2) Map acquisitions to the memory slots specified by the circuit

Resolving patch:
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
@@ -27,6 +27,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.scheduler.config import ScheduleConfig
 
@@ -140,31 +142,50 @@ def translate_gates_to_pulse_defs(circuit: QuantumCircuit,
     circ_pulse_defs = []
 
     cmd_def = schedule_config.cmd_def
-    measured_qubits = set()  # Collect qubits that would like to be measured
+    qubit_mem_slots = {}  # Map measured qubit index to classical bit index
 
     def get_measure_schedule() -> CircuitPulseDef:
         """Create a schedule to measure the qubits queued for measuring."""
         measures = set()
         all_qubits = set()
         sched = Schedule()
-        for q in measured_qubits:
-            measures.add(tuple(schedule_config.meas_map[q]))
+        for qubit in qubit_mem_slots:
+            measures.add(tuple(schedule_config.meas_map[qubit]))
         for qubits in measures:
             all_qubits.update(qubits)
-            # TODO (Issue #2704): Respect MemorySlots from the input circuit
-            sched |= cmd_def.get('measure', qubits)
-        measured_qubits.clear()
+            unused_mem_slots = set(qubits) - set(qubit_mem_slots.values())
+            default_sched = cmd_def.get('measure', qubits)
+            for time, inst in default_sched.instructions:
+                if isinstance(inst, AcquireInstruction):
+                    mem_slots = []
+                    for channel in inst.acquires:
+                        if channel.index in qubit_mem_slots.keys():
+                            mem_slots.append(MemorySlot(qubit_mem_slots[channel.index]))
+                        else:
+                            mem_slots.append(MemorySlot(unused_mem_slots.pop()))
+                    new_acquire = AcquireInstruction(command=inst.command,
+                                                     acquires=inst.acquires,
+                                                     mem_slots=mem_slots)
+                    sched._union((time, new_acquire))
+                # 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))
+        qubit_mem_slots.clear()
         return CircuitPulseDef(schedule=sched, qubits=list(all_qubits))
 
-    for inst, qubits, _ in circuit.data:
+    for inst, qubits, clbits in circuit.data:
         inst_qubits = [qubit.index for qubit in qubits]  # We want only the indices of the qubits
-        if any(q in measured_qubits for q in inst_qubits):
+        if any(q in qubit_mem_slots for q in inst_qubits):
             # If we are operating on a qubit that was scheduled to be measured, process that first
             circ_pulse_defs.append(get_measure_schedule())
         if isinstance(inst, Barrier):
             circ_pulse_defs.append(CircuitPulseDef(schedule=inst, qubits=inst_qubits))
         elif isinstance(inst, Measure):
-            measured_qubits.update(inst_qubits)
+            if (len(inst_qubits) != 1 and len(clbits) != 1):
+                raise QiskitError("Qubit '{0}' or classical bit '{1}' errored because the "
+                                  "circuit Measure instruction only takes one of "
+                                  "each.".format(inst_qubits, clbits))
+            qubit_mem_slots[inst_qubits[0]] = clbits[0].index
         else:
             try:
                 circ_pulse_defs.append(
@@ -174,7 +195,7 @@ def get_measure_schedule() -> CircuitPulseDef:
                 raise QiskitError("Operation '{0}' on qubit(s) {1} not supported by the backend "
                                   "command definition. Did you remember to transpile your input "
                                   "circuit for the same backend?".format(inst.name, inst_qubits))
-    if measured_qubits:
+    if qubit_mem_slots:
         circ_pulse_defs.append(get_measure_schedule())
 
     return circ_pulse_defs


Test patch:
diff --git a/qiskit/test/mock/fake_openpulse_3q.py b/qiskit/test/mock/fake_openpulse_3q.py
--- a/qiskit/test/mock/fake_openpulse_3q.py
+++ b/qiskit/test/mock/fake_openpulse_3q.py
@@ -53,7 +53,7 @@ def __init__(self):
             dt=1.3333,
             dtm=10.5,
             rep_times=[100, 250, 500, 1000],
-            meas_map=[[0, 1]],
+            meas_map=[[0, 1, 2]],
             channel_bandwidth=[
                 [-0.2, 0.4], [-0.3, 0.3], [-0.3, 0.3],
                 [-0.02, 0.02], [-0.02, 0.02], [-0.02, 0.02],
diff --git a/test/python/scheduler/test_basic_scheduler.py b/test/python/scheduler/test_basic_scheduler.py
--- a/test/python/scheduler/test_basic_scheduler.py
+++ b/test/python/scheduler/test_basic_scheduler.py
@@ -15,7 +15,8 @@
 """Test cases for the pulse scheduler passes."""
 
 from qiskit import QuantumRegister, ClassicalRegister, QuantumCircuit, schedule
-from qiskit.pulse import Schedule, DriveChannel
+from qiskit.pulse import (Schedule, DriveChannel, AcquireChannel, Acquire,
+                          MeasureChannel, MemorySlot)
 
 from qiskit.test.mock import FakeOpenPulse2Q, FakeOpenPulse3Q
 from qiskit.test import QiskitTestCase
@@ -108,10 +109,7 @@ def test_asap_pass(self):
             (28, self.cmd_def.get('u2', [1], 0.5, 0.25)),
             (56, self.cmd_def.get('cx', [0, 1])),
             (78, self.cmd_def.get('measure', [0, 1])))
-        for actual, expected in zip(sched.instructions, expected.instructions):
-            self.assertEqual(actual[0], expected[0])
-            self.assertEqual(actual[1].command, expected[1].command)
-            self.assertEqual(actual[1].channels, expected[1].channels)
+        self.assertEqual(sched.instructions, expected.instructions)
 
     def test_alap_resource_respecting(self):
         """Test that the ALAP pass properly respects busy resources when backwards scheduling.
@@ -142,10 +140,7 @@ def test_cmd_def_schedules_unaltered(self):
         qc.cx(q[0], q[1])
         sched1 = schedule(qc, self.backend, method="as_soon_as_possible")
         sched2 = schedule(qc, self.backend, method="as_late_as_possible")
-        for asap, alap in zip(sched1.instructions, sched2.instructions):
-            self.assertEqual(asap[0], alap[0])
-            self.assertEqual(asap[1].command, alap[1].command)
-            self.assertEqual(asap[1].channels, alap[1].channels)
+        self.assertEqual(sched1.instructions, sched2.instructions)
         insts = sched1.instructions
         self.assertEqual(insts[0][0], 0)
         self.assertEqual(insts[1][0], 10)
@@ -153,8 +148,12 @@ def test_cmd_def_schedules_unaltered(self):
         self.assertEqual(insts[3][0], 20)
 
     def test_measure_combined(self):
-        """Test that measures on different qubits are combined, but measures on the same qubit
-        adds another measure to the schedule."""
+        """
+        Test to check for measure on the same qubit which generated another measure schedule.
+
+        The measures on different qubits are combined, but measures on the same qubit
+        adds another measure to the schedule.
+        """
         q = QuantumRegister(2)
         c = ClassicalRegister(2)
         qc = QuantumCircuit(q, c)
@@ -168,11 +167,10 @@ def test_measure_combined(self):
             self.cmd_def.get('u2', [0], 3.14, 1.57),
             (28, self.cmd_def.get('cx', [0, 1])),
             (50, self.cmd_def.get('measure', [0, 1])),
-            (60, self.cmd_def.get('measure', [0, 1])))
-        for actual, expected in zip(sched.instructions, expected.instructions):
-            self.assertEqual(actual[0], expected[0])
-            self.assertEqual(actual[1].command, expected[1].command)
-            self.assertEqual(actual[1].channels, expected[1].channels)
+            (60, self.cmd_def.get('measure', [0, 1]).filter(channels=[MeasureChannel(1)])),
+            (60, Acquire(duration=10)([AcquireChannel(0), AcquireChannel(1)],
+                                      [MemorySlot(0), MemorySlot(1)])))
+        self.assertEqual(sched.instructions, expected.instructions)
 
     def test_3q_schedule(self):
         """Test a schedule that was recommended by David McKay :D """
@@ -195,10 +193,7 @@ def test_3q_schedule(self):
             (50, cmd_def.get('cx', [1, 2])),
             (72, cmd_def.get('u2', [2], 0.778, 0.122)),
             (74, cmd_def.get('u3', [0], 3.14, 1.57)))
-        for actual, expected in zip(sched.instructions, expected.instructions):
-            self.assertEqual(actual[0], expected[0])
-            self.assertEqual(actual[1].command, expected[1].command)
-            self.assertEqual(actual[1].channels, expected[1].channels)
+        self.assertEqual(sched.instructions, expected.instructions)
 
     def test_schedule_multi(self):
         """Test scheduling multiple circuits at once."""
@@ -210,10 +205,7 @@ def test_schedule_multi(self):
         qc1.cx(q[0], q[1])
         schedules = schedule([qc0, qc1], self.backend)
         expected_insts = schedule(qc0, self.backend).instructions
-        for actual, expected in zip(schedules[0].instructions, expected_insts):
-            self.assertEqual(actual[0], expected[0])
-            self.assertEqual(actual[1].command, expected[1].command)
-            self.assertEqual(actual[1].channels, expected[1].channels)
+        self.assertEqual(schedules[0].instructions, expected_insts)
 
     def test_circuit_name_kept(self):
         """Test that the new schedule gets its name from the circuit."""
@@ -251,10 +243,7 @@ def test_can_add_gates_into_free_space(self):
             (28, self.cmd_def.get('u1', [1], 3.14)),
             (28, self.cmd_def.get('u2', [0], 0, 0)),
             (28, self.cmd_def.get('u2', [1], 0, 0)))
-        for actual, expected in zip(sched.instructions, expected.instructions):
-            self.assertEqual(actual[0], expected[0])
-            self.assertEqual(actual[1].command, expected[1].command)
-            self.assertEqual(actual[1].channels, expected[1].channels)
+        self.assertEqual(sched.instructions, expected.instructions)
 
     def test_barriers_in_middle(self):
         """As a follow on to `test_can_add_gates_into_free_space`, similar issues
@@ -276,7 +265,66 @@ def test_barriers_in_middle(self):
             (28, self.cmd_def.get('u1', [1], 3.14)),
             (28, self.cmd_def.get('u2', [0], 0, 0)),
             (28, self.cmd_def.get('u2', [1], 0, 0)))
-        for actual, expected in zip(sched.instructions, expected.instructions):
-            self.assertEqual(actual[0], expected[0])
-            self.assertEqual(actual[1].command, expected[1].command)
-            self.assertEqual(actual[1].channels, expected[1].channels)
+        self.assertEqual(sched.instructions, expected.instructions)
+
+    def test_only_needed_measures(self):
+        """Test that `MeasureChannel`s are only added for measured qubits."""
+        q = QuantumRegister(2)
+        c = ClassicalRegister(2)
+        qc = QuantumCircuit(q, c)
+        qc.measure(q[1], c[1])
+        sched_all_channels = schedule(qc, self.backend, method="as_soon_as_possible").channels
+        deleted_channels = [MeasureChannel(0)]
+        self.assertNotIn(sched_all_channels, deleted_channels)
+
+    def test_user_mapping_for_memslots(self):
+        """
+        Test that the new schedule only has required `MeasureChannel`s and that the
+        `MemorySlot`s are mapped according to the input circuit.
+        """
+        q = QuantumRegister(2)
+        c = ClassicalRegister(2)
+        qc = QuantumCircuit(q, c)
+        qc.measure(q[0], c[1])
+        sched = schedule(qc, self.backend)
+        expected = Schedule(
+            self.cmd_def.get('measure', [0, 1]).filter(channels=[MeasureChannel(0)]),
+            Acquire(duration=10)([AcquireChannel(0), AcquireChannel(1)],
+                                 [MemorySlot(1), MemorySlot(0)]))
+        self.assertEqual(sched.instructions, expected.instructions)
+
+    def test_user_mapping_for_memslots_3Q(self):
+        """Test measuring two of three qubits."""
+        backend = FakeOpenPulse3Q()
+        cmd_def = backend.defaults().build_cmd_def()
+        q = QuantumRegister(3)
+        c = ClassicalRegister(3)
+        qc = QuantumCircuit(q, c)
+        qc.measure(q[1], c[2])
+        qc.measure(q[2], c[0])
+        sched = schedule(qc, backend)
+        expected = Schedule(
+            cmd_def.get('measure', [0, 1, 2]).filter(
+                channels=[MeasureChannel(1), MeasureChannel(2)]),
+            Acquire(duration=10)([AcquireChannel(0), AcquireChannel(1), AcquireChannel(2)],
+                                 [MemorySlot(1), MemorySlot(2), MemorySlot(0)]))
+        self.assertEqual(sched.instructions, expected.instructions)
+
+    def test_multiple_measure_in_3Q(self):
+        """Test multiple measure, user memslot mapping, 3Q."""
+        backend = FakeOpenPulse3Q()
+        cmd_def = backend.defaults().build_cmd_def()
+        q = QuantumRegister(3)
+        c = ClassicalRegister(5)
+        qc = QuantumCircuit(q, c)
+        qc.measure(q[0], c[2])
+        qc.measure(q[0], c[4])
+        sched = schedule(qc, backend)
+        expected = Schedule(
+            cmd_def.get('measure', [0, 1, 2]).filter(channels=[MeasureChannel(0)]),
+            Acquire(duration=10)([AcquireChannel(0), AcquireChannel(1), AcquireChannel(2)],
+                                 [MemorySlot(2), MemorySlot(0), MemorySlot(1)]),
+            (10, cmd_def.get('measure', [0, 1, 2]).filter(channels=[MeasureChannel(0)])),
+            (10, Acquire(duration=10)([AcquireChannel(0), AcquireChannel(1), AcquireChannel(2)],
+                                      [MemorySlot(4), MemorySlot(0), MemorySlot(1)])))
+        self.assertEqual(sched.instructions, expected.instructions)