Converting a Python numeric expression to LaTeX
Asked Answered
W

6

39

I need to convert strings with valid Python syntax such as:

'1+2**(x+y)'

and get the equivalent LaTeX:

$1+2^{x+y}$

I have tried SymPy's latex function but it processes actual expression, rather than the string form of it:

>>> latex(1+2**(x+y))
'$1 + 2^{x + y}$'
>>> latex('1+2**(x+y)')
'$1+2**(x+y)$'

but to even do this, it requires x and y to be declared as type "symbols".

I want something more straightforward, preferably doable with the parser from the compiler module.

>>> compiler.parse('1+2**(x+y)')
Module(None, Stmt([Discard(Add((Const(1), Power((Const(2), Add((Name('x'), Name('y'))))))))]))

Last but not least, the why: I need to generate those LaTeX snippets so that I can show them in a webpage with MathJax.

Weinberger answered 5/10, 2010 at 19:33 Comment(1)
An important distinction is that you are parsing these expressions not to evaluate them, but to do document layout with them. It is telling that you knew to replace the parens around x+y with braces, but there are certainly other places where the parens would need to be preserved. I think you are going to have to give some thorough thought to what you are trying to parse and what it should look like when you're done. Then you can start thinking through how that transformation might be achieved given a suitable parser. This will also help you formulate what the parser will need to do.Cestoid
C
24

You can use sympy.latex with eval:

s = "1+2**(x+y)"
sympy.latex(eval(s))   # prints '$1 + {2}^{x + y}$'

You still have to declare the variables as symbols, but if this is really a problem, it's much easier to write a parser to do this than to parse everything and generate the latex from scratch.

Canaletto answered 5/10, 2010 at 22:4 Comment(1)
thanks Tom10. Simple, but elegant. My original implementation went somewhat like this: s = "1+2**(x+y)" eval('sympy.latex(%s)'%s) after turning the variables into symbols first.Weinberger
S
30

Here's a rather long but still incomplete method that doesn't involve sympy in any way. It's enough to cover the example of (-b-sqrt(b**2-4*a*c))/(2*a) which gets translated to \frac{- b - \sqrt{b^{2} - 4 \; a \; c}}{2 \; a} and renders as

alt text

It basically creates the AST and walks it producing the latex math the corresponds to the AST nodes. What's there should give enough of an idea how to extend it in the places it's lacking.


import ast

class LatexVisitor(ast.NodeVisitor):

    def prec(self, n):
        return getattr(self, 'prec_'+n.__class__.__name__, getattr(self, 'generic_prec'))(n)

    def visit_Call(self, n):
        func = self.visit(n.func)
        args = ', '.join(map(self.visit, n.args))
        if func == 'sqrt':
            return '\sqrt{%s}' % args
        else:
            return r'\operatorname{%s}\left(%s\right)' % (func, args)

    def prec_Call(self, n):
        return 1000

    def visit_Name(self, n):
        return n.id

    def prec_Name(self, n):
        return 1000

    def visit_UnaryOp(self, n):
        if self.prec(n.op) > self.prec(n.operand):
            return r'%s \left(%s\right)' % (self.visit(n.op), self.visit(n.operand))
        else:
            return r'%s %s' % (self.visit(n.op), self.visit(n.operand))

    def prec_UnaryOp(self, n):
        return self.prec(n.op)

    def visit_BinOp(self, n):
        if self.prec(n.op) > self.prec(n.left):
            left = r'\left(%s\right)' % self.visit(n.left)
        else:
            left = self.visit(n.left)
        if self.prec(n.op) > self.prec(n.right):
            right = r'\left(%s\right)' % self.visit(n.right)
        else:
            right = self.visit(n.right)
        if isinstance(n.op, ast.Div):
            return r'\frac{%s}{%s}' % (self.visit(n.left), self.visit(n.right))
        elif isinstance(n.op, ast.FloorDiv):
            return r'\left\lfloor\frac{%s}{%s}\right\rfloor' % (self.visit(n.left), self.visit(n.right))
        elif isinstance(n.op, ast.Pow):
            return r'%s^{%s}' % (left, self.visit(n.right))
        else:
            return r'%s %s %s' % (left, self.visit(n.op), right)

    def prec_BinOp(self, n):
        return self.prec(n.op)

    def visit_Sub(self, n):
        return '-'

    def prec_Sub(self, n):
        return 300

    def visit_Add(self, n):
        return '+'

    def prec_Add(self, n):
        return 300

    def visit_Mult(self, n):
        return '\\;'

    def prec_Mult(self, n):
        return 400

    def visit_Mod(self, n):
        return '\\bmod'

    def prec_Mod(self, n):
        return 500

    def prec_Pow(self, n):
        return 700

    def prec_Div(self, n):
        return 400

    def prec_FloorDiv(self, n):
        return 400

    def visit_LShift(self, n):
        return '\\operatorname{shiftLeft}'

    def visit_RShift(self, n):
        return '\\operatorname{shiftRight}'

    def visit_BitOr(self, n):
        return '\\operatorname{or}'

    def visit_BitXor(self, n):
        return '\\operatorname{xor}'

    def visit_BitAnd(self, n):
        return '\\operatorname{and}'

    def visit_Invert(self, n):
        return '\\operatorname{invert}'

    def prec_Invert(self, n):
        return 800

    def visit_Not(self, n):
        return '\\neg'

    def prec_Not(self, n):
        return 800

    def visit_UAdd(self, n):
        return '+'

    def prec_UAdd(self, n):
        return 800

    def visit_USub(self, n):
        return '-'

    def prec_USub(self, n):
        return 800
    def visit_Num(self, n):
        return str(n.n)

    def prec_Num(self, n):
        return 1000

    def generic_visit(self, n):
        if isinstance(n, ast.AST):
            return r'' % (n.__class__.__name__, ', '.join(map(self.visit, [getattr(n, f) for f in n._fields])))
        else:
            return str(n)

    def generic_prec(self, n):
        return 0

