Repository: Qiskit/qiskit

Issue:
Circuit drawers should indicate which gates have been overloaded with pulse gates
<!-- ⚠️ If you do not respect this template, your issue will be closed -->
<!-- ⚠️ Make sure to browse the opened and closed issues to confirm this idea does not exist. -->

### What is the expected enhancement?
The circuit drawers will have some indication whether or not a gate has been replaced with a custom pulse gate ("calibration")/.



Discussion:
In drawer terms, this means that the content of the box should be replaced-by/annotated-with "calibration" (or "cal", for short)?

For example, given the following situation from https://qiskit.org/documentation/tutorials/circuits_advanced/05_pulse_gates.html, is this the expected output?

```
circ.add_calibration('h', [0], h_q0)
```
![tutorials_circuits_advanced_05_pulse_gates_10_1](https://user-images.githubusercontent.com/766693/110928095-c5797780-8326-11eb-839c-a03987b3cfce.png)



 
I think this is a good start. In an ideal world this would be an interactive diagram that could be moused over to show the schedule. 
Ask the composer widget people. 
> In an ideal world this would be an interactive diagram that could be moused over to show the schedule.

While that might be possible on MPL (more exploration needed here), for sure it wont be possible for `'latex'` and `'text'`. 
Hello, I am interested in this issue, could I work on it?
It is possible in MPL using interactive mode in a notebook.  I had a demo of this functionality that I gave to @nkanazawa1989 some time ago (years?) that looked much like the current timeline plot, but then the actual pulse shapes would appear on mouseover.  Do it in JS would be a better solution though.

Resolving patch:
diff --git a/qiskit/visualization/circuit_visualization.py b/qiskit/visualization/circuit_visualization.py
--- a/qiskit/visualization/circuit_visualization.py
+++ b/qiskit/visualization/circuit_visualization.py
@@ -675,5 +675,6 @@ def _matplotlib_circuit_drawer(
         global_phase=global_phase,
         qregs=circuit.qregs,
         cregs=circuit.cregs,
+        calibrations=circuit.calibrations,
     )
     return qcd.draw(filename)
diff --git a/qiskit/visualization/matplotlib.py b/qiskit/visualization/matplotlib.py
--- a/qiskit/visualization/matplotlib.py
+++ b/qiskit/visualization/matplotlib.py
@@ -81,6 +81,7 @@ def __init__(
         global_phase=None,
         qregs=None,
         cregs=None,
+        calibrations=None,
     ):
 
         if not HAS_MATPLOTLIB:
@@ -149,6 +150,7 @@ def __init__(
         self._initial_state = initial_state
         self._cregbundle = cregbundle
         self._global_phase = global_phase
+        self._calibrations = calibrations
 
         self._fs = self._style["fs"]
         self._sfs = self._style["sfs"]
@@ -378,7 +380,7 @@ def _get_layer_widths(self):
 
                 base_type = None if not hasattr(op, "base_gate") else op.base_gate
                 gate_text, ctrl_text, raw_gate_text = get_gate_ctrl_text(
-                    op, "mpl", style=self._style
+                    op, "mpl", style=self._style, calibrations=self._calibrations
                 )
                 self._data[node]["gate_text"] = gate_text
                 self._data[node]["ctrl_text"] = ctrl_text
@@ -424,7 +426,7 @@ def _get_layer_widths(self):
                         self._get_text_width(gate_text + " ()", fontsize=self._sfs)
                         + raw_param_width
                     )
-                    gate_width = (raw_gate_width + 0.08) * 1.5
+                    gate_width = (raw_gate_width + 0.08) * 1.58
 
                 # otherwise, standard gate or multiqubit gate
                 else:
@@ -1039,7 +1041,7 @@ def _multiqubit_gate(self, node, xy=None):
 
         # Swap gate
         if isinstance(op, SwapGate):
-            self._swap(xy, self._data[node]["lc"])
+            self._swap(xy, node, self._data[node]["lc"])
             return
 
         # RZZ Gate
@@ -1157,7 +1159,7 @@ def _control_gate(self, node):
             self._gate(node, xy[num_ctrl_qubits:][0])
 
         elif isinstance(base_type, SwapGate):
-            self._swap(xy[num_ctrl_qubits:], self._data[node]["lc"])
+            self._swap(xy[num_ctrl_qubits:], node, self._data[node]["lc"])
 
         else:
             self._multiqubit_gate(node, xy[num_ctrl_qubits:])
@@ -1199,11 +1201,20 @@ def _ctrl_qubit(self, xy, fc=None, ec=None, tc=None, text="", text_top=None):
             zorder=PORDER_GATE,
         )
         self._ax.add_patch(box)
