Repository: Qiskit/qiskit

Issue:
Routing pass does not account for calibrations when assessing gate direction
### Environment

- **Qiskit Terra version**: 0.23.2
- **Python version**: 3.10.9
- **Operating system**: Fedora Linux 37


### What is happening?

When transpiling a circuit with a custom gate with a calibration, the gate direction pass does not check the circuit calibrations for the gate. It only checks the target and looks at a special-cased gates.

### How can we reproduce the issue?

```python
from qiskit import QuantumCircuit, pulse, transpile
from qiskit.circuit.gate import Gate
from qiskit.circuit.library import CXGate
from qiskit.transpiler import Target


# Placeholder schedule because the schedule content does not matter
sched = pulse.ScheduleBlock()
# Custom target with one two qubit gate added so that the target coupling map is connected
target = Target(num_qubits=2)
target.add_instruction(CXGate(), properties={(0, 1): None})
gate = Gate("my_2q_gate", 2, [])

circ = QuantumCircuit(2)
circ.append(gate, (0, 1))
circ.add_calibration(gate, (0, 1), sched)

transpile(circ, target=target, optimization_level=0)
```

Running this code produces:

```
Traceback (most recent call last):          
  File "/reverse.py", line 16, in <module>                                                                                                                                                           
    transpile(circ, target=target, optimization_level=1)
  File "/lib/python3.10/site-packages/qiskit/compiler/transpiler.py", line 381, in transpile
    _serial_transpile_circuit(                          
  File "/lib/python3.10/site-packages/qiskit/compiler/transpiler.py", line 474, in _serial_transpile_circuit
    result = pass_manager.run(circuit, callback=callback, output_name=output_name)
  File "/lib/python3.10/site-packages/qiskit/transpiler/passmanager.py", line 528, in run
    return super().run(circuits, output_name, callback)                                                                
  File "/lib/python3.10/site-packages/qiskit/transpiler/passmanager.py", line 228, in run
    return self._run_single_circuit(circuits, output_name, callback)
  File "/lib/python3.10/site-packages/qiskit/transpiler/passmanager.py", line 283, in _run_single_circuit
    result = running_passmanager.run(circuit, output_name=output_name, callback=callback)
  File "/lib/python3.10/site-packages/qiskit/transpiler/runningpassmanager.py", line 125, in run
    dag = self._do_pass(pass_, dag, passset.options)                                                                   
  File "/lib/python3.10/site-packages/qiskit/transpiler/runningpassmanager.py", line 173, in _do_pass
    dag = self._run_this_pass(pass_, dag)                                                                              
  File "/lib/python3.10/site-packages/qiskit/transpiler/runningpassmanager.py", line 202, in _run_this_pass
    new_dag = pass_.run(dag)                       
  File "/lib/python3.10/site-packages/qiskit/transpiler/passes/utils/gate_direction.py", line 300, in run
    return self._run_target(dag, layout_map)
  File "/lib/python3.10/site-packages/qiskit/transpiler/passes/utils/gate_direction.py", line 270, in _run_target
    raise TranspilerError(
qiskit.transpiler.exceptions.TranspilerError: "Flipping of gate direction is only supported for ['cx', 'cz', 'ecr'] at this time, not 'my_2q_gate'."
```

### What should happen?

Transpilation should run without error. It should leave the circuit unmodified.

### Any suggestions?

In `gate_direction.py`, right before/after the target is checked in `_run_target`, the circuit's calibrations should be checked for the gate and no exception should be raised if the calibration is there.

Additionally, there may be an additional bug here because the error message is confusingly about flipping the gate direction when the calibration has the right direction present. A "gate not found" type of message would be more appropriate. I wonder if there are other ways to trigger the "flipping the gate" message where the issue is that the gate was not found at all.

This issue was first noticed in the [qiskit-dynamics PR](https://github.com/Qiskit/qiskit-dynamics/pull/193#discussion_r1126753960) for the DynamicsBackend tutorial. In most cases, we are working only with gates already in the Target so this issue does not come up.


Discussion:

Resolving patch:
diff --git a/qiskit/transpiler/passes/utils/gate_direction.py b/qiskit/transpiler/passes/utils/gate_direction.py
--- a/qiskit/transpiler/passes/utils/gate_direction.py
+++ b/qiskit/transpiler/passes/utils/gate_direction.py
@@ -166,6 +166,8 @@ def _run_coupling_map(self, dag, wire_map, edges=None):
                 continue
             if len(node.qargs) != 2:
                 continue
+            if dag.has_calibration_for(node):
+                continue
             qargs = (wire_map[node.qargs[0]], wire_map[node.qargs[1]])
             if qargs not in edges and (qargs[1], qargs[0]) not in edges:
                 raise TranspilerError(
@@ -209,6 +211,8 @@ def _run_target(self, dag, wire_map):
                 continue
             if len(node.qargs) != 2:
                 continue
+            if dag.has_calibration_for(node):
+                continue
             qargs = (wire_map[node.qargs[0]], wire_map[node.qargs[1]])
             swapped = (qargs[1], qargs[0])
             if node.name in self._static_replacements:


Test patch:
diff --git a/test/python/transpiler/test_gate_direction.py b/test/python/transpiler/test_gate_direction.py
--- a/test/python/transpiler/test_gate_direction.py
+++ b/test/python/transpiler/test_gate_direction.py
@@ -17,8 +17,8 @@
 
 import ddt
 
-from qiskit import ClassicalRegister, QuantumRegister, QuantumCircuit
-from qiskit.circuit import Parameter
+from qiskit import ClassicalRegister, QuantumRegister, QuantumCircuit, pulse
+from qiskit.circuit import Parameter, Gate
 from qiskit.circuit.library import (
     CXGate,
     CZGate,
@@ -439,6 +439,33 @@ def test_target_control_flow(self):
         pass_ = GateDirection(None, target)
         self.assertEqual(pass_(circuit), expected)
 
+    def test_allows_calibrated_gates_coupling_map(self):
+        """Test that the gate direction pass allows a gate that's got a calibration to pass through
+        without error."""
+        cm = CouplingMap([(1, 0)])
+
+        gate = Gate("my_2q_gate", 2, [])
+        circuit = QuantumCircuit(2)
+        circuit.append(gate, (0, 1))
+        circuit.add_calibration(gate, (0, 1), pulse.ScheduleBlock())
+
+        pass_ = GateDirection(cm)
+        self.assertEqual(pass_(circuit), circuit)
+
+    def test_allows_calibrated_gates_target(self):
+        """Test that the gate direction pass allows a gate that's got a calibration to pass through
+        without error."""
+        target = Target(num_qubits=2)
+        target.add_instruction(CXGate(), properties={(0, 1): None})
+
+        gate = Gate("my_2q_gate", 2, [])
+        circuit = QuantumCircuit(2)
+        circuit.append(gate, (0, 1))
+        circuit.add_calibration(gate, (0, 1), pulse.ScheduleBlock())
+
+        pass_ = GateDirection(None, target)
+        self.assertEqual(pass_(circuit), circuit)
+
 
 if __name__ == "__main__":
     unittest.main()