-
-
Notifications
You must be signed in to change notification settings - Fork 11k
Allow rolling multiple axes at the same time. #7438
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
Sounds like a good idea and straightforward generalization. Two comments:
|
shift : int or tuple of ints | ||
The number of places by which elements are shifted. If a tuple, | ||
then `axis` must be a tuple of the same size, and each of the | ||
given axes is shifted by the corresponding number. If an int |
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.
Would be good to add a ..versionadded:: thingy here for the sequence of ints (also slightly prefer sequence over tuple).
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 it should really be "array-like of ints"? I kept tuple mostly for consistency with e.g. np.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.
Oh, OK, no I prefer tuple or sequence of ints. Thought we used sequence mostly, but if we use tuple as well, just keep it as it is. It currently is more an "array-like of ints" in implementation, but I don't like that too much to be honest (you could put in a 2x2 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.
So to be clear. If you don't mind just ignoring the broadcasting for now, I think I would prefer something like:
try:
axes = tuple(axis)
except:
axes = (axis,)
or the inverted try: operator.index(axis)
. But if the broadcasting is important to you, maybe as is, is just simpler, hmmm.
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.
Well, I did not know that iterating over a broadcast object would result in nd-iteration (i.e. a 2x2 axis would get flattened out at that point).
I still like the broadcasting behavior, though.
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 you could just check broadcast(...).shape
to be 0 or 1D and raise an error otherwise, that would seem acceptable to me. Or multiply the length of the tuples manually if they are length 1, but probably that gets annoying.
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.
Coming back to the tuple-vs-sequence issue, I just noticed that sum
, for example, would error out if a non-tuple sequence is passed as the axis
argument. Not sure if that means sum
should be improved or we should only accept tuples to avoid weird edge 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.
I think it does not matter. In most of the python side stuff, we will allow any sequence and sometimes iterable (tuple(iterable) works). I don't think we have to strife for perfect consistency when it comes to tuples vs. sequences vs. iterables for the this type of arguments.
If you prefer, you can make it a strict tuple here as well, but basically I would pick whatever is easiest.
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.
It seems we got side tracked here and forgot about the versionadded tag. It would be good to mention somewhere that roll along multiple axes was added with this tag. IIRC it could either go here, or also into the Notes section.
The first implementation (one-axis-at-a-time, 79aae0ad4a31cc86a18a36f7c8028428f560a9cc) does not suffer from the slowdown (which is around 3 to 4-fold), but loses on multidimensional rolling (it rolls each axis one at a time). What is the status of |
|
||
x2r = np.roll(x2, (1, 1), axis=(0, 1)) | ||
assert_equal(x2r, np.array([[9, 5, 6, 7, 8], [4, 0, 1, 2, 3]])) | ||
|
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 would like some tests here that actually do roll at least two dimensions at the same time. And now that I think about it, what happens if you roll the same dimension twice ;)? Some simple error cases would be good, 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.
Oh, the last case does that kind of roll I guess. Negative roll might be interesting.
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.
Done.
Yeah, it probably is about that. But there is no definite schedule, so I think you also might as well put something in here now ;). Also there is a bit of a problem with subclass handeling, which might create problems. The other option would be to actually do the manual assignments and only use slicing. In some sense I think it is the best way, but probably also tedious. |
What's wrong with subclass handling? If a subclass messes up fancy indexing I can't do much about it... |
I think I would prefer to make |
(unless this function casts them to an array in any case, but I do not think it does) |
It casts using |
Yeah, which should be fine mostly, there are some wonky cases, but those are probably limited to
|
Neat, and faster than even the original (1D) implementation. |
Yeah, should be faster, except maybe for small arrays. Can you add a test for large rolls (rolls larger than the dimension)? Since I think there is a modulo operation missing as is and the test should have noticed that. |
Another point might be difference for very weird subclasses, hmmmmm. |
Added the missing modulo on the shift, and test cases. |
I was thinking about array wrap now. I forgot about things such as astropy units, which might be broken by the |
Astropy units seem to handle this fine. Masked arrays too. |
OK, if units work, I think we can give it a try as is. Did not go over the code again right now, but I think it should be fine, so will merge in a bit if no-one else has comments (or I notice something later). |
return res | ||
broadcasted = broadcast(shift, axis) | ||
if len(broadcasted.shape) > 1: | ||
raise ValueError("'axis' should be a scalar or a 1D sequence") |
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 maybe mention shift and axis.
Could you please squash the commits when done? |
cbebfcd<
8000
/code>
to
d3204c8
Compare
A quick test suggests that this implementation from @seberg, relying on slices rather than index arrays, is 1.5~3x faster than the previous (1D) roll (depending on the axis). Also switched the error message for invalid inputs to match the one of ufuncs, because the axis can actually also be negative.
Done. |
Thanks! lets give it a shot. |
Ah, will wait for the tests, so might not do it tonight ;). |
Thanks again. |
Nice was just looking for something like this to dedup some utility code. Is this going in 1.12.0 then? |
yep |
Also switched the error message to match the one of ufuncs, because the
axis can actually also be negative.
See #5867.