Repository: Qiskit/qiskit

Issue:
Matplotlib circuit drawer is unconditionally using latex mode for gate names
<!-- ⚠️ 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**: Master
- **Python version**: Any
- **Operating system**: Any

### What is the current behavior?

The matplotlib circuit drawer is unconditionally using latex mode for all gate names. This is a change in behavior from previous releases and can lead to weird results. For example, having a gate named `my_gate^22` will result in this being drawn:

![latex_mode](https://user-images.githubusercontent.com/2447371/86925246-9a3aea80-c0fe-11ea-8650-b4e830495d4d.png)

but the string should

`my_gate^22`

like: 

![latex_mode](https://user-images.githubusercontent.com/2447371/86926234-d1f66200-c0ff-11ea-9fee-f11a187c04cf.png)

but obviously without the rendering bug (I used an older qiskit version to show the previous behavior which had other bugs)

### Steps to reproduce the problem

Run:

```
from qiskit.circuit import QuantumCircuit
from qiskit.circuit import Instruction
                  
qc = QuantumCircuit(2)
qc.append(Instruction('my_gate^22', 1, 0, []), [0], [])
qc.draw('mpl', filename='/tmp/latex_mode.png')
```

### What is the expected behavior?

The gate title text is not in latex mode by default anymore, the documented behavior is to use `$` around the latex portions of the string.

### Suggested solutions

Fix the default change.



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
@@ -20,7 +20,9 @@
 import itertools
 import json
 import logging
+import re
 from warnings import warn
+
 import numpy as np
 
 try:
@@ -32,6 +34,13 @@
 except ImportError:
     HAS_MATPLOTLIB = False
 
+try:
+    from pylatexenc.latex2text import LatexNodes2Text
+
+    HAS_PYLATEX = True
+except ImportError:
+    HAS_PYLATEX = False
+
 from qiskit.circuit import ControlledGate
 from qiskit.visualization.qcstyle import DefaultStyle, BWStyle
 from qiskit import user_config
@@ -113,6 +122,10 @@ def __init__(self, qregs, cregs, ops,
             raise ImportError('The class MatplotlibDrawer needs matplotlib. '
                               'To install, run "pip install matplotlib".')
 
+        if not HAS_PYLATEX:
+            raise ImportError('The class MatplotlibDrawer needs pylatexenc. '
+                              'to install, run "pip install pylatexenc".')
+
         self._ast = None
         self._scale = 1.0 if scale is None else scale
         self._creg = []
@@ -198,9 +211,6 @@ def __init__(self, qregs, cregs, ops,
 
         # these char arrays are for finding text_width when not
         # using get_renderer method for the matplotlib backend
-        self._latex_chars = ('$', '{', '}', '_', '\\left', '\\right',
-                             '\\dagger', '\\rangle')
-        self._latex_chars1 = ('\\mapsto', '\\pi', '\\;')
         self._char_list = {' ': (0.0958, 0.0583), '!': (0.1208, 0.0729), '"': (0.1396, 0.0875),
                            '#': (0.2521, 0.1562), '$': (0.1917, 0.1167), '%': (0.2854, 0.1771),
                            '&': (0.2333, 0.1458), "'": (0.0833, 0.0521), '(': (0.1167, 0.0729),
@@ -234,6 +244,8 @@ def __init__(self, qregs, cregs, ops,
                            'z': (0.1562, 0.0979), '{': (0.1917, 0.1188), '|': (0.1, 0.0604),
                            '}': (0.1896, 0.1188)}
 
+        self._mathmode_regex = re.compile(r"(?<!\\)\$(.*)(?<!\\)\$")
+
     def _registers(self, creg, qreg):
         self._creg = []
         for r in creg:
@@ -255,12 +267,21 @@ def _get_text_width(self, text, fontsize):
             t = plt.text(0.5, 0.5, text, fontsize=fontsize)
             return t.get_window_extent(renderer=self.renderer).width / 60.0
         else:
-            # if not using a get_renderer method, first remove
-            # any latex chars before getting width
-            for t in self._latex_chars1:
-                text = text.replace(t, 'r')
-            for t in self._latex_chars:
-                text = text.replace(t, '')
+            math_mode_match = self._mathmode_regex.search(text)
+            num_underscores = 0
+            num_carets = 0
+            if math_mode_match:
+                math_mode_text = math_mode_match.group(1)
+                num_underscores = math_mode_text.count('_')
+                num_carets = math_mode_text.count('^')
+            text = LatexNodes2Text().latex_to_text(text)
+            # If there are subscripts or superscripts in mathtext string
+            # we need to account for that spacing by manually removing
+            # from text string for text length
+            if num_underscores:
+                text = text.replace('_', '', num_underscores)
+            if num_carets:
+                text = text.replace('^', '', num_carets)
 
             f = 0 if fontsize == self._style.fs else 1
             sum_text = 0.0
@@ -285,9 +306,6 @@ def param_parse(self, v):
                 param_parts[i] = '$-$' + param_parts[i][1:]
 
         param_parts = ', '.join(param_parts)
-        # remove $'s since "${}$".format will add them back on the outside
-        param_parts = param_parts.replace('$', '')
-        param_parts = param_parts.replace('-', u'\u02d7')
         return param_parts
 
     def _get_gate_ctrl_text(self, op):
@@ -309,18 +327,12 @@ def _get_gate_ctrl_text(self, op):
             gate_text = op.name
 
         if gate_text in self._style.disptex:
-            gate_text = "${}$".format(self._style.disptex[gate_text])
+            gate_text = "{}".format(self._style.disptex[gate_text])
         else:
-            gate_text = "${}$".format(gate_text[0].upper() + gate_text[1:])
+            gate_text = "{}".format(gate_text[0].upper() + gate_text[1:])
 
-        # mathtext .format removes spaces so add them back and it changes
-        # hyphen to wide minus sign, so use unicode hyphen to prevent that
-        gate_text = gate_text.replace(' ', '\\;')
-        gate_text = gate_text.replace('-', u'\u02d7')
         if ctrl_text:
-            ctrl_text = "${}$".format(ctrl_text[0].upper() + ctrl_text[1:])
-            ctrl_text = ctrl_text.replace(' ', '\\;')
-            ctrl_text = ctrl_text.replace('-', u'\u02d7')
+            ctrl_text = "{}".format(ctrl_text[0].upper() + ctrl_text[1:])
         return gate_text, ctrl_text
 
     def _get_colors(self, op):
@@ -613,7 +625,7 @@ def _fix_double_script(label):
                     label = _fix_double_script(label) + initial_qbit
                     text_width = self._get_text_width(label, self._style.fs)
             else:
-                label = '${name}$'.format(name=reg.register.name)
+                label = '{name}'.format(name=reg.register.name)
                 label = _fix_double_script(label) + initial_qbit
                 text_width = self._get_text_width(label, self._style.fs)
 
@@ -635,7 +647,7 @@ def _fix_double_script(label):
             for ii, (reg, nreg) in enumerate(itertools.zip_longest(self._creg, n_creg)):
                 pos = y_off - idx
                 if self.cregbundle:
-                    label = '${}$'.format(reg.register.name)
+                    label = '{}'.format(reg.register.name)
                     label = _fix_double_script(label) + initial_cbit
                     text_width = self._get_text_width(reg.register.name, self._style.fs) * 1.15
                     if text_width > longest_label_width:
@@ -757,7 +769,7 @@ def _draw_ops(self, verbose=False):
 
                 if op.name == 'cu1' or op.name == 'rzz' or base_name == 'rzz':
                     tname = 'U1' if op.name == 'cu1' else 'zz'
-                    gate_width = (self._get_text_width(tname + ' ()$$',
+                    gate_width = (self._get_text_width(tname + ' ()',
                                                        fontsize=self._style.sfs)
                                   + param_width) * 1.5
                 else:
@@ -824,7 +836,7 @@ def _draw_ops(self, verbose=False):
                 # load param
                 if (op.type == 'op' and hasattr(op.op, 'params') and len(op.op.params) > 0
                         and not any([isinstance(param, np.ndarray) for param in op.op.params])):
-                    param = "${}$".format(self.param_parse(op.op.params))
+                    param = "{}".format(self.param_parse(op.op.params))
                 else:
                     param = ''
 
@@ -920,7 +932,7 @@ def _draw_ops(self, verbose=False):
                         self._ctrl_qubit(q_xy[num_ctrl_qubits + 1], fc=ec, ec=ec, tc=tc)
                     stext = self._style.disptex['u1'] if op.name == 'cu1' else 'zz'
                     self._sidetext(qreg_b, tc=tc,
-                                   text='${}$'.format(stext) + ' ' + '({})'.format(param))
+                                   text='{}'.format(stext) + ' ' + '({})'.format(param))
                     self._line(qreg_b, qreg_t, lc=lc)
 
                 # swap gate
diff --git a/qiskit/visualization/qcstyle.py b/qiskit/visualization/qcstyle.py
--- a/qiskit/visualization/qcstyle.py
+++ b/qiskit/visualization/qcstyle.py
@@ -47,32 +47,33 @@ def __init__(self):
         self.sfs = 8
         self.disptex = {
             'id': 'I',
-            'u0': 'U_0',
-            'u1': 'U_1',
-            'u2': 'U_2',
-            'u3': 'U_3',
+            'u0': '$\\mathrm{U}_0$',
+            'u1': '$\\mathrm{U}_1$',
+            'u2': '$\\mathrm{U}_2$',
+            'u3': '$\\mathrm{U}_3$',
             'x': 'X',
             'y': 'Y',
             'z': 'Z',
             'h': 'H',
             's': 'S',
-            'sdg': 'S^\\dagger',
+            'sdg': '$\\mathrm{S}^\\dagger$',
             't': 'T',
-            'tdg': 'T^\\dagger',
+            'tdg': '$\\mathrm{T}^\\dagger$',
             'iswap': 'Iswap',
             'dcx': 'Dcx',
             'ms': 'MS',
             'diagonal': 'Diagonal',
             'unitary': 'Unitary',
             'r': 'R',
-            'rx': 'R_x',
-            'ry': 'R_y',
-            'rz': 'R_z',
-            'rxx': 'R_{xx}',
-            'ryy': 'R_{yy}',
-            'rzx': 'R_{zx}',
-            'reset': '\\left|0\\right\\rangle',
-            'initialize': '|psi>'
+            'rx': '$\\mathrm{R}_\\mathrm{X}$',
+            'ry': '$\\mathrm{R}_\\mathrm{Y}$',
+            'rz': '$\\mathrm{R}_\\mathrm{Z}$',
+            'rxx': '$\\mathrm{R}_{\\mathrm{XX}}$',
+            'ryy': '$\\mathrm{R}_{\\mathrm{YY}}$',
+            'rzx': '$\\mathrm{R}_{\\mathrm{ZX}}$',
+            'rzz': '$\\mathrm{R}_{\\mathrm{ZZ}}$',
+            'reset': '$\\left|0\\right\\rangle$',
+            'initialize': '$|\\psi\\rangle$'
         }
         self.dispcol = {
             'u0': basis_color,
@@ -158,32 +159,33 @@ def __init__(self):
         self.sfs = 8
         self.disptex = {
             'id': 'I',
-            'u0': 'U_0',
-            'u1': 'U_1',
-            'u2': 'U_2',
-            'u3': 'U_3',
+            'u0': '$\\mathrm{U}_0$',
+            'u1': '$\\mathrm{U}_1$',
+            'u2': '$\\mathrm{U}_2$',
+            'u3': '$\\mathrm{U}_3$',
             'x': 'X',
             'y': 'Y',
             'z': 'Z',
             'h': 'H',
             's': 'S',
-            'sdg': 'S^\\dagger',
+            'sdg': '$\\mathrm{S}^\\dagger$',
             't': 'T',
-            'tdg': 'T^\\dagger',
+            'tdg': '$\\mathrm{T}^\\dagger$',
             'iswap': 'Iswap',
             'dcx': 'Dcx',
             'ms': 'MS',
             'diagonal': 'Diagonal',
             'unitary': 'Unitary',
             'r': 'R',
-            'rx': 'R_x',
-            'ry': 'R_y',
-            'rz': 'R_z',
-            'rxx': 'R_{xx}',
-            'ryy': 'R_{yy}',
-            'rzx': 'R_{zx}',
-            'reset': '\\left|0\\right\\rangle',
-            'initialize': '|psi>'
+            'rx': '$\\mathrm{R}_\\mathrm{X}$',
+            'ry': '$\\mathrm{R}_\\mathrm{Y}$',
+            'rz': '$\\mathrm{R}_\\mathrm{Z}$',
+            'rxx': '$\\mathrm{R}_{\\mathrm{XX}}$',
+            'ryy': '$\\mathrm{R}_{\\mathrm{YY}}$',
+            'rzx': '$\\mathrm{R}_{\\mathrm{ZX}}$',
+            'rzz': '$\\mathrm{R}_{\\mathrm{ZZ}}$',
+            'reset': '$\\left|0\\right\\rangle$',
+            'initialize': '$|\\psi\\rangle$'
         }
         self.dispcol = {
             'u0': '#ffffff',


Test patch:
diff --git a/test/ipynb/mpl/references/big_gates.png b/test/ipynb/mpl/references/big_gates.png
Binary files a/test/ipynb/mpl/references/big_gates.png and b/test/ipynb/mpl/references/big_gates.png differ
diff --git a/test/ipynb/mpl/references/cnot.png b/test/ipynb/mpl/references/cnot.png
Binary files a/test/ipynb/mpl/references/cnot.png and b/test/ipynb/mpl/references/cnot.png differ
diff --git a/test/ipynb/mpl/references/conditional.png b/test/ipynb/mpl/references/conditional.png
Binary files a/test/ipynb/mpl/references/conditional.png and b/test/ipynb/mpl/references/conditional.png differ
diff --git a/test/ipynb/mpl/references/creg_initial_false.png b/test/ipynb/mpl/references/creg_initial_false.png
Binary files a/test/ipynb/mpl/references/creg_initial_false.png and b/test/ipynb/mpl/references/creg_initial_false.png differ
diff --git a/test/ipynb/mpl/references/creg_initial_true.png b/test/ipynb/mpl/references/creg_initial_true.png
Binary files a/test/ipynb/mpl/references/creg_initial_true.png and b/test/ipynb/mpl/references/creg_initial_true.png differ
diff --git a/test/ipynb/mpl/references/cswap_rzz.png b/test/ipynb/mpl/references/cswap_rzz.png
Binary files a/test/ipynb/mpl/references/cswap_rzz.png and b/test/ipynb/mpl/references/cswap_rzz.png differ
diff --git a/test/ipynb/mpl/references/ctrl_labels.png b/test/ipynb/mpl/references/ctrl_labels.png
Binary files a/test/ipynb/mpl/references/ctrl_labels.png and b/test/ipynb/mpl/references/ctrl_labels.png differ
diff --git a/test/ipynb/mpl/references/fold_4.png b/test/ipynb/mpl/references/fold_4.png
Binary files a/test/ipynb/mpl/references/fold_4.png and b/test/ipynb/mpl/references/fold_4.png differ
diff --git a/test/ipynb/mpl/references/fold_minus1.png b/test/ipynb/mpl/references/fold_minus1.png
Binary files a/test/ipynb/mpl/references/fold_minus1.png and b/test/ipynb/mpl/references/fold_minus1.png differ
diff --git a/test/ipynb/mpl/references/ghz_to_gate.png b/test/ipynb/mpl/references/ghz_to_gate.png
Binary files a/test/ipynb/mpl/references/ghz_to_gate.png and b/test/ipynb/mpl/references/ghz_to_gate.png differ
diff --git a/test/ipynb/mpl/references/long_name.png b/test/ipynb/mpl/references/long_name.png
Binary files a/test/ipynb/mpl/references/long_name.png and b/test/ipynb/mpl/references/long_name.png differ
diff --git a/test/ipynb/mpl/references/no_barriers.png b/test/ipynb/mpl/references/no_barriers.png
Binary files a/test/ipynb/mpl/references/no_barriers.png and b/test/ipynb/mpl/references/no_barriers.png differ
diff --git a/test/ipynb/mpl/references/pauli_clifford.png b/test/ipynb/mpl/references/pauli_clifford.png
Binary files a/test/ipynb/mpl/references/pauli_clifford.png and b/test/ipynb/mpl/references/pauli_clifford.png differ
diff --git a/test/ipynb/mpl/references/plot_barriers_false.png b/test/ipynb/mpl/references/plot_barriers_false.png
Binary files a/test/ipynb/mpl/references/plot_barriers_false.png and b/test/ipynb/mpl/references/plot_barriers_false.png differ
diff --git a/test/ipynb/mpl/references/plot_barriers_true.png b/test/ipynb/mpl/references/plot_barriers_true.png
Binary files a/test/ipynb/mpl/references/plot_barriers_true.png and b/test/ipynb/mpl/references/plot_barriers_true.png differ
diff --git a/test/ipynb/mpl/references/plot_partial_barrier.png b/test/ipynb/mpl/references/plot_partial_barrier.png
Binary files a/test/ipynb/mpl/references/plot_partial_barrier.png and b/test/ipynb/mpl/references/plot_partial_barrier.png differ
diff --git a/test/ipynb/mpl/references/r_gates.png b/test/ipynb/mpl/references/r_gates.png
Binary files a/test/ipynb/mpl/references/r_gates.png and b/test/ipynb/mpl/references/r_gates.png differ
diff --git a/test/ipynb/mpl/references/scale_default.png b/test/ipynb/mpl/references/scale_default.png
Binary files a/test/ipynb/mpl/references/scale_default.png and b/test/ipynb/mpl/references/scale_default.png differ
diff --git a/test/ipynb/mpl/references/scale_double.png b/test/ipynb/mpl/references/scale_double.png
Binary files a/test/ipynb/mpl/references/scale_double.png and b/test/ipynb/mpl/references/scale_double.png differ
diff --git a/test/ipynb/mpl/references/scale_half.png b/test/ipynb/mpl/references/scale_half.png
Binary files a/test/ipynb/mpl/references/scale_half.png and b/test/ipynb/mpl/references/scale_half.png differ
diff --git a/test/ipynb/mpl/references/u_gates.png b/test/ipynb/mpl/references/u_gates.png
Binary files a/test/ipynb/mpl/references/u_gates.png and b/test/ipynb/mpl/references/u_gates.png differ