+
+        # adjust label height according to number of lines of text
+        label_padding = 0.7
+        if text is not None:
+            text_lines = text.count("\n")
+            if not text.endswith("(cal)\n"):
+                for _ in range(text_lines):
+                    label_padding += 0.3
+
         if text_top is None:
             return
 
         # display the control label at the top or bottom if there is one
-        ctrl_ypos = ypos + 0.7 * HIG if text_top else ypos - 0.3 * HIG
+        ctrl_ypos = ypos + label_padding * HIG if text_top else ypos - 0.3 * HIG
         self._ax.text(
             xpos,
             ctrl_ypos,
@@ -1271,15 +1282,32 @@ def _symmetric_gate(self, node, base_type):
             self._ctrl_qubit(xy[num_ctrl_qubits], fc=ec, ec=ec, tc=tc)
             if not isinstance(base_type, (U1Gate, PhaseGate)):
                 self._ctrl_qubit(xy[num_ctrl_qubits + 1], fc=ec, ec=ec, tc=tc)
+
             self._sidetext(node, qubit_b, tc=tc, text=f"{gate_text} ({self._data[node]['param']})")
             self._line(qubit_b, qubit_t, lc=lc)
 
-    def _swap(self, xy, color=None):
+    def _swap(self, xy, node, color=None):
         """Draw a Swap gate"""
         self._swap_cross(xy[0], color=color)
         self._swap_cross(xy[1], color=color)
         self._line(xy[0], xy[1], lc=color)
 
+        # add calibration text
+        gate_text = self._data[node]["gate_text"].split("\n")[-1]
+        if self._data[node]["raw_gate_text"] in self._calibrations:
+            xpos, ypos = xy[0]
+            self._ax.text(
+                xpos,
+                ypos + 0.7 * HIG,
+                gate_text,
+                ha="center",
+                va="top",
+                fontsize=self._style["sfs"],
+                color=self._style["tc"],
+                clip_on=True,
+                zorder=PORDER_TEXT,
+            )
+
     def _swap_cross(self, xy, color=None):
         """Draw the Swap cross symbol"""
         xpos, ypos = xy
diff --git a/qiskit/visualization/utils.py b/qiskit/visualization/utils.py
--- a/qiskit/visualization/utils.py
+++ b/qiskit/visualization/utils.py
@@ -48,7 +48,7 @@
     HAS_PYLATEX = False
 
 
-def get_gate_ctrl_text(op, drawer, style=None):
+def get_gate_ctrl_text(op, drawer, style=None, calibrations=None):
     """Load the gate_text and ctrl_text strings based on names and labels"""
     op_label = getattr(op, "label", None)
     op_type = type(op)
@@ -111,6 +111,13 @@ def get_gate_ctrl_text(op, drawer, style=None):
     ):
         gate_text = gate_text.capitalize()
 
+    if drawer == "mpl" and op.name in calibrations:
+        if isinstance(op, ControlledGate):
+            ctrl_text = "" if ctrl_text is None else ctrl_text
+            ctrl_text = "(cal)\n" + ctrl_text
+        else:
+            gate_text = gate_text + "\n(cal)"
+
     return gate_text, ctrl_text, raw_gate_text
 
 


Test patch:
diff --git a/test/ipynb/mpl/circuit/references/calibrations.png b/test/ipynb/mpl/circuit/references/calibrations.png
new file mode 100644
Binary files /dev/null and b/test/ipynb/mpl/circuit/references/calibrations.png differ
diff --git a/test/ipynb/mpl/circuit/references/calibrations_with_control_gates.png b/test/ipynb/mpl/circuit/references/calibrations_with_control_gates.png
new file mode 100644
Binary files /dev/null and b/test/ipynb/mpl/circuit/references/calibrations_with_control_gates.png differ
diff --git a/test/ipynb/mpl/circuit/references/calibrations_with_rzz_and_rxx.png b/test/ipynb/mpl/circuit/references/calibrations_with_rzz_and_rxx.png
new file mode 100644
Binary files /dev/null and b/test/ipynb/mpl/circuit/references/calibrations_with_rzz_and_rxx.png differ
diff --git a/test/ipynb/mpl/circuit/references/calibrations_with_swap_and_reset.png b/test/ipynb/mpl/circuit/references/calibrations_with_swap_and_reset.png
new file mode 100644
Binary files /dev/null and b/test/ipynb/mpl/circuit/references/calibrations_with_swap_and_reset.png differ
diff --git a/test/ipynb/mpl/circuit/test_circuit_matplotlib_drawer.py b/test/ipynb/mpl/circuit/test_circuit_matplotlib_drawer.py
--- a/test/ipynb/mpl/circuit/test_circuit_matplotlib_drawer.py
+++ b/test/ipynb/mpl/circuit/test_circuit_matplotlib_drawer.py
@@ -98,6 +98,105 @@ def test_empty_circuit(self):
 
         self.circuit_drawer(circuit, filename="empty_circut.png")
 
