-
-
Notifications
You must be signed in to change notification settings - Fork 10.9k
BUG: Fix crash on 0d return value in apply_along_axis #8441
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
Conversation
3c6ea94
to
daa4f4e
Compare
numpy/lib/shape_base.py
Outdated
# save the first result | ||
outarr_view[tuple(ind)] = res | ||
k = 1 | ||
while k < Ntot: |
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.
Is this really likely to be faster than using ndindex
here?
f71bedf
to
27cd75b
Compare
numpy/lib/shape_base.py
Outdated
inarr_view = transpose(arr, dims_in[:axis] + dims_in[axis+1:] + [axis]) | ||
|
||
# indices | ||
inds = ndindex(inarr_view.shape[:nd-1]) |
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.
To be honest, most of this function could probably be replaced with a carefully constructed nditer
that does the transpose
s for us, but that's probably not going to increase performance much anyway
Right now, this happens: >>> evil = lambda x: 1 if x.all() else [2, 3]
# silent broadcasting
>>> apply_along_axis(evil, 0, np.array([[False, True, True]]))))
array([[2, 1, 1],
[3, 1, 1]])
# same function, different order
>>> apply_along_axis(evil, 0, np.array([[True, False, True]]))))
ValueError: setting an array element with a sequence. Would it be better if both raised |
8d761e9
to
3a159d4
Compare
numpy/lib/shape_base.py
Outdated
if not isinstance(res, matrix): | ||
outarr = res.__array_prepare__(outarr) | ||
if outshape != outarr.shape: | ||
raise ValueError('__array_prepare__ should not change the shape of the resultant array') |
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.
Is this assumption (__array_prepare__(x).shape == x.shape
) supposed to be valid? How do we deal with matrix
violating it elsewhere?
numpy/lib/tests/test_shape_base.py
Outdated
) | ||
|
||
result = apply_along_axis(double, 1, m) | ||
assert isinstance(result, np.matrix) |
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.
Note that assert shouldn't be u 10000 sed at all, use np.testing.assert_ instead (applies to multiple lines below as well)
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.
In my defense, those other lines were not part of this patch. Do you want me to fix all cases of assert in this file anyway?
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.
Or should this be self.assertIsInstance
?
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.
Changed to assert_(isinstance(...))
, because that's what other tests do. Switching to self.assertIsInstance
can be discussed elsewhere
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.
In my defense, those other lines were not part of this patch. Do you want me to fix all cases of assert in this file anyway?
if you could, that'd be useful
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.
self.assertIsInstance
seems fine to use if you prefer that, it has a sane implementation in both unittest
and unittest2
.
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.
@rgommers: I've patched the other ones in that file
Ugh, this needs a patch in |
numpy/lib/shape_base.py
Outdated
outarr = zeros(outshape, res.dtype) | ||
# matrices call reshape in __array_prepare__, and this is apparently ok! | ||
if not isinstance(res, matrix): | ||
outarr = res.__array_prepare__(outarr) |
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.
Can someone comment on whether this is actually correct, or if it can just be omitted altogether
__array_prepare__
seems to always do something screwy for builtin array subclasses. It's not clear to me what the contract entails, and whether I'm violating it, or np.matrix
and np.ma.MaskedArray
are
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.
@mhvk may know
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.
I think MaskedConstant.__array_prepare__
was broken - fixed in #8508. I think a corollary of this is that np.ma.apply_along_axis
can basically defer now.
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.
I really like the approach here, thanks for tackling this clean-up!
numpy/lib/shape_base.py
Outdated
outarr = zeros(outshape, res.dtype) | ||
# matrices call reshape in __array_prepare__, and this is apparently ok! | ||
if not isinstance(res, matrix): | ||
outarr = res.__array_prepare__(outarr) |
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.
@mhvk may know
numpy/lib/shape_base.py
Outdated
# save the first result | ||
outarr_view[ind0] = res | ||
for ind in inds: | ||
outarr_view[ind] = asanyarray(func1d(inarr_view[ind], *args, **kwargs)) |
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.
I recognize that this works to update the view inplace, but it still makes me a little nervous. It would be more direct and obviously correct to do the transposing afterwards.
This would change the memory layout of the result array. Transposing afterwards would ensure that array is written with contiguous writes, which could result in a marginal performance improvement (at least for this function).
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.
Yep, my goal here was to leave the memory layout of the result array unaffected when compared to the old version.
In particular, the old code would always give a contiguous output, something that may have been relied upon
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.
I think we reserve the right to change the memory layout of arrays returned by NumPy functions unless it is documented as part of the API. Users who truly rely on contiguous output should be using np.ascontiguousarray
.
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.
I guess transposing afterwards would fix the above issue. If you don't think a contiguous output is beneficial, then I'll move the transpose
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.
We have done it before (e.g. even indexing I did it). If it is obvious/possible, then "the same as before" should be preferred most of the time I would say.
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.
I'd argue that the correctness is more obvious this way around, because otherwise the inverse transpose order is needed, which is more troublesome to generate
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.
I mostly like transposing afterwards more. I don't care so much about the memory layout.
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.
Actually I think you're right, it is simpler - the transpose order becomes (more complicated than that), which better conveys the insertion of the added axesdims_out[:axis] + dims_out[-res.ndim:] + dims_out[axis+1:]
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.
Should the transpose happen before or after the __array_wrap__
?
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.
Ok, I'm sold on your suggestion, but for a different reason: when res.__array_prepare__
is called, the axes of res
(a single output) and the final output are paired as if they were broadcasted, which seems desirable for any kind of axis metadata copying
numpy/lib/shape_base.py
Outdated
|
||
# arr, with the iteration axis at the end | ||
dims_in = list(range(nd)) | ||
inarr_view = transpose(arr, dims_in[:axis] + dims_in[axis+1:] + [axis]) |
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 assumes that transpose
always returns a view (and not sometimes a copy). is this acceptable?
(edit: updated mailing list url, which was dead. Not sure it points to the right thing any longer. https://mail.python.org/pipermail/numpy-discussion/2013-June/066822.html might be what I meant)
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.
Actually, only the output transpose
does, which only matters if we use __array_prepare__
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.
As @seberg writes in that mailing list discussion, transpose on base numpy arrays always returns a view, never a copy.
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.
But this might indeed break in surprising ways for ndarray subclasses
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.
@shoyer: Would an acceptable compromise be to not call __array_prepare__
if transpose
is a copy?
ce5a18e
to
8dba2ba
Compare
numpy/lib/shape_base.py
Outdated
arr = asanyarray(arr) | ||
nd = arr.ndim | ||
if axis < 0: | ||
axis += nd | ||
if (axis >= nd): | ||
if axis >= nd: |
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.
while we're at it, also check that axis is not still negative?
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.
I like the idea of killing this altogether, and delegating parsing axis
to moveaxis
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.
Alarmingly, I can't even find a precedent where numpy checks for this anywhere else within shapebase
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.
@eric-wieser - when you add the changelog, could you also move this ch
179B
eck up to before axis
is possibly changed and check for too-negative as well? That way, the error message gives the actual input rather than something that's already changed. I.e.,
if axis < -nd or axis >= nd:
raise ...
if axis < 0:
axis += nd
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.
@mhvk: Done
return outarr | ||
|
||
# arr, with the iteration axis at the end | ||
in_dims = list(range(nd)) |
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.
When I looked at this first, I thought you might consider using the (relatively new) np.moveaxis
, i.e.,
inarr_view = moveaxis(inarr, axis, -1)
This will not be any faster (as it just sets up a transpose
, like here), but is perhaps clearer. As a side benefit, you can remove above the checks on the validity of axis
, as moveaxis
does those anyway.
However, looking further down, it is not obvious moveaxis
would work there, since res.ndim
could in principle be >1-d (for which your code adds support, which I think is very nice!). But one could steal a bit from the moveaxis
code and write here
in_permute = [n for n in range(nd) if n != axis] + [axis]
inarr_view = transpose(arr, in_permute)
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.
Oh, good point. moveaxis
works with multiple axes too, so should work in both cases.
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.
For restoring axes, you could maybe write something like:
moveaxis(pre_arr, [i + nd - 1 for i in range(res.ndim)],
[i + axis for i in range(res.ndim)])
I think this is a slight improvement in clarity over building up the permutation axes for transpose directly.
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.
I think moveaxis
should learn a shorthand for dest such that this would work:
moveaxis(pre_arr, [i + nd - 1 for i in range(res.ndim)], axis)
Ie, if dest is a scalar, move all the source axes to that location, in the order they were passed in src
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.
numpy/lib/shape_base.py
Outdated
inarr_view = transpose(arr, in_dims[:axis] + in_dims[axis+1:] + [axis]) | ||
|
||
# compute indices for the iteration axes | ||
inds = ndindex(inarr_view.shape[:nd-1]) |
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.
Really like using the iterator here. Much cleaner!
numpy/lib/shape_base.py
Outdated
# remove the requested axis, and add the new ones on the end. | ||
# laid out so that each write is contiguous. | ||
# for a tuple index inds, pre_arr[inds] = func1d(inarr_view[inds]) | ||
pre_shape = arr.shape[:axis] + arr.shape[axis+1:] + res.shape |
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.
Here you can just use the shape of inarr_view
, i.e.,
pre_shape = inarr_view.shape + res.shape
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.
Nice!
numpy/lib/shape_base.py
Outdated
|
||
# permutation of axes such that out = pre_arr.transpose(pre_permute) | ||
pre_dims = list(range(pre_arr.ndim)) | ||
pre_permute = pre_dims[:axis] + list(roll(pre_dims[axis:], res.ndim)) |
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 a bit faster (no list->ndarray->list) and clearer (to me at least):
if res.ndim > 0:
pre_permute = pre_dims[:axis] + pre_dims[-res.ndim:] + pre_dims[axis+red.ndim:]
(you'd need to rename pre_dims
to pre_permute
if you want the transpose at the end even for res.ndim=0
)
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.
Yeah, that line was inspired by a talk extolling the virtues of std::rotate
.
Had that before, but without the if - as you correctly spot, it fails when res.ndim == 0
. Not a huge fan of special casing that.
Either way, I reckon moveaxis will do the job here too.
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.
Or how about just
pre_permute = pre_dims[:axis] + pre_dims[nd-res.ndim:] + pre_dims[axis+red.ndim:]
Adding the nd
there fixes the error when res.ndim= 0
numpy/lib/shape_base.py
Outdated
pre_arr = res.__array_wrap__(pre_arr) | ||
|
||
# finally, rotate the inserted axes back to where they belong | ||
return transpose(pre_arr, pre_permute) |
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.
For speed for the more common case (and required with my comment above):
return pre_arr if res.ndim == 0 else transpose(pre_arr, pre_permute)
numpy/lib/shape_base.py
Outdated
outarr = outarr.squeeze(axis) | ||
return outarr | ||
# matrices have to be transposed first, because they collapse dimensions! | ||
out_arr = transpose(pre_arr, pre_permute) |
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.
same as above
numpy/lib/tests/test_shape_base.py
Outdated
result = apply_along_axis(double, 0, m) | ||
assert isinstance(result, np.matrix) | ||
assert_(isinstance(result, np.matrix)) | ||
assert_array_equal( |
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.
Just on one line?
assert_array_equal(result, expected)
numpy/lib/tests/test_shape_base.py
Outdated
assert_array_equal( | ||
result, np.matrix([[0, 2], [4, 6]]) | ||
result, expected | ||
) |
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.
same
@eric-wieser - this is very nice indeed, I like the generality, and the care for subclasses. My comments are all small, but hopefully make this a little better still. |
I'll do another round of clean up tomorrow to address those |
I think I'd go for your approach rather than |
numpy/lib/shape_base.py
Outdated
@@ -103,11 +103,10 @@ def apply_along_axis(func1d, axis, arr, *args, **kwargs): | |||
% (axis, nd)) | |||
|
|||
# arr, with the iteration axis at the end | |||
in_dims = list(range(nd)) | |||
inarr_view = transpose(arr, in_dims[:axis] + in_dims[axis+1:] + [axis]) | |||
inarr_view = moveaxis(arr, axis, -1) |
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.
Beginning to think this would be a fair bit slower, due to the error checking in moveaxis, and isn't much clearer.
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.
Ooh, how about np.r_[:axis, axis+1:nd, axis]
060c5e7
to
7939511
Compare
Keep changing my mind on this one. I think it's best left without
I agree that the I'd rather not spend too much more time bikeshedding this. |
All reactions
Sorry, something went wrong.
7939511
to
7f4ab47
Compare
@eric-wieser - sorry for having brought up |
All reactions
Sorry, something went wrong.
numpy/lib/tests/test_shape_base.py
Outdated
|
||
def test_axis_insertion(self, cls=np.ndarray): | ||
a = np.arange(18).reshape((6, 3)) | ||
res = apply_along_axis(lambda x: np.diag(x).view(cls), 0, a) |
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.
Reading over these tests one last time, it occurs to me that we should be more comprehensive for testing insertion of new axes.
Right now you only test inserting at the start, but that doesn't really exercise the permutation logic in a comprehensive way. Let's also test inserting in the middle and at the end. Also, diag is axes order invariant, so none of the tests verify that new axes are inserted in the right order.
Sorry, something went wrong.
All reactions
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 outer product with its reverse would be better than diag then?
Sorry, something went wrong.
All reactions
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.
I think the outer product is still invariant to transposes. Something like x[:, np.newaxis] - x[np.newaxis, :]
would work, though.
Sorry, something went wrong.
All reactions
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.
On re-reading: yes, outer product with its reverse, would be fine
Sorry, something went wrong.
All reactions
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.
You didn't miss it the first time, I did indeed suggest a plain outer product beforehand, and realized my mistake. This is now done too.
Sorry, something went wrong.
All reactions
numpy/lib/tests/test_shape_base.py
Outdated
res = apply_along_axis(lambda x: np.diag(x).view(cls), 0, a) | ||
assert_(isinstance(res, cls)) | ||
assert_equal(res.ndim, 3) | ||
assert_array_equal(res[:,:,0], np.diag(a[:,0]).view(cls)) |
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.
A cleaner way to check this sort of thing is to construct the desired result array and just call assert_array_equal
(e.g., np.stack([np.diag(a[:,i]) for i in range(3)], axis=-1).view(cls)
). Otherwise it's easy to overlook one part of the equality check (e.g., shape, in this case).
Sorry, something went wrong.
All reactions
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.
@shoyer: I was hoping I could be done with this, but you are definitely right. I'll fix those things up soon
Sorry, something went wrong.
All reactions
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.
Ok, done. (except for masked arrays, where I don't think assert_equal does the right thing)
Sorry, something went wrong.
All reactions
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.
LGTM. Will merge this shortly if no one else has further comments.
Sorry, something went wrong.
All reactions
@eric-wieser could you please clean up the git history a little bit? Thanks |
All reactions
Sorry, something went wrong.
@shoyer: Fair, I'll give that a go over the weekend. |
All reactions
Sorry, something went wrong.
Also: ENH: Support arbitrary dimensionality of return value MAINT: remove special casing
.transpose does not specify that it must return a view, so subclasses (like np.ma.array) could otherwise break this. This exposes some more need for matrix special casing.
Note that this is not a full subsitute for np.ma.apply_along_axis, as that allows functions to return a mix of np.ma.masked and scalars
Copied from the implementation in core.shape_base.stack
5435697
to
d4bce01
Compare
@shoyer: Down from 13 commits to 8 commits. Look ok? I've confirmed that the final commit of these squashes is identical to the result of rebasing this branch on master. (possibly ignoring merges in the release notes) |
All reactions
Sorry, something went wrong.
I would usually squash down to one commit, but this is fine. |
All reactions
Sorry, something went wrong.
seberg
rgommers
mhvk
shoyer
Successfully merging this pull request may close these issues.
Also:
ENH: Support arbitrary dimensionality of return value
MAINT: remove special casing
Now supports
Addresses discussion from #8363