Repository: Qiskit/qiskit

Issue:
Allow the transpiler to find gate durations from calibrations
<!-- ⚠️ If you do not respect this template, your issue will be closed -->
<!-- ⚠️ Make sure to browse the opened and closed issues -->

### Information

- **Qiskit Terra version**:
- **Python version**:
- **Operating system**:

### What is the current behavior?

The `scheduling_method` of the transpiler fails when gate durations are determined by provided gate calibrations.

### Steps to reproduce the problem

Make a program with a calibration. (This is a brand new feature!)
```
from qiskit import pulse, QuantumCircuit

prog = QuantumCircuit(1, 1)
qc.rx(np.pi / 2, [0])
qc.x(0)
qc.delay(2, [0], unit='us')
qc.measure(0, 0)
    
qc.add_calibration('x', [0], pulse.Schedule(name='dummy_x'))  # The schedules can be empty for our purposes 
qc.add_calibration('rx', [0], pulse.Schedule(name='dummy_x90'), [np.pi / 2])
```

The times are fully specified, since each operation plays immediately after the last. The duration of the `rx` and `x` gates can be determined by the `duration` property of the provided schedule, in this silly example, the durations are `0`. The delay duration is 2 microseconds.

The following fails because the transpiler doesn't know how long `x` and `rx(pi/2)` are.
```
qc = transpile(qc, backend, scheduling_method='asap', dt=dt)
```

It works if you add it explicitly:
```
qc = transpile(qc, backend, scheduling_method='asap', dt=dt,
               instruction_durations=[('x', [qubit], pi_pulse.duration),
                                      ('rx', [qubit], x90_pulse.duration)]
```

### What is the expected behavior?

I should be able to transpile this program with only: `transpile(qc, backend, scheduling_method='asap', dt=dt)`. I shouldn't have to provide `instruction_durations`.

### Suggested solutions

The scheduling passes shouldn't have to be modified. I believe there is a place in `transpiler.py` where the `InstructionDurations` are determined from a backend, or from the function arguments. This part should be updated to *first* look for durations in the input `circuit.calibrations`. The calibrations table is in this format:

```
qc.calibrations = {
    '<gatename (ex: "rx")>': {
        {(<qubits tuple (ex: (0,))>, (<params tuple (ex: (np.pi/2,))>)): Schedule(...)},
        ...  # other cals for that gate, on different qubits or with different parameters
    },
    ...
}
```
It's important that the durations look to the calibrations first, since the cals overwrite whatever definition the backend would use.

This might be challenging for more than one circuit, since each circuit can have different durations. I would recommend getting this to work for one circuit first, or multiple circuits with the same calibrations.


Discussion:
Hey @lcapelluto, I am new to the repo and would like to try work on the issue.
@YosephKS Thank you! If you have questions, let me know. You can ask here, but I will be more responsive if you ask on Qiskit slack (`@lauren.capelluto`)

Resolving patch:
diff --git a/qiskit/compiler/transpile.py b/qiskit/compiler/transpile.py
--- a/qiskit/compiler/transpile.py
+++ b/qiskit/compiler/transpile.py
@@ -418,8 +418,7 @@ def _parse_transpile_args(circuits, backend,
     layout_method = _parse_layout_method(layout_method, num_circuits)
     routing_method = _parse_routing_method(routing_method, num_circuits)
     translation_method = _parse_translation_method(translation_method, num_circuits)
-    durations = _parse_instruction_durations(backend, instruction_durations, dt,
-                                             num_circuits)
+    durations = _parse_instruction_durations(backend, instruction_durations, dt, circuits)
     scheduling_method = _parse_scheduling_method(scheduling_method, num_circuits)
     seed_transpiler = _parse_seed_transpiler(seed_transpiler, num_circuits)
     optimization_level = _parse_optimization_level(optimization_level, num_circuits)
@@ -623,20 +622,36 @@ def _parse_scheduling_method(scheduling_method, num_circuits):
     return scheduling_method
 
 
-def _parse_instruction_durations(backend, inst_durations, dt, num_circuits):
-    durations = None
-    if inst_durations is None and backend:
+def _parse_instruction_durations(backend, inst_durations, dt, circuits):
+    """Create a list of ``InstructionDuration``s. If ``inst_durations`` is provided,
+    the backend will be ignored, otherwise, the durations will be populated from the
+    backend. If any circuits have gate calibrations, those calibration durations would
+    take precedence over backend durations, but be superceded by ``inst_duration``s.
+    """
+    if not inst_durations:
+        backend_durations = InstructionDurations()
         try:
             backend_durations = InstructionDurations.from_backend(backend)
         except AttributeError:
-            backend_durations = InstructionDurations()
-        durations = backend_durations.update(None, dt)
-    else:
-        durations = InstructionDurations(inst_durations,
-                                         dt or getattr(inst_durations, 'dt', None))
+            pass
+
+    durations = []
+    for circ in circuits:
+        circ_durations = InstructionDurations()
+        if not inst_durations:
+            circ_durations.update(backend_durations, dt or backend_durations.dt)
+
+        if circ.calibrations:
+            cal_durations = []
+            for gate, gate_cals in circ.calibrations.items():
+                for (qubits, _), schedule in gate_cals.items():
+                    cal_durations.append((gate, qubits, schedule.duration))
+            circ_durations.update(cal_durations, circ_durations.dt)
+
+        if inst_durations:
+            circ_durations.update(inst_durations, dt or getattr(inst_durations, 'dt', None))
 
-    if not isinstance(durations, list):
-        durations = [durations] * num_circuits
+        durations.append(circ_durations)
     return durations
 
 


Test patch:
diff --git a/test/python/compiler/test_transpiler.py b/test/python/compiler/test_transpiler.py
--- a/test/python/compiler/test_transpiler.py
+++ b/test/python/compiler/test_transpiler.py
@@ -936,6 +936,20 @@ def q0_rxt(tau):
         transpiled_circ = transpile(circ, FakeAlmaden())
         self.assertEqual(set(transpiled_circ.count_ops().keys()), {'rxt'})
 
+    def test_inst_durations_from_calibrations(self):
+        """Test that circuit calibrations can be used instead of explicitly
+        supplying inst_durations.
+        """
+        qc = QuantumCircuit(2)
+        qc.append(Gate('custom', 1, []), [0])
+
+        with pulse.build() as cal:
+            pulse.play(pulse.library.Gaussian(20, 1.0, 3.0), pulse.DriveChannel(0))
+        qc.add_calibration('custom', [0], cal)
+
+        out = transpile(qc, scheduling_method='alap')
+        self.assertEqual(out.duration, cal.duration)
+
     @data(0, 1, 2, 3)
     def test_circuit_with_delay(self, optimization_level):
         """Verify a circuit with delay can transpile to a scheduled circuit."""