Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why matplotlib replace a right parenthesis with "!" in latex expression?

I'm in the situation where I have to translate python expression to Latex Bitmap for the enduser (who feels confident enough to write python functions by himself but prefers to watch result in Latex).

I'm using Matplotlib.mathtext to do the job (from a translated latex raw string) with the following code.

import wx
import wx.lib.scrolledpanel as scrolled

import matplotlib as mpl
from matplotlib import cm 
from matplotlib import mathtext

class LatexBitmapFactory():
    """ Latex Expression to Bitmap """
    mpl.rc('image', origin='upper')
    parser = mathtext.MathTextParser("Bitmap")

    mpl.rc('text', usetex=True)
    DefaultProps = mpl.font_manager.FontProperties(family = "sans-serif",\
                                                    style = "normal",\
                                                    weight = "medium",\
                                                    size = 6)
    # size is changed from 6 to 7 
#-------------------------------------------------------------------------------
    def SetBitmap(self, _parent, _line, dpi = 150, prop = DefaultProps):
        bmp = self.mathtext_to_wxbitmap(_line, dpi, prop = prop)
        w,h = bmp.GetWidth(), bmp.GetHeight()
        return wx.StaticBitmap(_parent, -1, bmp, (80, 50), (w, h))
#-------------------------------------------------------------------------------
    def mathtext_to_wxbitmap(self, _s, dpi = 150, prop = DefaultProps):
        ftimage, depth = self.parser.parse(_s, dpi, prop)
        w,h = ftimage.get_width(), ftimage.get_height()
        return wx.BitmapFromBufferRGBA(w, h, ftimage.as_rgba_str())


EXP = r'$\left(\frac{A \cdot \left(vds \cdot rs + \operatorname{Vdp}\left(ri, Rn, Hr, Hd\right) \cdot rh\right) \cdot \left(rSurf + \left(1.0 - rSurf\right) \cdot ft\right) \cdot \left(1.0 - e^{- \left(\left(lr + \frac{\operatorname{Log}\left(2\right)}{tem \cdot 86400.0}\right)\right) \cdot tFr \cdot 3600.0}\right)}{rc \cdot \left(lr + \frac{\operatorname{Log}\left(2\right)}{tem \cdot 86400.0}\right) \cdot tFr \cdot 3600.0} + 1\right)$'

class aFrame(wx.Frame):
    def __init__(self, title="Edition"):
        wx.Frame.__init__(self, None, wx.ID_ANY, title=title, size=(600,400),
                          style=wx.DEFAULT_DIALOG_STYLE|wx.RESIZE_BORDER)
        self.SetBackgroundColour(wx.Colour(255,255,255))

        sizer = wx.FlexGridSizer(cols=25, vgap=4, hgap=4)
        panel = scrolled.ScrolledPanel(self)
        image_latex = LatexBitmapFactory().SetBitmap(panel, EXP)

        sizer.Add(image_latex, flag=wx.EXPAND|wx.ALL)
        panel.SetSizer(sizer)
        panel.SetAutoLayout(1)
        panel.SetupScrolling()


app = wx.App(redirect=True, filename="latexlog.txt")
frame = aFrame()
frame.Show()
app.MainLoop()

with size=6, the following picture is displayed Did you find the "!" ?

with size=7, I have this one perfect !

The latex expression is correct, the second picture is correct. I don't have any error message, just a right parenthesis replaced with "!".

If I continue writing the expression I still have "!" with size 6.

T_T

If the expression is simpler, the right parenthesis is correctly displayed.

Any idea to solve it ?

like image 408
user2652620 Avatar asked Sep 28 '15 14:09

user2652620


3 Answers

TL;DR There is a bug in the following line of mathtext.py Line 727. It relates the right parenthesis at size Bigg to an index '\x21', but this is the index for an exclamation point. The line with a bit of context reads

