Skip to content

Commit

Permalink
Stop forcing mathtext mode for all strings (#4669)
Browse files Browse the repository at this point in the history
* Stop forcing mathtext mode for all strings

In #4616 the gate title and labels were changed to always use
matplotlib's mathtext mode. [1] This was a breaking change and will result
in users having weirdly formatted names and labels. For example places in
qiskit itself set gate names like 'Controlled-Evolution^1_dg' which will
not get rendered as expected in mathtext mode. The documented behavior for
labels and gate names before was to rely on matplotlib's rendering
behavior to expliclictly add '$' to the label string for sections that
should be rendered in mathtext mode. For example, this behavior was ported
to the latex circuit drawer in #3224 so that users could take advantage
of the same flexibility in how text was rendered by the drawer. This
commit removes the forced addition of '$' around all the label strings
to go back to the expected behavior.

Fixes #4667

[1] https://matplotlib.org/3.2.2/tutorials/text/usetex.html

* Also fix parameters

* Add explicit math mode to default gate names where necessary

* Remove conversions assuming hard coded mathtext mode

* Don't slant letters for standard gates

* Stop hard coding latex characters and use pylatexenc

Previously the manual text width detection had a hard coded subset of
latex special characters to try and convert a latex string to a text
string. This however is quite error prone because it's missing a large
portion of valid latex commands. For example, in the previous commit
when we added \\mathrm to stop slanting text in the standard gates was
missed by this. Instead of maintaing a manual list of commands this
commit just switches to use pylatexenc, which is already a visualization
requirement for the latex drawers, to do the conversion from latex to
unicode. This is much less error prone and will give a true width of the
strings even for user supplied gate names which can use any latex
commands they want.

* Use mathtext for U_* standard gates too

* Add pylatexenc to binder build

* Add release note

* Don't slant standard gate subscript and ALL CAPS

* Fix spacing with mathmode labels

This comit fixes a few edge case with mathmode text spacing. The first
is relying on pylatexenc to convert latex to text leaves '_' in for
subscripts in math mode strings. This fixes this by finding manually
removing those from the output string used for finding label width. The
other 2 fixes are from review comments about spacing for cui1/rzz and a
minus sign width.

* Use \psi \rangle for initialize label

* Fix oversight in mathmode handling

* new references

Co-authored-by: Luciano Bello <luciano.bello@ibm.com>
Co-authored-by: Julien Gacon <jules.gacon@googlemail.com>
  • Loading branch information
3 people authored Jul 28, 2020
1 parent 10ac1ec commit b84590b
Show file tree
Hide file tree
Showing 25 changed files with 84 additions and 55 deletions.
3 changes: 2 additions & 1 deletion postBuild
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@

# Dependencies
# - matplotlib: for MPL drawer
# - pylatexenc: for MPL drawer
# - pillow: for image comparison
# - appmode: jupyter extension for executing the notebook
pip install matplotlib pillow appmode
pip install matplotlib pylatexenc pillow appmode

# Activation of appmode extension
jupyter nbextension enable --py --sys-prefix appmode
Expand Down
64 changes: 38 additions & 26 deletions qiskit/visualization/matplotlib.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,9 @@
import itertools
import json
import logging
import re
from warnings import warn

import numpy as np

try:
Expand All @@ -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
Expand Down Expand Up @@ -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 = []
Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -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:
Expand All @@ -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
Expand All @@ -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):
Expand All @@ -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):
Expand Down Expand Up @@ -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)

Expand All @@ -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:
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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 = ''

Expand Down Expand Up @@ -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
Expand Down
58 changes: 30 additions & 28 deletions qiskit/visualization/qcstyle.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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',
Expand Down
14 changes: 14 additions & 0 deletions releasenotes/notes/pylatexenc-matplotlib-428f285f4cfd2d7c.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
---
upgrade:
- |
The ``'mpl'`` output mode for the
:meth:`~qiskit.circuit.QuantumCircuit.draw` method and
:func:`~qiskit.visualization.circuit_drawer` now requires the
`pylatexenc <https://pylatexenc.readthedocs.io/en/latest/latexencode/>`__
library to be installed. This was already an optional dependency for
visualization, but was only required for the ``'latex'`` output mode
before. It is now also required for the matplotlib drawer because it is
needed to handle correctly sizing gates with matplotlib's
`mathtext <https://matplotlib.org/3.2.2/tutorials/text/mathtext.html>`__
labels for gates.
Binary file modified test/ipynb/mpl/references/big_gates.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified test/ipynb/mpl/references/cnot.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified test/ipynb/mpl/references/conditional.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified test/ipynb/mpl/references/creg_initial_false.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified test/ipynb/mpl/references/creg_initial_true.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified test/ipynb/mpl/references/cswap_rzz.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified test/ipynb/mpl/references/ctrl_labels.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified test/ipynb/mpl/references/fold_4.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified test/ipynb/mpl/references/fold_minus1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified test/ipynb/mpl/references/ghz_to_gate.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified test/ipynb/mpl/references/long_name.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified test/ipynb/mpl/references/no_barriers.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified test/ipynb/mpl/references/pauli_clifford.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified test/ipynb/mpl/references/plot_barriers_false.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified test/ipynb/mpl/references/plot_barriers_true.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified test/ipynb/mpl/references/plot_partial_barrier.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified test/ipynb/mpl/references/r_gates.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified test/ipynb/mpl/references/scale_default.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified test/ipynb/mpl/references/scale_double.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified test/ipynb/mpl/references/scale_half.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified test/ipynb/mpl/references/u_gates.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

0 comments on commit b84590b

Please sign in to comment.