+    def test_calibrations(self):
+        """Test calibrations annotations
+        See https://github.com/Qiskit/qiskit-terra/issues/5920
+        """
+
+        circuit = QuantumCircuit(2, 2)
+        circuit.h(0)
+
+        from qiskit import pulse
+
+        with pulse.build(name="hadamard") as h_q0:
+            pulse.play(
+                pulse.library.Gaussian(duration=128, amp=0.1, sigma=16), pulse.DriveChannel(0)
+            )
+
+        circuit.add_calibration("h", [0], h_q0)
+
+        self.circuit_drawer(circuit, filename="calibrations.png")
+
+    def test_calibrations_with_control_gates(self):
+        """Test calibrations annotations
+        See https://github.com/Qiskit/qiskit-terra/issues/5920
+        """
+
+        circuit = QuantumCircuit(2, 2)
+        circuit.cx(0, 1)
+        circuit.ch(0, 1)
+
+        from qiskit import pulse
+
+        with pulse.build(name="cnot") as cx_q01:
+            pulse.play(
+                pulse.library.Gaussian(duration=128, amp=0.1, sigma=16), pulse.DriveChannel(1)
+            )
+
+        circuit.add_calibration("cx", [0, 1], cx_q01)
+
+        with pulse.build(name="ch") as ch_q01:
+            pulse.play(
+                pulse.library.Gaussian(duration=128, amp=0.1, sigma=16), pulse.DriveChannel(1)
+            )
+
+        circuit.add_calibration("ch", [0, 1], ch_q01)
+
+        self.circuit_drawer(circuit, filename="calibrations_with_control_gates.png")
+
+    def test_calibrations_with_swap_and_reset(self):
+        """Test calibrations annotations
+        See https://github.com/Qiskit/qiskit-terra/issues/5920
+        """
+
+        circuit = QuantumCircuit(2, 2)
+        circuit.swap(0, 1)
+        circuit.reset(0)
+
+        from qiskit import pulse
+
+        with pulse.build(name="swap") as swap_q01:
+            pulse.play(
+                pulse.library.Gaussian(duration=128, amp=0.1, sigma=16), pulse.DriveChannel(1)
+            )
+
+        circuit.add_calibration("swap", [0, 1], swap_q01)
+
+        with pulse.build(name="reset") as reset_q0:
+            pulse.play(
+                pulse.library.Gaussian(duration=128, amp=0.1, sigma=16), pulse.DriveChannel(1)
+            )
+
+        circuit.add_calibration("reset", [0], reset_q0)
+
+        self.circuit_drawer(circuit, filename="calibrations_with_swap_and_reset.png")
+
+    def test_calibrations_with_rzz_and_rxx(self):
+        """Test calibrations annotations
+        See https://github.com/Qiskit/qiskit-terra/issues/5920
+        """
+        circuit = QuantumCircuit(2, 2)
+        circuit.rzz(pi, 0, 1)
+        circuit.rxx(pi, 0, 1)
+
+        from qiskit import pulse
+
+        with pulse.build(name="rzz") as rzz_q01:
+            pulse.play(
+                pulse.library.Gaussian(duration=128, amp=0.1, sigma=16), pulse.DriveChannel(1)
+            )
+
+        circuit.add_calibration("rzz", [0, 1], rzz_q01)
+
+        with pulse.build(name="rxx") as rxx_q01:
+            pulse.play(
+                pulse.library.Gaussian(duration=128, amp=0.1, sigma=16), pulse.DriveChannel(1)
+            )
+
+        circuit.add_calibration("rxx", [0, 1], rxx_q01)
+
+        self.circuit_drawer(circuit, filename="calibrations_with_rzz_and_rxx.png")
+
     def test_no_ops(self):
         """Test circuit with no ops.
         See https://github.com/Qiskit/qiskit-terra/issues/5393"""