Repository: Qiskit/qiskit

Issue:
mpl circuit drawer requires excessive load time
<!-- ⚠️ 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**: Main
- **Python version**: 3.8
- **Operating system**: Ubuntu

### What is the current behavior?

When a text width calculation method was first added to the mpl circuit drawer, the method was called whenever it was needed. This has caused excessive load times due to repetitive method calls for medium to large circuits (100-1000+ gates), especially for gates with parameters.

### Steps to reproduce the problem



### What is the expected behavior?



### Suggested solutions

Caching the text widths will eliminate the repetitive calls.



Discussion:

Resolving patch:
diff --git a/qiskit/visualization/matplotlib.py b/qiskit/visualization/matplotlib.py
--- a/qiskit/visualization/matplotlib.py
+++ b/qiskit/visualization/matplotlib.py
@@ -200,6 +200,7 @@ def __init__(
         self._style["sfs"] *= self._scale
         self._lwidth15 = 1.5 * self._scale
         self._lwidth2 = 2.0 * self._scale
+        self._gate_width = {}
 
         # default is to use character table for text width,
         # but get_renderer will work with some mpl backends
@@ -535,7 +536,7 @@ def _get_colors(self, op):
         return fc, ec, gt, self._style["tc"], sc, lc
 
     def _multiqubit_gate(
-        self, xy, c_xy=None, fc=None, ec=None, gt=None, sc=None, text="", subtext=""
+        self, op, xy, c_xy=None, fc=None, ec=None, gt=None, sc=None, text="", subtext=""
     ):
         xpos = min([x[0] for x in xy])
         ypos = min([y[1] for y in xy])
@@ -547,10 +548,7 @@ def _multiqubit_gate(
         fs = self._style["fs"]
         sfs = self._style["sfs"]
 
-        # added .21 is for qubit numbers on the left side
-        text_width = self._get_text_width(text, fs) + 0.21
-        sub_width = self._get_text_width(subtext, sfs, param=True) + 0.21
-        wid = max((text_width, sub_width, WID))
+        wid = max(self._gate_width[op] + 0.21, WID)
 
         qubit_span = abs(ypos) - abs(ypos_max) + 1
         height = HIG + (qubit_span - 1)
@@ -630,14 +628,12 @@ def _multiqubit_gate(
                     wrap=True,
                 )
 
-    def _gate(self, xy, fc=None, ec=None, gt=None, sc=None, text="", subtext=""):
+    def _gate(self, op, xy, fc=None, ec=None, gt=None, sc=None, text="", subtext=""):
         xpos, ypos = xy
         fs = self._style["fs"]
         sfs = self._style["sfs"]
 
-        text_width = self._get_text_width(text, fs)
-        sub_width = self._get_text_width(subtext, sfs, param=True)
-        wid = max((text_width, sub_width, WID))
+        wid = max(self._gate_width[op], WID)
 
         box = self.patches_mod.Rectangle(
             xy=(xpos - 0.5 * wid, ypos - 0.5 * HIG),
@@ -687,11 +683,11 @@ def _gate(self, xy, fc=None, ec=None, gt=None, sc=None, text="", subtext=""):
                     zorder=PORDER_TEXT,
                 )
 
-    def _sidetext(self, xy, tc=None, text=""):
+    def _sidetext(self, op, xy, tc=None, text=""):
         xpos, ypos = xy
 
         # 0.11 = the initial gap, add 1/2 text width to place on the right
-        text_width = self._get_text_width(text, self._style["sfs"])
+        text_width = self._gate_width[op]
         xp = xpos + 0.11 + text_width / 2
         self._ax.text(
             xp,
@@ -741,12 +737,12 @@ def _line(self, xy0, xy1, lc=None, ls=None, zorder=PORDER_LINE):
                 zorder=zorder,
             )
 
-    def _measure(self, qxy, cxy, cid, fc=None, ec=None, gt=None, sc=None):
+    def _measure(self, op, qxy, cxy, cid, fc=None, ec=None, gt=None, sc=None):
         qx, qy = qxy
         cx, cy = cxy
 
         # draw gate box
-        self._gate(qxy, fc=fc, ec=ec, gt=gt, sc=sc)
+        self._gate(op, qxy, fc=fc, ec=ec, gt=gt, sc=sc)
 
         # add measure symbol
         arc = self.patches_mod.Arc(
@@ -1216,10 +1212,13 @@ def _draw_ops(self, verbose=False):
         sfs = self._style["sfs"]
         for layer in self._ops:
             widest_box = 0.0
+            self._gate_width = {}
             #
             # compute the layer_width for this layer
             #
             for op in layer:
+                self._gate_width[op] = WID
+
                 if op.op._directive or op.name == "measure":
                     continue
 
@@ -1247,10 +1246,10 @@ def _draw_ops(self, verbose=False):
                     param = self._param_parse(op.op.params)
                     if op.name == "initialize":
                         param = "[%s]" % param
-                    param = "${}$".format(param)
-                    param_width = self._get_text_width(param, fontsize=sfs, param=True) + 0.08
+                    raw_param_width = self._get_text_width(param, fontsize=sfs, param=True)
+                    param_width = raw_param_width + 0.08
                 else:
-                    param_width = 0.0
+                    param_width = raw_param_width = 0.0
 
                 if op.name == "rzz" or base_name in ["u1", "p", "rzz"]:
                     if base_name == "u1":
@@ -1259,11 +1258,13 @@ def _draw_ops(self, verbose=False):
                         tname = "P"
                     else:
                         tname = "ZZ"
-                    gate_width = (
-                        self._get_text_width(tname + " ()", fontsize=sfs) + param_width
-                    ) * 1.5
+                    raw_gate_width = (
+                        self._get_text_width(tname + " ()", fontsize=sfs) + raw_param_width
+                    )
+                    gate_width = (raw_gate_width + 0.08) * 1.5
                 else:
-                    gate_width = self._get_text_width(gate_text, fontsize=fs) + 0.10
+                    raw_gate_width = self._get_text_width(gate_text, fontsize=fs)
+                    gate_width = raw_gate_width + 0.10
                     # add .21 for the qubit numbers on the left of the multibit gates
                     if op.name not in _standard_1q_gates and base_name not in _standard_1q_gates:
                         gate_width += 0.21
@@ -1271,6 +1272,7 @@ def _draw_ops(self, verbose=False):
                 box_width = max(gate_width, ctrl_width, param_width, WID)
                 if box_width > widest_box:
                     widest_box = box_width
+                self._gate_width[op] = max(raw_gate_width, raw_param_width)
 
             layer_width = int(widest_box) + 1
             this_anc = prev_anc + 1
@@ -1391,7 +1393,7 @@ def _draw_ops(self, verbose=False):
                 #
                 if op.name == "measure":
                     vv = self._clbit_dict[c_idxs[0]]["index"]
-                    self._measure(q_xy[0], c_xy[0], vv, fc=fc, ec=ec, gt=gt, sc=sc)
+                    self._measure(op, q_xy[0], c_xy[0], vv, fc=fc, ec=ec, gt=gt, sc=sc)
 
                 elif op.op._directive:
                     _barriers = {"coord": [], "group": []}
@@ -1406,24 +1408,27 @@ def _draw_ops(self, verbose=False):
                 elif op.name == "initialize":
                     vec = "$[{}]$".format(param.replace("$", ""))
                     if len(q_xy) == 1:
-                        self._gate(q_xy[0], fc=fc, ec=ec, gt=gt, sc=sc, text=gate_text, subtext=vec)
+                        self._gate(
+                            op, q_xy[0], fc=fc, ec=ec, gt=gt, sc=sc, text=gate_text, subtext=vec
+                        )
                     else:
                         self._multiqubit_gate(
-                            q_xy, fc=fc, ec=ec, gt=gt, sc=sc, text=gate_text, subtext=vec
+                            op, q_xy, fc=fc, ec=ec, gt=gt, sc=sc, text=gate_text, subtext=vec
                         )
+
                 elif isinstance(op.op, Delay):
                     param_text = "(%s)" % param
                     if op.op.unit:
                         param_text += "[%s]" % op.op.unit
                     self._gate(
-                        q_xy[0], fc=fc, ec=ec, gt=gt, sc=sc, text=gate_text, subtext=param_text
+                        op, q_xy[0], fc=fc, ec=ec, gt=gt, sc=sc, text=gate_text, subtext=param_text
                     )
                 #
                 # draw single qubit gates
                 #
                 elif len(q_xy) == 1 and not op.cargs:
                     self._gate(
-                        q_xy[0], fc=fc, ec=ec, gt=gt, sc=sc, text=gate_text, subtext=str(param)
+                        op, q_xy[0], fc=fc, ec=ec, gt=gt, sc=sc, text=gate_text, subtext=str(param)
                     )
                 #
                 # draw controlled and special gates
@@ -1466,7 +1471,7 @@ def _draw_ops(self, verbose=False):
                     else:
                         stext = "ZZ"
                     self._sidetext(
-                        qubit_b, tc=tc, text="{}".format(stext) + " " + "({})".format(param)
+                        op, qubit_b, tc=tc, text="{}".format(stext) + " " + "({})".format(param)
                     )
                     self._line(qubit_b, qubit_t, lc=lc)
 
@@ -1512,6 +1517,7 @@ def _draw_ops(self, verbose=False):
                         self._x_tgt_qubit(q_xy[num_ctrl_qubits], ec=ec, ac=tgt)
                     elif num_qargs == 1:
                         self._gate(
+                            op,
                             q_xy[num_ctrl_qubits],
                             fc=fc,
                             ec=ec,
@@ -1522,6 +1528,7 @@ def _draw_ops(self, verbose=False):
                         )
                     else:
                         self._multiqubit_gate(
+                            op,
                             q_xy[num_ctrl_qubits:],
                             fc=fc,
                             ec=ec,
@@ -1534,6 +1541,7 @@ def _draw_ops(self, verbose=False):
                 # draw multi-qubit gate as final default
                 else:
                     self._multiqubit_gate(
+                        op,
                         q_xy,
                         c_xy,
                         fc=fc,


Test patch: