-
-
Notifications
You must be signed in to change notification settings - Fork 4.4k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
numpy lambdify of piecewise doesn't work for invalid values #11306
Comments
Wrong PR. It was changed at #9657. |
Why was it changed to use In [31]: class MyNumPyPrinter(NumPyPrinter):
....: def _print_Piecewise(self, expr):
....: return LambdaPrinter._print_Piecewise(self, expr)
....:
In [33]: lambdify([x, y], p, modules='numpy', printer=MyNumPyPrinter)(0, 1)
Out[33]: 0 |
The reason we don't use if/then/else is that it breaks as soon as you try to pass array arguments: lambdify([x, y], p, modules='numpy', printer=MyNumPyPrinter)(0, np.array([1, 0])) ValueError: The truth value of an array with more than one element is ambiguous. Use a.any() or a.all() However you are correct to point out that using def _print_Piecewise(self, expr):
"Piecewise function printer"
exprs = [self._print(arg.expr) for arg in expr.args]
conds = [self._print(arg.cond) for arg in expr.args]
if (len(conds) == 1) and exprs[0] == '0':
return '0'
# We expect exprs and conds to be in order here
# First condition to evaluate to True is returned
# Default result: float zero
result = 'where({0}, {1}, 0.)'.format(conds[-1], exprs[-1])
for expr, cond in reversed(list(zip(exprs[:-1], conds[:-1]))):
result = 'where({0}, {1}, {2})'.format(cond, expr, result)
return result I don't remember exactly why I chose to use I'll put together the PR to fix this. |
(Yes, it had to do with trying to make piecewise broadcasting work in numba, before I fully understood how |
Your printer that uses where doesn't fix the problem. It prints this Piecewise as
which still evaluates every instance of |
In fact, given that Python is an eagerly evaluated language, I don't see how it's more efficient either. |
Of course you're correct that all the arguments to |
(The lazy evaluation misconception must have come from something I was doing with numba.) I can't think of a good solution to this until we have the ability to do array allocations in the code generator. NumPy, like Python, evaluates all function arguments eagerly. The only way to prevent a Piecewise represented in NumPy from evaluating an expression which will raise is to use Python's built-in branching (if/else), but that branching is not going to handle array argument broadcasting. The NumPy docs seem to indicate that you should be able to use |
Are we already setting that flag? The default numpy behavior is
|
Oh, it evaluates 1/0 in Python. So is the solution to wrap any divided expressions with |
In other words, this works: >>> from numpy import *
>>> l = lambda x, y: select([less(y, -1),less_equal(y, 1),True], [1/array(x),x,1/array(x)], default=nan)
>>> l(0, 1)
array(0.0)
>>> l(0, array([0, 1]))
array([ 0., 0.]) |
That doesn't change the inefficiency issue, though, but as far as I know no one has complained about that yet. |
I guess |
Probably (I thought it was confusing that numpy's default behavior would be to raise on division by zero...) |
Looks like I have this commented out in my fork, where apparently I ran into this problem before: def _print_Integer(self, expr):
# Upcast to work around an integer overflow bug
return 'asarray({0}, dtype=float)'.format(expr) |
But the overflow issue is a good reminder. If something like |
What was the integer overflow bug? The right place to put this is in |
Interesting behavior:
|
Wrapping every symbol would do it, as then everything would be an array, but it's probably overkill. We really only need to protect denominators. |
p = x**y
lambdify([x, y], p, modules='numpy', printer=NumPyPrinter)(50, 10e40) OverflowError: (34, 'Numerical result out of range') |
Actually, a much cleaner solution is to wrap the input values as arrays: In [27]: lambdify([x, y], p, modules='numpy')(array(0), array(1))
/Users/aaronmeurer/anaconda3/lib/python3.5/site-packages/numpy/__init__.py:1: RuntimeWarning: divide by zero encountered in true_divide
"""
Out[27]: array(0.0) So we should make it so that An issue here is that doing this automatically would be a backwards compatibility break with the current way lambdify works, because currently it lets you mix and match modules, like |
My thought is we should probably do both (wrap inputs and arguments to |
Can you give an example of that? I'm unclear what you mean. |
Oddly, I can't seem to create an example. I don't see why this works: p = Piecewise((1/x, y < -1), (Pow(Integer(50), Integer(10e40), evaluate=False), y <= 1), (1/x, True))
lambdify([x, y], p, modules='numpy', printer=NumPyPrinter)(50, 0) inf |
Never mind, it doesn't actually work. I just had the Integer-wrapping thing in there by accident. If you run it normally, it just hangs. |
To get back to the main point, do you think we should just add a decorator to the lambdify'd function when called with numpy, like def array_wrap(func):
def wrapper(*args, **kwargs):
return func(*[np.asarray(i) for i in args], **kwargs)
return wrapper |
Yes (or the equivalent). Regarding the integer overflow thing, I'd consider that to be a separate issue. If it comes up in real problems, we can look at fixing it. The example you gave feels contrived, since evaluating |
Agreed. I'll write the PR to address this issue like you suggested, unless you have another idea. |
Fixes sympy/sympy#11306 // edited by skirpichev
Fixes sympy/sympy#11306 // edited by skirpichev Signed-off-by: Sergey B Kirpichev <skirpichev@gmail.com>
I was looking at this in relation to #14671, and found the following fails (assumes numpy is available):
The problem is that numpy is used by default, but the input argument wrapping is only enabled if numpy is explicitly requested: sympy/sympy/utilities/lambdify.py Lines 436 to 440 in f3bba6c
Is there a reason for the module_provided part of the test?
|
There's probably not a good reason. I think that change predates the inclusion of numpy as the default. |
I tried removing the sympy/sympy/utilities/tests/test_lambdify.py Lines 693 to 694 in 91adc74
with
|
I guess let's just leave it there for now. If we implement my suggestion from #14671 it won't matter because it would only affect the printing of Piecewise in the NumPyPrinter. |
The automatic wrapping of input arguments was to fix the behaviour of Piecewise expressions when passed to lambdify with the numpy module (issue sympy#11306). However this resulted in performance regressions for all code using lambdify with numpy (issue sympy#14671). This commit removes the automatic wrapping code, and updates the lambdify documentation to explain how to handle Piecewise with numpy.
This example is given at https://stackoverflow.com/questions/38040846/error-with-sympy-lambdify-for-piecewise-functions-and-numpy-module
The Piecewise is designed to avoid zero division for (0, 1), but the problem is that the numpy lambdify printer uses
select
which evaluates all values, and then picks the one corresponding to the first true selection.
ZeroDivisionErrors aside, this is also somewhat inefficient, and one could see this causing an unreasonable slowdown for a large piecewise, where all values are evaluated instead of just the one for the first true condition.
@richardotis you changed it to use
select
at #9749. Any thoughts?The text was updated successfully, but these errors were encountered: