Repository: Qiskit/qiskit

Issue:
ALAP scheduling fails when a circuit with custom instruction is supplied
<!-- ⚠️ If you do not respect this template, your issue will be closed -->
<!-- ⚠️ Make sure to browse the opened and closed issues -->
### What is the current behavior?
Transpiling with `scheduling_method="alap"` but without `backend` fails when a circuit with custom instructions is supplied.
This is due to the instruction name mismatch. It occurs when `ALAPSchedule` pass calls `reverse_ops()`, which changes the instruction name. (So this error does not happen if we use ASAP scheduler.)

### Steps to reproduce the problem
```
bell = QuantumCircuit(2, name="bell")
bell.h(0)
bell.cx(0, 1)
qc = QuantumCircuit(2)
qc.delay(500, 1)
qc.append(bell.to_instruction(), [0, 1])
scheduled = transpile(qc,
                      scheduling_method='alap',
                      instruction_durations=[('bell', [0, 1], 1000)])

==> qiskit.transpiler.exceptions.TranspilerError: 'Duration of bell_reverse on qubits [0, 1] is not found.'
```

### What is the expected behavior?
The above circuit should be scheduled successfully.

### Suggested solutions
We may have three options:
1. don't add "_reverse" to the name of the reversed instruction
2. patch the `instruction_durations` with the new .._reverse gate duration as well
3. rewrite the ALAP scheduler without using `reverse_ops()`




Discussion:

Resolving patch:
diff --git a/qiskit/transpiler/passes/scheduling/alap.py b/qiskit/transpiler/passes/scheduling/alap.py
--- a/qiskit/transpiler/passes/scheduling/alap.py
+++ b/qiskit/transpiler/passes/scheduling/alap.py
@@ -11,10 +11,13 @@
 # that they have been altered from the originals.
 
 """ALAP Scheduling."""
+from collections import defaultdict
+from typing import List
 
+from qiskit.circuit.delay import Delay
+from qiskit.dagcircuit import DAGCircuit
 from qiskit.transpiler.basepasses import TransformationPass
 from qiskit.transpiler.exceptions import TranspilerError
-from qiskit.transpiler.passes.scheduling.asap import ASAPSchedule
 
 
 class ALAPSchedule(TransformationPass):
@@ -27,7 +30,7 @@ def __init__(self, durations):
             durations (InstructionDurations): Durations of instructions to be used in scheduling
         """
         super().__init__()
-        self._asap = ASAPSchedule(durations)
+        self.durations = durations
 
     def run(self, dag, time_unit=None):  # pylint: disable=arguments-differ
         """Run the ALAPSchedule pass on `dag`.
@@ -48,9 +51,42 @@ def run(self, dag, time_unit=None):  # pylint: disable=arguments-differ
         if not time_unit:
             time_unit = self.property_set['time_unit']
 
-        new_dag = dag.reverse_ops()
-        new_dag = self._asap.run(new_dag, time_unit)
-        new_dag = new_dag.reverse_ops()
+        new_dag = DAGCircuit()
+        for qreg in dag.qregs.values():
+            new_dag.add_qreg(qreg)
+        for creg in dag.cregs.values():
+            new_dag.add_creg(creg)
+
+        qubit_time_available = defaultdict(int)
+
+        def pad_with_delays(qubits: List[int], until, unit) -> None:
+            """Pad idle time-slots in ``qubits`` with delays in ``unit`` until ``until``."""
+            for q in qubits:
+                if qubit_time_available[q] < until:
+                    idle_duration = until - qubit_time_available[q]
+                    new_dag.apply_operation_front(Delay(idle_duration, unit), [q], [])
+
+        for node in reversed(list(dag.topological_op_nodes())):
+            start_time = max(qubit_time_available[q] for q in node.qargs)
+            pad_with_delays(node.qargs, until=start_time, unit=time_unit)
+
+            new_node = new_dag.apply_operation_front(node.op, node.qargs, node.cargs,
+                                                     node.condition)
+            duration = self.durations.get(node.op, node.qargs, unit=time_unit)
+            # set duration for each instruction (tricky but necessary)
+            new_node.op.duration = duration
+            new_node.op.unit = time_unit
+
+            stop_time = start_time + duration
+            # update time table
+            for q in node.qargs:
+                qubit_time_available[q] = stop_time
+
+        working_qubits = qubit_time_available.keys()
+        circuit_duration = max(qubit_time_available[q] for q in working_qubits)
+        pad_with_delays(new_dag.qubits, until=circuit_duration, unit=time_unit)
 
         new_dag.name = dag.name
+        new_dag.duration = circuit_duration
+        new_dag.unit = time_unit
         return new_dag
diff --git a/qiskit/transpiler/passes/scheduling/asap.py b/qiskit/transpiler/passes/scheduling/asap.py
--- a/qiskit/transpiler/passes/scheduling/asap.py
+++ b/qiskit/transpiler/passes/scheduling/asap.py
@@ -50,6 +50,7 @@ def run(self, dag, time_unit=None):  # pylint: disable=arguments-differ
 
         if not time_unit:
             time_unit = self.property_set['time_unit']
+
         new_dag = DAGCircuit()
         for qreg in dag.qregs.values():
             new_dag.add_qreg(qreg)


Test patch:
diff --git a/test/python/circuit/test_scheduled_circuit.py b/test/python/circuit/test_scheduled_circuit.py
--- a/test/python/circuit/test_scheduled_circuit.py
+++ b/test/python/circuit/test_scheduled_circuit.py
@@ -151,6 +151,19 @@ def test_transpile_delay_circuit_without_backend(self):
                               instruction_durations=[('h', 0, 200), ('cx', [0, 1], 700)])
         self.assertEqual(scheduled.duration, 1200)
 
+    def test_transpile_circuit_with_custom_instruction(self):
+        """See: https://github.com/Qiskit/qiskit-terra/issues/5154"""
+        bell = QuantumCircuit(2, name="bell")
+        bell.h(0)
+        bell.cx(0, 1)
+        qc = QuantumCircuit(2)
+        qc.delay(500, 1)
+        qc.append(bell.to_instruction(), [0, 1])
+        scheduled = transpile(qc,
+                              scheduling_method='alap',
+                              instruction_durations=[('bell', [0, 1], 1000)])
+        self.assertEqual(scheduled.duration, 1500)
+
     def test_transpile_delay_circuit_without_scheduling_method_as_normal_circuit(self):
         qc = QuantumCircuit(2)
         qc.h(0)
diff --git a/test/python/transpiler/test_scheduling_pass.py b/test/python/transpiler/test_scheduling_pass.py
new file mode 100644
--- /dev/null
+++ b/test/python/transpiler/test_scheduling_pass.py
@@ -0,0 +1,52 @@
+# This code is part of Qiskit.
+#
+# (C) Copyright IBM 2020.
+#
+# 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 the Scheduling passes"""
+
+import unittest
+
+from qiskit import QuantumCircuit
+from qiskit.converters import circuit_to_dag
+from qiskit.transpiler.instruction_durations import InstructionDurations
+from qiskit.transpiler.passes import ASAPSchedule, ALAPSchedule
+
+from qiskit.test import QiskitTestCase
+
+
+class TestSchedulingPass(QiskitTestCase):
+    """Tests the Scheduling passes"""
+
+    def test_alap_agree_with_reverse_asap_reverse(self):
+        """Test if ALAP schedule agrees with doubly-reversed ASAP schedule.
+        """
+        qc = QuantumCircuit(2)
+        qc.h(0)
+        qc.delay(500, 1)
+        qc.cx(0, 1)
+        qc.measure_all()
+
+        dag = circuit_to_dag(qc)
+        durations = InstructionDurations([('h', 0, 200), ('cx', [0, 1], 700),
+                                          ('measure', None, 1000)])
+
+        alap_dag = ALAPSchedule(durations).run(dag, time_unit="dt")
+
+        new_dag = dag.reverse_ops()
+        new_dag = ASAPSchedule(durations).run(new_dag, time_unit="dt")
+        new_dag = new_dag.reverse_ops()
+        new_dag.name = dag.name
+
+        self.assertEqual(alap_dag, new_dag)
+
+
+if __name__ == '__main__':
+    unittest.main()