_size_alternatives = {
    '('          : [('rm', '('), ('ex', '\xa1'), ('ex', '\xb3'),
                    ('ex', '\xb5'), ('ex', '\xc3')],
    ')'          : [('rm', ')'), ('ex', '\xa2'), ('ex', '\xb4'),
                    ('ex', '\xb6'), ('ex', '\x21')],

I am not exactly sure which index to change to, but I suggest you change your local copy of mathtext.py to read as follows:

_size_alternatives = {
    '('          : [('rm', '('), ('ex', '\xa1'), ('ex', '\xb3'),
                    ('ex', '\xb5'), ('ex', '\x28')],
    ')'          : [('rm', ')'), ('ex', '\xa2'), ('ex', '\xb4'),
                    ('ex', '\xb6'), ('ex', '\x29')]

The parantheses this produces are a little over rounded as they are the basic parantheses, but they work. Equally - you could substitute in the bigg size ones - '\xb5' and 'xb6'

Reported on matplotlib github Issue 5210


I can reproduce this problem with size=6 using the code as provided (made the constant a bit bigger in case it was a width issue). I cannot reproduce the "fix" by setting size = 7, but I can if I go up to size = 8 or higher - suggesting this might be a nasty edge case bug and possibly system dependent...

I did quite a bit of investigation / diagnosis (see below), but it seems there is a bug - reported on matplotlib github here.

However, reducing to a matplotlib only example yields a very nice rendering as follows. Note I've setup my matplotlib to use latex rendering by default - but you can set the options explicitly for the same results.

Code

 import matplotlib.pyplot as plt
 import matplotlib as mpl

 mpl.rc('image', origin='upper')

 mpl.rc('text', usetex=True)
 DefaultProps = mpl.font_manager.FontProperties(family = "sans-serif",\
                                                 style = "normal",\
                                                 weight = "medium",\
                                                 size = 6)

 EXP = r'$\left(\frac{A \cdot \left(vds \cdot rs + \operatorname{Vdp}\left(ri, Rn, Hr, Hd\right) \cdot rh\right) \cdot \left(rSurf + \left(1.0 - rSurf\right) \cdot ft\right) \cdot \left(1.0 - e^{- \left(\left(lr + \frac{\operatorname{Log}\left(2\right)}{tem \cdot 86400.0}\right)\right) \cdot tFr \cdot 3600.0}\right)}{rc \cdot \left(lr + \frac{\operatorname{Log}\left(2\right)}{tem \cdot 86400.0}\right) \cdot tFr \cdot 3600.0} + 10589 \right)$'

 plt.title(EXP, fontsize=6)
 plt.gca().set_axis_off() # Get rid of the plotting axis for clarity

 plt.show()

Output

Output window cropped and zoomed a bit for clarity, but you can see the bracket is rendered OK

Formula rendered

This suggests that the problem is either the way the matplotlib rendering engine is being used, the output to bitmap, or the interaction with wxPython

From experimentation, I noticed that if you increase the dpi to 300 the code works fine at size = 6, but starts to fail again at size = 3. This implies the problem is that one of the libraries does not think it can render the element in a certain number of pixels.

Root cause

Diagnosing which bit was doing this is hard (IMO)

First, I added

    self.parser.to_png('D:\\math_out.png', _s, color=u'black', dpi=150)

as the first line of mathtext_to_wxbitmap(self, _s, dpi = 150, prop = DefaultProps). This gave a nice output png, making me think it probably wasn't the matplotlib parser at fault... EDIT based on @Baptiste's useful answer, I tested this a bit more. Actually - if I explicitly pass in fontsize to this call, I can replicate the appearance of the exclamation point. Also, dpi passed into this call is ignored - so in my tests I am actually dealing with a 300 dpi image. So, focus should rest on the MathTextParser and it could be a dpi issue.


Further investigation

A bit more investigation - I monkey patched my matplotlib installation - putting in a print(result) directly after the call to parseString() here. With a working expression that goes fine and prints out a textual representation. With the bugged scenario, I see:

Traceback (most recent call last):
  File "D:\baptiste_test.py", line 9, in <module>
    parser.to_png(filename, s, fontsize=size)
  File "C:\Python27\lib\site-packages\matplotlib\mathtext.py", line 3101, in to_
png
    rgba, depth = self.to_rgba(texstr, color=color, dpi=dpi, fontsize=fontsize)
  File "C:\Python27\lib\site-packages\matplotlib\mathtext.py", line 3066, in to_
rgba
    x, depth = self.to_mask(texstr, dpi=dpi, fontsize=fontsize)
  File "C:\Python27\lib\site-packages\matplotlib\mathtext.py", line 3039, in to_
mask
    ftimage, depth = self.parse(texstr, dpi=dpi, prop=prop)
  File "C:\Python27\lib\site-packages\matplotlib\mathtext.py", line 3012, in par
se
    box = self._parser.parse(s, font_output, fontsize, dpi)
  File "C:\Python27\lib\site-packages\matplotlib\mathtext.py", line 2339, in par
se
    print(result[0])
  File "C:\Python27\lib\site-packages\matplotlib\mathtext.py", line 1403, in __r
epr__
    ' '.join([repr(x) for x in self.children]))
  File "C:\Python27\lib\site-packages\matplotlib\mathtext.py", line 1403, in __r
epr__
    ' '.join([repr(x) for x in self.children]))
  File "C:\Python27\lib\site-packages\matplotlib\mathtext.py", line 1403, in __r
