-
-
Notifications
You must be signed in to change notification settings - Fork 4.3k
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
Use sum/fsum & prod/fprod in some python related printers #24685
Conversation
✅ Hi, I am the SymPy bot. I'm here to help you write a release notes entry. Please read the guide on how to write release notes.
Click here to see the pull request description that was parsed.
|
sympy/printing/numpy.py
Outdated
@@ -63,6 +63,14 @@ def _print_seq(self, seq): | |||
delimiter=', ' | |||
return '({},)'.format(delimiter.join(self._print(item) for item in seq)) | |||
|
|||
def _print_Add(self, e): | |||
return '{}({})'.format(self._module_format(self._module + '.sum'), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Doesn't this have the problem with #5642?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
maybe? would reduce(add, ...))
make a difference?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not for the particular issue
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@asmeurer I tried to figure out what a regression test would look like. You don't happen to able to construct one?
f48a7c5
to
4a2db98
Compare
In a situation where |
I think it would make sense if a setting is added to the python printer which holds the value of the recursion limit and then the behaviour is switched based on whether you would hit that limit. (then you can change that limit depending on what you want) Not sure how easy that would be though. E.g. is the limit reached also when you have a combination of multiplications and additions? As a side node, >>> import numpy as np
>>> A = np.array([1,2])
>>> sum([1, A])
array([2, 3])
>>> 1 + A
array([2, 3]) |
Right, my point is that we cannot replace our current generated Here's an example adapted from SO: In [1]: class p():
...: def __init__(self, x, y):
...: self.x=x ; self.y=y
...: def __repr__(self):
...: return "(%r,%r)"%(self.x,self.y)
...: def __add__(self, P):
...: return p(self.x+P.x, self.y+P.y)
...:
In [2]: pts=[p(1,0), p(2,1), p(-3,4)]
In [3]: pts[0] + pts[1] + pts[2]
Out[3]: (0,5)
In [4]: sum(pts)
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
<ipython-input-4-45922890d470> in <module>()
----> 1 sum(pts)
TypeError: unsupported operand type(s) for +: 'int' and 'instance'
In [5]: import operator
In [6]: import functools
In [7]: functools.reduce(operator.add, pts)
Out[7]: (0,5)
In [8]: sum(pts[1:], pts[0])
Out[8]: (0,5) |
It sounds expensive to Another option might be to add a limit on the number of arguments, above which |
I don't think RecursionError should be caught. Using reduce seems fine to me though. |
Timings with numpy arrays: In [32]: a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u,v,w,x,y,z = [np.ones(100)] * 26
In [33]: %timeit a+b+c+d+e+f+g+h+i+j+k+l+m+n+o+p+q+r+s+t+u+v+w+x+y+z
20.2 µs ± 280 ns per loop (mean ± std. dev. of 7 runs, 10000 loops each)
In [34]: %timeit reduce(add, [a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u,v,w,x,y,z])
22.6 µs ± 217 ns per loop (mean ± std. dev. of 7 runs, 10000 loops each)
In [35]: %timeit sum([b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u,v,w,x,y,z], a)
20 µs ± 190 ns per loop (mean ± std. dev. of 7 runs, 10000 loops each)
In [36]: %timeit a+b+c
1.71 µs ± 4.94 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)
In [37]: %timeit reduce(add, [a,b,c])
2.38 µs ± 21.3 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)
In [38]: %timeit sum([b,c], a)
1.99 µs ± 20.8 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each) With individual floats: In [39]: a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u,v,w,x,y,z = [1.0] * 26
In [40]: %timeit a+b+c+d+e+f+g+h+i+j+k+l+m+n+o+p+q+r+s+t+u+v+w+x+y+z
903 ns ± 5.48 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)
In [41]: %timeit reduce(add, [a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u,v,w,x,y,z])
1.59 µs ± 8.42 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)
In [42]: %timeit sum([b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u,v,w,x,y,z], a)
829 ns ± 3.05 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)
In [43]: %timeit a+b+c
117 ns ± 0.29 ns per loop (mean ± std. dev. of 7 runs, 10000000 loops each)
In [44]: %timeit reduce(add, [a,b,c])
361 ns ± 1.96 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)
In [45]: %timeit sum([b,c], a)
233 ns ± 1.07 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each) I guess that with enough terms |
This is the sort of issue you will run into with the NumPy printer: >>> np.array([1, 2]) + np.array([[1, 2], [1, 3]])
array([[2, 4],
[2, 5]])
>>> np.sum([np.array([1, 2]), np.array([[1, 2], [1, 3]])])
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<__array_function__ internals>", line 180, in sum
File "/Users/aaronmeurer/anaconda3/lib/python3.9/site-packages/numpy/core/fromnumeric.py", line 2296, in sum
return _wrapreduction(a, np.add, 'sum', axis, dtype, out, keepdims=keepdims,
File "/Users/aaronmeurer/anaconda3/lib/python3.9/site-packages/numpy/core/fromnumeric.py", line 86, in _wrapreduction
return ufunc.reduce(obj, axis, dtype, out, **passkwargs)
ValueError: could not broadcast input array from shape (2,2) into shape (2,) NumPy does implicitly treat a list of arrays as a larger array, but it doesn't implicitly broadcast those arrays. This is essentially the same problem as #5642 where we do this with matrices. It most often occurs with scalar values because those sort of happen automatically when an expression doesn't have any variables in it, but it applies to any broadcasting. Even when it sometimes does work, you get By the way, the code for NumPy should be passing >>> np.array([1, 2]) + np.array([1, 2])
array([2, 4])
>>> np.sum((np.array([1, 2]), np.array([1, 2])))
6 I think we can do this in this case by calling |
def __exit__(self, exc_t, exc_v, exc_tb): | ||
sys.setrecursionlimit(self._ori_limit) | ||
|
||
# TODO, reproduce using a smaller expression. (this takes too long...) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Something like this works:
lambdify(x, Add(*[x**i for i in range(10000)]))
mpmath has fsum https://mpmath.org/doc/current/general.html#fsum. According to its docstring it's faster and more accurate for more than 2 terms. |
Can't you avoid this by simply using the python buildin >>> sum([np.array([1, 2]), np.array([[1, 2], [1, 3]])])
array([[2, 4],
[2, 5]]) |
@@ -94,6 +94,8 @@ class AbstractPythonCodePrinter(CodePrinter): | |||
fully_qualified_modules=True, | |||
contract=False, | |||
standard='python3', | |||
num_terms_sum=42, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
honestly, 42 sounds pretty reasonable here, but it could also be just the actual limit such that the printer writes exactly the same code now as it did before except when that code would not be valid
Sure but |
sympy/printing/pycode.py
Outdated
# is already better served by the mpmath printer. And finally, subclassing and | ||
# overriding this remains an option. | ||
if len(expr.args) >= self._settings['num_terms_sum']: | ||
return 'sum([%s], start=%s)' % ( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this should use module_format
and builtins.sum
like here:
sympy/sympy/printing/pycode.py
Lines 271 to 280 in 677fc6b
def _print_Sum(self, expr): | |
loops = ( | |
'for {i} in range({a}, {b}+1)'.format( | |
i=self._print(i), | |
a=self._print(a), | |
b=self._print(b)) | |
for i, a, b in expr.limits) | |
return '(builtins.sum({function} {loops}))'.format( | |
function=self._print(expr.function), | |
loops=' '.join(loops)) |
(e.g. clashing import names/definitions)
Actually, now that I look at it, that code looks bugged since it doesn't use module_format :)
Although to be honest, the difference isn't as much as I expected, even for a huge number of arrays
|
>>> (arr_5_3_4 + arr_4 + arr_3_4).shape
(5, 3, 4)
>>> numpy.sum([arr_5_3_4, arr_4, arr_3_4], axis=0).shape
ValueError: setting an array element with a sequence. The requested array has an inhomogeneous shape after 1 dimensions. The detected shape was (3,) + inhomogeneous part.
EDIT: |
It's already there, unless I missed something? |
I won't have the time to pursue fixing this PR for the foreseeable future. |
Tentative fix for recusion error when using lambdify.
References to other Issues or PRs
Fixes #24673
Brief description of what is fixed or changed
Other comments
Perhaps this needs to be opt-in with some printer setting for backwards compatibility. (
lambdify
can then use that setting by default). Also, I have foundfunctools.reduce(operators.add, [...])
to be a better replacement forsum
in generic python code (but of course I can't remember an example right now).Release Notes
NO ENTRY