Skip to content

Commit

Permalink
Merge pull request #7 from alexhagen/master
Browse files Browse the repository at this point in the history
enabled 'np.power' -> exponent, and also enabled replacing simple dec…
  • Loading branch information
erwanp committed Jun 15, 2018
2 parents d0d063f + 81fa728 commit bc9cbfd
Show file tree
Hide file tree
Showing 2 changed files with 164 additions and 104 deletions.
142 changes: 97 additions & 45 deletions pytexit/core/core.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
"""
Parser and core Routines
Parser and core Routines
"""

from __future__ import absolute_import, division, print_function, unicode_literals
Expand Down Expand Up @@ -41,6 +41,15 @@
'Ξ': 'Xi'
}

fracs = {
0.5: ['', 1, 2],
-0.5: ['-', 1, 2],
0.75: ['', 3, 4],
-0.75: ['-', 3, 4],
0.25: ['', 1, 4],
-0.25: ['-', 1, 4]
}

# Modules removed from expressions:
clear_modules = ['math',
'np', 'numpy',
Expand All @@ -64,10 +73,12 @@ def uprint(*expr):




class LatexVisitor(ast.NodeVisitor):

def __init__(self, dummy_var='u', upperscript='ˆ', lowerscript='_',
verbose=False, simplify=True):
verbose=False, simplify=True, simplify_fractions=False,
simplify_ints=False):
super(LatexVisitor, self).__init__()
# super().__init__() # doesn't work in Python 2.x
self.dummy_var = dummy_var
Expand All @@ -76,14 +87,29 @@ def __init__(self, dummy_var='u', upperscript='ˆ', lowerscript='_',
self.lower = lowerscript

self.verbose = verbose
self.simplify=simplify
self.simplify = simplify
self.simplify_fractions = simplify_fractions
self.simplify_ints = simplify_ints

def looks_like_int(self, a):
""" Check if the input ``a`` looks like an integer """
if self.simplify_ints:
if not isinstance(a, str):
a = str(a)
try:
value = float(a.split('.')[1]) == 0.0
except (IndexError, ValueError):
value = False
return value
else:
return False

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

def visit_ListComp(self, n, kwout=False):
''' Analyse a list comprehension
Output :
''' Analyse a list comprehension
Output :
- kw : used by some functions to display nice formulas (e.g : sum)
- args : standard output to display a range in other cases
'''
Expand Down Expand Up @@ -157,6 +183,12 @@ def visit_Call(self, n):
return r'\cosh^{-1}(%s)' % args
elif func in ['arctanh']:
return r'\tanh^{-1}(%s)' % args
elif func in ['power']:
args = args.split(',')
return self.power(self.parenthesis(args[0]), args[1])
elif func in ['divide']:
args = args.split(',')
return self.division(args[0], args[1])
elif func in ['abs', 'fabs']:
return r'|%s|' % args

Expand All @@ -169,7 +201,7 @@ def visit_Call(self, n):
elif func in ['quad']:
(f,a,b) = list(map(self.visit, n.args))
return r'\int_{%s}^{%s} %s(%s) d%s' %(a,b,f,self.dummy_var,self.dummy_var)
#
#
# Sum
elif func in ['sum']:
if blist:
Expand All @@ -194,7 +226,7 @@ def visit_Name(self, n):
- Recognize upperscripts in identifiers names (default: ˆ, valid in Python3)
Note that using ˆ is not recommanded in variable names because it may
be confused with the operator ^, but in some special cases of extensively
long formulas with lots of indices, it may help the readability of the
long formulas with lots of indices, it may help the readability of the
code
'''

Expand All @@ -204,23 +236,23 @@ def visit_Name(self, n):
warn('Only one upperscript character supported per identifier')

def build_tree(expr, level=1):
''' Builds a tree out of a valid Python identifier, according to the
''' Builds a tree out of a valid Python identifier, according to the
following proposed formalism:
Formalism
----------
----------
Python -> Real
k_i_j -> k_i,j
k_i__j -> k_(i_j)
k_i__j -> k_(i_j)
k_iˆj -> k_i^j
k_iˆˆj -> k_(i^j)
k_i__1_i__2ˆj__1ˆˆj__2 -> k_(i_1,i_2)^(j_1,j_2)
Even if one may agree that this last expression isn't a very
readable variable name.
The idea behind this is that I want my Python formula to be the
The idea behind this is that I want my Python formula to be the
same objects as the LaTeX formula I write in my reports / papers
It allows me to:
Expand Down Expand Up @@ -280,7 +312,6 @@ def convert_underscores(self, expr):

def convert_symbols(self, expr):
m = expr

# Standard greek letters
if m in ['alpha', 'beta', 'gamma', 'delta', 'epsilon', 'zeta', 'eta', 'theta',
'iota', 'kappa', 'mu', 'nu', 'xi', 'pi', 'rho', 'sigma',
Expand Down Expand Up @@ -318,9 +349,10 @@ def prec_Name(self, n):
def visit_UnaryOp(self, n):
# Note: removed space between {0} and {1}... so that 10**-3 yields
# $$10^{-3}$$ and not $$10^{- 3}$$

if self.prec(n.op) > self.prec(n.operand):
return r'{0}{1}'.format(self.visit(n.op), self.parenthesis(self.visit(n.operand)))
return r'{0}{1}'.format(self.visit(n.op),
self.parenthesis(self.visit(n.operand)))
else:
return r'{0}{1}'.format(self.visit(n.op), self.visit(n.operand))

Expand All @@ -336,18 +368,32 @@ def visit_BinOp(self, n):
right = self.parenthesis(self.visit(n.right))
else:
right = self.visit(n.right)

# Special binary operators
if isinstance(n.op, ast.Div):
if self.simplify_fractions:
left_is_int = self.looks_like_int(left)
right_is_int = self.looks_like_int(right)
if left_is_int or right_is_int:
if left_is_int and right_is_int:
return self.division('%d' % int(float(left)),
'%d' % int(float(right)))
elif left_is_int:
return self.division('%d' % int(float(left)),
self.visit(n.right))
else:
return self.division(self.visit(n.left),
'%d' % int(float(right)))
return self.division(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))
return r'\left\lfloor\frac{%s}{%s}\right\rfloor' % \
(self.visit(n.left), self.visit(n.right))
elif isinstance(n.op, ast.Pow):
return self.power(left, self.visit(n.right))
elif isinstance(n.op, ast.Mult):

if self.simplify:

def looks_like_float(a):
''' Care for the special case of 2 floats/integer
We don't want 'a*2' where we could have simply written '2a' '''
Expand All @@ -356,22 +402,22 @@ def looks_like_float(a):
return True
except ValueError:
return False

left_is_float = looks_like_float(left)
right_is_float = looks_like_float(right)

# Get multiplication operator. Force x if floats are involved
if left_is_float or right_is_float:
operator = r'\times'
else: # get standard Mult operator (see visit_Mult)
operator = self.visit(n.op)

# Simplify in some cases
if right_is_float and not left[0].isdigit(): # simplify (a*2 --> 2a)
return r'{0}{1}'.format(right, left)
elif left_is_float and not right[0].isdigit(): # simplify (2*a --> 2a)
return r'{0}{1}'.format(left, right)
else:
else:
return r'{0}{1}{2}'.format(left, operator, right)
else:
return r'{0}{1}{2}'.format(left, self.visit(n.op), right)
Expand Down Expand Up @@ -455,6 +501,12 @@ def prec_USub(self, n):
return 800

def visit_Num(self, n):
if self.simplify_fractions:
if any([n.n == key for key in fracs.keys()]):
string = r'{0}\frac{{{1}}}{{{2}}}'.format(*fracs[n.n])
return string
if self.looks_like_int(n.n):
return '%d' % n.n
return str(n.n)

def prec_Num(self, n):
Expand Down Expand Up @@ -513,7 +565,7 @@ def group(self,expr):
return expr
else:
return self.brackets(expr)

def parenthesis(self,expr):
return r'\left({0}\right)'.format(expr)

Expand All @@ -535,13 +587,13 @@ def operator(self, func, args=None):

def clean(expr):
''' Removes unnessary calls to libraries
Examples
--------
np.exp(-3) would be read exp(-3)
np.exp(-3) would be read exp(-3)
'''

expr = expr.strip() # remove spaces on the side

for m in clear_modules:
Expand All @@ -554,47 +606,47 @@ def clean(expr):

def simplify(s):
''' Cleans the generated text in post-processing '''
# Remove unecessary parenthesis?

# Remove unecessary parenthesis?
# -------------
# TODO: look for groups that looks like \(\([\(.+\)]*\)\ ),
# (2 pairs of external parenthesis around any number (even 0) of closed pairs
# of parenthesis) -> then remove one of the the two external parenthesis
# TODO: look for groups that looks like \(\([\(.+\)]*\)\ ),
# (2 pairs of external parenthesis around any number (even 0) of closed pairs
# of parenthesis) -> then remove one of the the two external parenthesis
# TRied with re.findall(r'\(\([^\(^\)]*(\([^\(^\)]+\))*[^\(^\)]*\)\)', s) but
# it doesnt work. One should better try to look for inner pairs and remove that
# one after one..
# one after one..


# Improve readability:


# Replace 'NUMBER e NUMBER' with powers of 10
# ------------
regexp = re.compile(r'(\d*\.{0,1}\d+)[e]([-+]?\d*\.{0,1}\d+)')

matches = regexp.findall(s)
splits = regexp.split(s)
assert len(splits) == (len(matches) + 1) + (2 * len(matches))
# splitted groups prefactor, exponent
# splitted groups prefactor, exponent

new_s = ''
# loop over all match and replace
# loop over all match and replace
# ... I didnt find any better way to do that given that we want a conditional
# ... replace (.sub wouldnt work)
for i, (prefactor, exponent) in enumerate(matches):
new_s += splits[3*i]

if len(exponent) == 1:
exp_str = r'{0}'.format(exponent)
else:
exp_str = r'{'+'{0}'.format(exponent)+'}'

if prefactor == '1':
new_s += r'10^{0}'.format(exp_str)
else:
new_s += r'{0}\times10^{1}'.format(prefactor, exp_str)
if len(splits) % 3 == 1: # add last ones
new_s += splits[-1]
s = new_s
return s

return s

0 comments on commit bc9cbfd

Please sign in to comment.