epr__
    ' '.join([repr(x) for x in self.children]))
  File "C:\Python27\lib\site-packages\matplotlib\mathtext.py", line 1403, in __r
epr__
    ' '.join([repr(x) for x in self.children]))
UnicodeEncodeError: 'ascii' codec can't encode character u'\xb3' in position 1:
ordinal not in range(128)

This indicates that the bug might originate in a mis-translated character - maybe a missing codepoint in the font?

I also noted that you can reproduce without the N letter in Baptiste's minimal example.


Further further investigation

Sticking some debug prints in _get_glyph within the BakomaFonts class. In the failing case, the code seems to be looking up an exclamation (u'!') when you'd expect it to be looking for u'\xc4' and returning parenrightBigg (i.e. where the corresponding left bracket is looking up u'\xc3' and returning parenleftBigg). In situations where it only uses parenrightbigg, there is no problem (this occurs for fontsize=5 in the given example but no others). The debug line I put in _get_glyph was:

print('Getting glyph for symbol',repr(unicode(sym)))
print('Returning',cached_font, num, symbol_name, fontsize, slanted)

I guess whether it needs the bigg or Bigg version is based on a combination of fontsize and dpi

OK - I think problem is in this line : https://github.com/matplotlib/matplotlib/blob/master/lib/matplotlib/mathtext.py#L727

this reads (with a little context):

