123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643 |
- """ rewrite of lambdify - This stuff is not stable at all.
- It is for internal use in the new plotting module.
- It may (will! see the Q'n'A in the source) be rewritten.
- It's completely self contained. Especially it does not use lambdarepr.
- It does not aim to replace the current lambdify. Most importantly it will never
- ever support anything else than SymPy expressions (no Matrices, dictionaries
- and so on).
- """
- import re
- from sympy.core.numbers import (I, NumberSymbol, oo, zoo)
- from sympy.core.symbol import Symbol
- from sympy.utilities.iterables import numbered_symbols
- # We parse the expression string into a tree that identifies functions. Then
- # we translate the names of the functions and we translate also some strings
- # that are not names of functions (all this according to translation
- # dictionaries).
- # If the translation goes to another module (like numpy) the
- # module is imported and 'func' is translated to 'module.func'.
- # If a function can not be translated, the inner nodes of that part of the
- # tree are not translated. So if we have Integral(sqrt(x)), sqrt is not
- # translated to np.sqrt and the Integral does not crash.
- # A namespace for all this is generated by crawling the (func, args) tree of
- # the expression. The creation of this namespace involves many ugly
- # workarounds.
- # The namespace consists of all the names needed for the SymPy expression and
- # all the name of modules used for translation. Those modules are imported only
- # as a name (import numpy as np) in order to keep the namespace small and
- # manageable.
- # Please, if there is a bug, do not try to fix it here! Rewrite this by using
- # the method proposed in the last Q'n'A below. That way the new function will
- # work just as well, be just as simple, but it wont need any new workarounds.
- # If you insist on fixing it here, look at the workarounds in the function
- # sympy_expression_namespace and in lambdify.
- # Q: Why are you not using Python abstract syntax tree?
- # A: Because it is more complicated and not much more powerful in this case.
- # Q: What if I have Symbol('sin') or g=Function('f')?
- # A: You will break the algorithm. We should use srepr to defend against this?
- # The problem with Symbol('sin') is that it will be printed as 'sin'. The
- # parser will distinguish it from the function 'sin' because functions are
- # detected thanks to the opening parenthesis, but the lambda expression won't
- # understand the difference if we have also the sin function.
- # The solution (complicated) is to use srepr and maybe ast.
- # The problem with the g=Function('f') is that it will be printed as 'f' but in
- # the global namespace we have only 'g'. But as the same printer is used in the
- # constructor of the namespace there will be no problem.
- # Q: What if some of the printers are not printing as expected?
- # A: The algorithm wont work. You must use srepr for those cases. But even
- # srepr may not print well. All problems with printers should be considered
- # bugs.
- # Q: What about _imp_ functions?
- # A: Those are taken care for by evalf. A special case treatment will work
- # faster but it's not worth the code complexity.
- # Q: Will ast fix all possible problems?
- # A: No. You will always have to use some printer. Even srepr may not work in
- # some cases. But if the printer does not work, that should be considered a
- # bug.
- # Q: Is there same way to fix all possible problems?
- # A: Probably by constructing our strings ourself by traversing the (func,
- # args) tree and creating the namespace at the same time. That actually sounds
- # good.
- from sympy.external import import_module
- import warnings
- #TODO debugging output
- class vectorized_lambdify:
- """ Return a sufficiently smart, vectorized and lambdified function.
- Returns only reals.
- Explanation
- ===========
- This function uses experimental_lambdify to created a lambdified
- expression ready to be used with numpy. Many of the functions in SymPy
- are not implemented in numpy so in some cases we resort to Python cmath or
- even to evalf.
- The following translations are tried:
- only numpy complex
- - on errors raised by SymPy trying to work with ndarray:
- only Python cmath and then vectorize complex128
- When using Python cmath there is no need for evalf or float/complex
- because Python cmath calls those.
- This function never tries to mix numpy directly with evalf because numpy
- does not understand SymPy Float. If this is needed one can use the
- float_wrap_evalf/complex_wrap_evalf options of experimental_lambdify or
- better one can be explicit about the dtypes that numpy works with.
- Check numpy bug http://projects.scipy.org/numpy/ticket/1013 to know what
- types of errors to expect.
- """
- def __init__(self, args, expr):
- self.args = args
- self.expr = expr
- self.np = import_module('numpy')
- self.lambda_func_1 = experimental_lambdify(
- args, expr, use_np=True)
- self.vector_func_1 = self.lambda_func_1
- self.lambda_func_2 = experimental_lambdify(
- args, expr, use_python_cmath=True)
- self.vector_func_2 = self.np.vectorize(
- self.lambda_func_2, otypes=[complex])
- self.vector_func = self.vector_func_1
- self.failure = False
- def __call__(self, *args):
- np = self.np
- try:
- temp_args = (np.array(a, dtype=complex) for a in args)
- results = self.vector_func(*temp_args)
- results = np.ma.masked_where(
- np.abs(results.imag) > 1e-7 * np.abs(results),
- results.real, copy=False)
- return results
- except ValueError:
- if self.failure:
- raise
- self.failure = True
- self.vector_func = self.vector_func_2
- warnings.warn(
- 'The evaluation of the expression is problematic. '
- 'We are trying a failback method that may still work. '
- 'Please report this as a bug.')
- return self.__call__(*args)
- class lambdify:
- """Returns the lambdified function.
- Explanation
- ===========
- This function uses experimental_lambdify to create a lambdified
- expression. It uses cmath to lambdify the expression. If the function
- is not implemented in Python cmath, Python cmath calls evalf on those
- functions.
- """
- def __init__(self, args, expr):
- self.args = args
- self.expr = expr
- self.lambda_func_1 = experimental_lambdify(
- args, expr, use_python_cmath=True, use_evalf=True)
- self.lambda_func_2 = experimental_lambdify(
- args, expr, use_python_math=True, use_evalf=True)
- self.lambda_func_3 = experimental_lambdify(
- args, expr, use_evalf=True, complex_wrap_evalf=True)
- self.lambda_func = self.lambda_func_1
- self.failure = False
- def __call__(self, args):
- try:
- #The result can be sympy.Float. Hence wrap it with complex type.
- result = complex(self.lambda_func(args))
- if abs(result.imag) > 1e-7 * abs(result):
- return None
- return result.real
- except (ZeroDivisionError, OverflowError):
- return None
- except TypeError as e:
- if self.failure:
- raise e
- if self.lambda_func == self.lambda_func_1:
- self.lambda_func = self.lambda_func_2
- return self.__call__(args)
- self.failure = True
- self.lambda_func = self.lambda_func_3
- warnings.warn(
- 'The evaluation of the expression is problematic. '
- 'We are trying a failback method that may still work. '
- 'Please report this as a bug.', stacklevel=2)
- return self.__call__(args)
- def experimental_lambdify(*args, **kwargs):
- l = Lambdifier(*args, **kwargs)
- return l
- class Lambdifier:
- def __init__(self, args, expr, print_lambda=False, use_evalf=False,
- float_wrap_evalf=False, complex_wrap_evalf=False,
- use_np=False, use_python_math=False, use_python_cmath=False,
- use_interval=False):
- self.print_lambda = print_lambda
- self.use_evalf = use_evalf
- self.float_wrap_evalf = float_wrap_evalf
- self.complex_wrap_evalf = complex_wrap_evalf
- self.use_np = use_np
- self.use_python_math = use_python_math
- self.use_python_cmath = use_python_cmath
- self.use_interval = use_interval
- # Constructing the argument string
- # - check
- if not all(isinstance(a, Symbol) for a in args):
- raise ValueError('The arguments must be Symbols.')
- # - use numbered symbols
- syms = numbered_symbols(exclude=expr.free_symbols)
- newargs = [next(syms) for _ in args]
- expr = expr.xreplace(dict(zip(args, newargs)))
- argstr = ', '.join([str(a) for a in newargs])
- del syms, newargs, args
- # Constructing the translation dictionaries and making the translation
- self.dict_str = self.get_dict_str()
- self.dict_fun = self.get_dict_fun()
- exprstr = str(expr)
- newexpr = self.tree2str_translate(self.str2tree(exprstr))
- # Constructing the namespaces
- namespace = {}
- namespace.update(self.sympy_atoms_namespace(expr))
- namespace.update(self.sympy_expression_namespace(expr))
- # XXX Workaround
- # Ugly workaround because Pow(a,Half) prints as sqrt(a)
- # and sympy_expression_namespace can not catch it.
- from sympy.functions.elementary.miscellaneous import sqrt
- namespace.update({'sqrt': sqrt})
- namespace.update({'Eq': lambda x, y: x == y})
- namespace.update({'Ne': lambda x, y: x != y})
- # End workaround.
- if use_python_math:
- namespace.update({'math': __import__('math')})
- if use_python_cmath:
- namespace.update({'cmath': __import__('cmath')})
- if use_np:
- try:
- namespace.update({'np': __import__('numpy')})
- except ImportError:
- raise ImportError(
- 'experimental_lambdify failed to import numpy.')
- if use_interval:
- namespace.update({'imath': __import__(
- 'sympy.plotting.intervalmath', fromlist=['intervalmath'])})
- namespace.update({'math': __import__('math')})
- # Construct the lambda
- if self.print_lambda:
- print(newexpr)
- eval_str = 'lambda %s : ( %s )' % (argstr, newexpr)
- self.eval_str = eval_str
- exec("MYNEWLAMBDA = %s" % eval_str, namespace)
- self.lambda_func = namespace['MYNEWLAMBDA']
- def __call__(self, *args, **kwargs):
- return self.lambda_func(*args, **kwargs)
- ##############################################################################
- # Dicts for translating from SymPy to other modules
- ##############################################################################
- ###
- # builtins
- ###
- # Functions with different names in builtins
- builtin_functions_different = {
- 'Min': 'min',
- 'Max': 'max',
- 'Abs': 'abs',
- }
- # Strings that should be translated
- builtin_not_functions = {
- 'I': '1j',
- # 'oo': '1e400',
- }
- ###
- # numpy
- ###
- # Functions that are the same in numpy
- numpy_functions_same = [
- 'sin', 'cos', 'tan', 'sinh', 'cosh', 'tanh', 'exp', 'log',
- 'sqrt', 'floor', 'conjugate',
- ]
- # Functions with different names in numpy
- numpy_functions_different = {
- "acos": "arccos",
- "acosh": "arccosh",
- "arg": "angle",
- "asin": "arcsin",
- "asinh": "arcsinh",
- "atan": "arctan",
- "atan2": "arctan2",
- "atanh": "arctanh",
- "ceiling": "ceil",
- "im": "imag",
- "ln": "log",
- "Max": "amax",
- "Min": "amin",
- "re": "real",
- "Abs": "abs",
- }
- # Strings that should be translated
- numpy_not_functions = {
- 'pi': 'np.pi',
- 'oo': 'np.inf',
- 'E': 'np.e',
- }
- ###
- # Python math
- ###
- # Functions that are the same in math
- math_functions_same = [
- 'sin', 'cos', 'tan', 'asin', 'acos', 'atan', 'atan2',
- 'sinh', 'cosh', 'tanh', 'asinh', 'acosh', 'atanh',
- 'exp', 'log', 'erf', 'sqrt', 'floor', 'factorial', 'gamma',
- ]
- # Functions with different names in math
- math_functions_different = {
- 'ceiling': 'ceil',
- 'ln': 'log',
- 'loggamma': 'lgamma'
- }
- # Strings that should be translated
- math_not_functions = {
- 'pi': 'math.pi',
- 'E': 'math.e',
- }
- ###
- # Python cmath
- ###
- # Functions that are the same in cmath
- cmath_functions_same = [
- 'sin', 'cos', 'tan', 'asin', 'acos', 'atan',
- 'sinh', 'cosh', 'tanh', 'asinh', 'acosh', 'atanh',
- 'exp', 'log', 'sqrt',
- ]
- # Functions with different names in cmath
- cmath_functions_different = {
- 'ln': 'log',
- 'arg': 'phase',
- }
- # Strings that should be translated
- cmath_not_functions = {
- 'pi': 'cmath.pi',
- 'E': 'cmath.e',
- }
- ###
- # intervalmath
- ###
- interval_not_functions = {
- 'pi': 'math.pi',
- 'E': 'math.e'
- }
- interval_functions_same = [
- 'sin', 'cos', 'exp', 'tan', 'atan', 'log',
- 'sqrt', 'cosh', 'sinh', 'tanh', 'floor',
- 'acos', 'asin', 'acosh', 'asinh', 'atanh',
- 'Abs', 'And', 'Or'
- ]
- interval_functions_different = {
- 'Min': 'imin',
- 'Max': 'imax',
- 'ceiling': 'ceil',
- }
- ###
- # mpmath, etc
- ###
- #TODO
- ###
- # Create the final ordered tuples of dictionaries
- ###
- # For strings
- def get_dict_str(self):
- dict_str = dict(self.builtin_not_functions)
- if self.use_np:
- dict_str.update(self.numpy_not_functions)
- if self.use_python_math:
- dict_str.update(self.math_not_functions)
- if self.use_python_cmath:
- dict_str.update(self.cmath_not_functions)
- if self.use_interval:
- dict_str.update(self.interval_not_functions)
- return dict_str
- # For functions
- def get_dict_fun(self):
- dict_fun = dict(self.builtin_functions_different)
- if self.use_np:
- for s in self.numpy_functions_same:
- dict_fun[s] = 'np.' + s
- for k, v in self.numpy_functions_different.items():
- dict_fun[k] = 'np.' + v
- if self.use_python_math:
- for s in self.math_functions_same:
- dict_fun[s] = 'math.' + s
- for k, v in self.math_functions_different.items():
- dict_fun[k] = 'math.' + v
- if self.use_python_cmath:
- for s in self.cmath_functions_same:
- dict_fun[s] = 'cmath.' + s
- for k, v in self.cmath_functions_different.items():
- dict_fun[k] = 'cmath.' + v
- if self.use_interval:
- for s in self.interval_functions_same:
- dict_fun[s] = 'imath.' + s
- for k, v in self.interval_functions_different.items():
- dict_fun[k] = 'imath.' + v
- return dict_fun
- ##############################################################################
- # The translator functions, tree parsers, etc.
- ##############################################################################
- def str2tree(self, exprstr):
- """Converts an expression string to a tree.
- Explanation
- ===========
- Functions are represented by ('func_name(', tree_of_arguments).
- Other expressions are (head_string, mid_tree, tail_str).
- Expressions that do not contain functions are directly returned.
- Examples
- ========
- >>> from sympy.abc import x, y, z
- >>> from sympy import Integral, sin
- >>> from sympy.plotting.experimental_lambdify import Lambdifier
- >>> str2tree = Lambdifier([x], x).str2tree
- >>> str2tree(str(Integral(x, (x, 1, y))))
- ('', ('Integral(', 'x, (x, 1, y)'), ')')
- >>> str2tree(str(x+y))
- 'x + y'
- >>> str2tree(str(x+y*sin(z)+1))
- ('x + y*', ('sin(', 'z'), ') + 1')
- >>> str2tree('sin(y*(y + 1.1) + (sin(y)))')
- ('', ('sin(', ('y*(y + 1.1) + (', ('sin(', 'y'), '))')), ')')
- """
- #matches the first 'function_name('
- first_par = re.search(r'(\w+\()', exprstr)
- if first_par is None:
- return exprstr
- else:
- start = first_par.start()
- end = first_par.end()
- head = exprstr[:start]
- func = exprstr[start:end]
- tail = exprstr[end:]
- count = 0
- for i, c in enumerate(tail):
- if c == '(':
- count += 1
- elif c == ')':
- count -= 1
- if count == -1:
- break
- func_tail = self.str2tree(tail[:i])
- tail = self.str2tree(tail[i:])
- return (head, (func, func_tail), tail)
- @classmethod
- def tree2str(cls, tree):
- """Converts a tree to string without translations.
- Examples
- ========
- >>> from sympy.abc import x, y, z
- >>> from sympy import sin
- >>> from sympy.plotting.experimental_lambdify import Lambdifier
- >>> str2tree = Lambdifier([x], x).str2tree
- >>> tree2str = Lambdifier([x], x).tree2str
- >>> tree2str(str2tree(str(x+y*sin(z)+1)))
- 'x + y*sin(z) + 1'
- """
- if isinstance(tree, str):
- return tree
- else:
- return ''.join(map(cls.tree2str, tree))
- def tree2str_translate(self, tree):
- """Converts a tree to string with translations.
- Explanation
- ===========
- Function names are translated by translate_func.
- Other strings are translated by translate_str.
- """
- if isinstance(tree, str):
- return self.translate_str(tree)
- elif isinstance(tree, tuple) and len(tree) == 2:
- return self.translate_func(tree[0][:-1], tree[1])
- else:
- return ''.join([self.tree2str_translate(t) for t in tree])
- def translate_str(self, estr):
- """Translate substrings of estr using in order the dictionaries in
- dict_tuple_str."""
- for pattern, repl in self.dict_str.items():
- estr = re.sub(pattern, repl, estr)
- return estr
- def translate_func(self, func_name, argtree):
- """Translate function names and the tree of arguments.
- Explanation
- ===========
- If the function name is not in the dictionaries of dict_tuple_fun then the
- function is surrounded by a float((...).evalf()).
- The use of float is necessary as np.<function>(sympy.Float(..)) raises an
- error."""
- if func_name in self.dict_fun:
- new_name = self.dict_fun[func_name]
- argstr = self.tree2str_translate(argtree)
- return new_name + '(' + argstr
- elif func_name in ['Eq', 'Ne']:
- op = {'Eq': '==', 'Ne': '!='}
- return "(lambda x, y: x {} y)({}".format(op[func_name], self.tree2str_translate(argtree))
- else:
- template = '(%s(%s)).evalf(' if self.use_evalf else '%s(%s'
- if self.float_wrap_evalf:
- template = 'float(%s)' % template
- elif self.complex_wrap_evalf:
- template = 'complex(%s)' % template
- # Wrapping should only happen on the outermost expression, which
- # is the only thing we know will be a number.
- float_wrap_evalf = self.float_wrap_evalf
- complex_wrap_evalf = self.complex_wrap_evalf
- self.float_wrap_evalf = False
- self.complex_wrap_evalf = False
- ret = template % (func_name, self.tree2str_translate(argtree))
- self.float_wrap_evalf = float_wrap_evalf
- self.complex_wrap_evalf = complex_wrap_evalf
- return ret
- ##############################################################################
- # The namespace constructors
- ##############################################################################
- @classmethod
- def sympy_expression_namespace(cls, expr):
- """Traverses the (func, args) tree of an expression and creates a SymPy
- namespace. All other modules are imported only as a module name. That way
- the namespace is not polluted and rests quite small. It probably causes much
- more variable lookups and so it takes more time, but there are no tests on
- that for the moment."""
- if expr is None:
- return {}
- else:
- funcname = str(expr.func)
- # XXX Workaround
- # Here we add an ugly workaround because str(func(x))
- # is not always the same as str(func). Eg
- # >>> str(Integral(x))
- # "Integral(x)"
- # >>> str(Integral)
- # "<class 'sympy.integrals.integrals.Integral'>"
- # >>> str(sqrt(x))
- # "sqrt(x)"
- # >>> str(sqrt)
- # "<function sqrt at 0x3d92de8>"
- # >>> str(sin(x))
- # "sin(x)"
- # >>> str(sin)
- # "sin"
- # Either one of those can be used but not all at the same time.
- # The code considers the sin example as the right one.
- regexlist = [
- r'<class \'sympy[\w.]*?.([\w]*)\'>$',
- # the example Integral
- r'<function ([\w]*) at 0x[\w]*>$', # the example sqrt
- ]
- for r in regexlist:
- m = re.match(r, funcname)
- if m is not None:
- funcname = m.groups()[0]
- # End of the workaround
- # XXX debug: print funcname
- args_dict = {}
- for a in expr.args:
- if (isinstance(a, Symbol) or
- isinstance(a, NumberSymbol) or
- a in [I, zoo, oo]):
- continue
- else:
- args_dict.update(cls.sympy_expression_namespace(a))
- args_dict.update({funcname: expr.func})
- return args_dict
- @staticmethod
- def sympy_atoms_namespace(expr):
- """For no real reason this function is separated from
- sympy_expression_namespace. It can be moved to it."""
- atoms = expr.atoms(Symbol, NumberSymbol, I, zoo, oo)
- d = {}
- for a in atoms:
- # XXX debug: print 'atom:' + str(a)
- d[str(a)] = a
- return d
|