def py2tex(expr):
    pt = ast.parse(expr)
    return LatexVisitor().visit(pt.body[0].value)

Serrell answered 6/10, 2010 at 16:14 Comment(6)
Wow! this is pretty amazing! Thanks Geoff! Haven't tested it yet but it should become a pretty useful piece of code once it is tested against a wider variety of examples.Weinberger
Note that the \operatorname command used for function calls requires amsmath. For mathjax you need to add the appropriate extension per mathjax.org/resources/docs/?tex.html#amsmath-and-amssymbolSerrell
Thanks. There is a problem with minus ej: "3-(1+2)" -> 3 - 1 + 2. Insert > elif self.prec(n.op) == self.prec(n.right) and isinstance(n.op, ast.Sub): > right = r'\left(%s\right)' % self.visit(n.right) in second if inside visit_BinOp()Papal
There is now a library based on this answer: pypi.python.org/pypi/pytexit.Bunde
Can I suggest to add a backslash before {0}{1} under the # Usual math functions ? This will (more) correctly output 'cos(a)' as $$\cos\left(a\right)$$, where cos is seen as a proper function.Nimwegen
This solution is so cool! In addition to @Edoot's comment above, there is also a problem with power of USub I think, e.g.: "(-3)**2". In the def visit_BinOp(self, n): under elif isinstance(n.op, ast.Pow): I replaced return r'%s^{%s}' % (left, self.visit(n.right)) with if isinstance(n.left, (ast.UnaryOp)): return r'(%s)^{%s}' % (left, self.visit(n.right)) else: return r'%s^{%s}' % (left, self.visit(n.right)) and it seems to solve the issue.Agminate
C
24

You can use sympy.latex with eval:

s = "1+2**(x+y)"
sympy.latex(eval(s))   # prints '$1 + {2}^{x + y}$'

You still have to declare the variables as symbols, but if this is really a problem, it's much easier to write a parser to do this than to parse everything and generate the latex from scratch.

Canaletto answered 5/10, 2010 at 22:4 Comment(1)
thanks Tom10. Simple, but elegant. My original implementation went somewhat like this: s = "1+2**(x+y)" eval('sympy.latex(%s)'%s) after turning the variables into symbols first.Weinberger
B
17

You can use SymPy. Just pass the string to the sympify() function first, which will convert it to a valid SymPy expression (i.e., create the Symbols for you, etc.). So you could do

>>> latex(sympify('1+2**(x+y)'))
1 + 2^{x + y}

S() is also a shortcut to sympify(), i.e., latex(S('1+2**(x+y)')) also works.

Blithesome answered 29/11, 2010 at 21:45 Comment(0)
S
6

To build on tom10's answer you can define a dictionary that will generate symbols and use that when calling eval:

from collections import defaultdict
class GenerateSymbols(defaultdict):
  def __missing__(self, key):
    return sympy.Symbol(key)

Then if you use

sympy.latex(eval('1+2**(x+y)',GenerateSymbols()))

you shouldn't have to worry about pre-creating Symbols for the variables.

Serrell answered 5/10, 2010 at 22:56 Comment(1)
Thanks Geoff, Brilliant! see my fixes below.Weinberger
W
6

Just a little fix to Geoff Reedy excellent answer:

class GenerateSymbols(defaultdict):
    def __missing__(self, key):
        self[key] = sympy.Symbol(key)
        return self[key]

Before it would not add the new item to the dict. Now you can use this with your expression:

d= GenerateSymbols()    
eq = '(-b-sqrt(b**2-4*a*c))/(2*a)'

and you can further simplify it before converting it to LaTeX:

sympy.latex(sympy.simplify(eval(eq,d)))

and you get this:

'$- \\frac{b + \\operatorname{sqrt}\\left(- 4 a c + b^{2}\\right)}{2 a}$'
Weinberger answered 6/10, 2010 at 11:16 Comment(1)
If someone has a solution not involving sympy, please post! I don't want to have to drag sympy along my app just for this little task! (But I mus admit that way better that having to depend on sage of some other CAS behemoths)Weinberger
W
0

To render @Geoff Reedy latex format text. We could use matplotlib.

exp = py2tex('1**x+y')
import matplotlib.pyplot as plt
text = exp

plt.text(0.36, 0.5, r"$%s$" %text, fontsize=24)
ax = plt.gca()
ax.axes.get_xaxis().set_visible(False)
ax.axes.get_yaxis().set_visible(False)

plt.show()

enter image description here

Wheal answered 3/6, 2022 at 6:33 Comment(1)
I had problems with saving the image. Not sure why? The image gets saved using plt.savefig('image.png') but its BLANKKK!Wheal

© 2022 - 2024 — McMap. All rights reserved.