_size_alternatives = {
    '('          : [('rm', '('), ('ex', '\xa1'), ('ex', '\xb3'),
                    ('ex', '\xb5'), ('ex', '\xc3')],
    ')'          : [('rm', ')'), ('ex', '\xa2'), ('ex', '\xb4'),
                    ('ex', '\xb6'), ('ex', '\x21')],   ## <---- Incorrect line.

the '\x21' is wrong - but I can't work out what the right one is '\x29' is close, but "too curved". I guessed at '\xc4' following the pattern, but that is a down arrow. Hopefully one of the core devs can easily lookup this number (decimal 195) against the glyph it renders and correct.

like image 119
J Richard Snape Avatar answered Nov 14 '22 10:11

J Richard Snape


Here is the shortest piece of code I was able to write to get this unexpected behavior:

import matplotlib.mathtext as mt

s = r'$\left(\frac{\frac{\frac{M}{I}}{N}}' \
    r'{\frac{\frac{B}{U}}{G}}\right)$'
parser = mt.MathTextParser("Bitmap")
for size in range(1, 30):
    filename = "figure{0}.png".format(size)
    parser.to_png(filename, s, fontsize=size)

The outputs look like this (a selection 6-12):

size 6

size 7

size 8

size 9

size 10

size 11

size 12

This post was more to share the minimal code to reproduce the error rather than an answer to the question

like image 3
Flabetvibes Avatar answered Nov 14 '22 10:11

Flabetvibes


I still have "!" with dpi='300' at size 9, but around 9 and except 3, it's ok.

I made a new Frame to play with both dpi and size and with '()' and '[]'. Just reuse 'LatexBitmapFactory' to play.

I'm on windows XP, using python 2.7. I have the same error on Windows 7.

class aFrame(wx.Frame):
    def __init__(self, title="Edition"):
        wx.Frame.__init__(self, None, wx.ID_ANY, title=title, size=(1300,600),
                          style=wx.DEFAULT_DIALOG_STYLE)
        self.SetBackgroundColour(wx.Colour(255,255,255))

        self.sizer = wx.BoxSizer(wx.VERTICAL)
        self.panel = scrolled.ScrolledPanel(self)


        init_dpi = 300
        min_dpi, max_dpi = 100, 500
        self.dpi_slider = wx.Slider(self.panel, wx.ID_ANY,
                                    init_dpi, min_dpi, max_dpi, (30, 60), (250, -1), 
                                    wx.SL_HORIZONTAL | wx.SL_AUTOTICKS | wx.SL_LABELS)
        init_size = 9
        min_size, max_size = 3, 40
        self.size_slider = wx.Slider(
            self.panel, wx.ID_ANY, init_size, min_size, max_size, (30, 60), (250, -1), 
            wx.SL_HORIZONTAL | wx.SL_AUTOTICKS | wx.SL_LABELS)

        self.log = wx.StaticText(self.panel, wx.ID_ANY, "DPI:%d SIZE:%d" %(init_dpi, init_size))
        image_latex1 = self.create_lateximage(init_dpi, init_size)
        image_latex2 = self.create_lateximage(init_dpi, init_size, replace_parenthesis=True)
        self.sizer.Add(image_latex1, 1, flag=wx.ALIGN_CENTER|wx.EXPAND|wx.ALL)
        self.sizer.Add(image_latex2, 1, flag=wx.ALIGN_CENTER|wx.EXPAND|wx.ALL)
        self.sizer.Add(self.dpi_slider, 0, wx.ALIGN_CENTER)
        self.sizer.Add(self.size_slider, 0, wx.ALIGN_CENTER)
        self.sizer.Add(self.log, 0, wx.ALIGN_CENTER)

        self.dpi_slider.Bind(wx.EVT_SCROLL_CHANGED, self.OnSliderChanged)
        self.size_slider.Bind(wx.EVT_SCROLL_CHANGED, self.OnSliderChanged)
        self.panel.SetSizer(self.sizer)
        self.panel.SetAutoLayout(1)
        self.panel.SetupScrolling()

    def create_lateximage(self, dpi, size, replace_parenthesis=False):
        updatedProps = mpl.font_manager.FontProperties(family = "sans-serif",\
                                                       style = "normal",\
                                                       weight = "medium",\
                                                       size = size)
        if replace_parenthesis:
            tp_exp = EXP.replace("right)", "right]")
            tp_exp = tp_exp.replace("left(", "left[")
            return LatexBitmapFactory().SetBitmap(self.panel, tp_exp, dpi=dpi, prop=updatedProps)
        return LatexBitmapFactory().SetBitmap(self.panel, EXP, dpi=dpi, prop=updatedProps)

    def OnSliderChanged(self, evt):
        dpi  = int(self.dpi_slider.GetValue())
        size = int(self.size_slider.GetValue())
        self.log.SetLabel("DPI:%d SIZE:%d" %(dpi, size))

        self.Freeze()
        new_image_latex1 = self.create_lateximage(dpi, size)
        new_image_latex2 = self.create_lateximage(dpi, size, True)
        prev_image_latex1 = self.sizer.Remove(0)
        prev_image_latex2 = self.sizer.Remove(0)
        del prev_image_latex1
        del prev_image_latex2

        self.sizer.Insert(0, new_image_latex2, 1, flag=wx.ALIGN_CENTER|wx.EXPAND|wx.ALL)
        self.sizer.Insert(0, new_image_latex1, 1, flag=wx.ALIGN_CENTER|wx.EXPAND|wx.ALL)
        self.sizer.Layout()
        assert len(self.sizer.GetChildren()) == 5,\
        " must have len 5, now %d"%len(self.sizer.GetChildren())
        self.panel.SetupScrolling()
        self.panel.SetScrollRate(100,100)
        self.Thaw()

app = wx.App(redirect=True, filename="latexlog.txt")
frame = aFrame()
frame.Show()
app.MainLoop()

enter image description here

like image 1
user2652620 Avatar answered Nov 14 '22 11:11

user2652620