Repository navigation
ENH: add support for NEP 35 array constructors (empty, ones, zeros and full) with like=Quantity(...)) - #17120
Conversation
|
Thank you for your contribution to Astropy! 🌌 This checklist is meant to remind the package maintainers who will review this pull request of some common things to look for.
|
|
👋 Thank you for your draft pull request! Do you know that you can use |
e438875 to
355af1a
Compare
empty, ones, zeros and full) with like=Quantity(...)
057e42c to
97499cd
Compare
97499cd to
a540f7a
Compare
|
I'll let units maintainers to decide if this needs a What's New. Thanks! |
empty, ones, zeros and full) with like=Quantity(...)empty, ones, zeros and full) with like=Quantity(...))
mhvk
left a comment
There was a problem hiding this comment.
Looks good, but with a suggestion. Also, how about asarray?
|
|
||
| elif function in DISPATCHED_FUNCTIONS: | ||
| dispatched_function = DISPATCHED_FUNCTIONS[function] | ||
| if function in SUPPORTED_NEP35_FUNCTIONS: |
There was a problem hiding this comment.
If we instead move this to after the try/except, as
if unit is True and function in SUPPORTED_NEP35_FUNCTIONS:
unit = self.unit
then we gain a tiny bit in performance, no longer need to adjust our test machinery, and there's no need for a private argument. I think at least at the moment that is fine, since all we ever get from self is the unit.
There was a problem hiding this comment.
Done. I was confused at first by the use of True as a sentinel value, which I believe is sufficient proof that it's not very clear, so I added my own, but the logic is yours !
|
|
||
| if not isinstance(stop, Quantity): | ||
| raise TypeError(f"Expected stop to be a Quantity, got {stop=} ({type(stop)=}") | ||
| if isinstance(stop, Quantity): |
There was a problem hiding this comment.
With my logic, this would become
out_unit = getattr(stop, "unit", True)
(I think it is OK to just look for unit - say, if a Column was passed in. That's consistent with what _quantities2arrays does.)
| ret_arr = np.empty(shape, dtype, order) | ||
| else: | ||
| ret_arr = np.empty(shape, dtype, order, device=device) | ||
| return ret_arr, _like.unit, None |
There was a problem hiding this comment.
So, here it would be return ret_arr, True, None
| all_wrapped_functions = get_wrapped_functions( | ||
| np, np.fft, np.linalg, np.lib.recfunctions | ||
| ) | ||
| all_wrapped_functions |= SUPPORTED_NEP35_FUNCTIONS |
There was a problem hiding this comment.
Can we add a comment referring to your numpy issue? Hopefully, we won't need to use SUPPORTED_NEP35_FUNCTIONS here eventually.
| f"helper is not re-exposing argument {nt!r} properly: " | ||
| f"expected {kt}, got {kh}" | ||
| ) | ||
| elif nt == "like": |
There was a problem hiding this comment.
This could go with my suggestion.
There was a problem hiding this comment.
not completely though: since re-exposing like doesn't really make sense, I'm still keeping this branch (simplified, though)
A what's new makes sense, if we support all of them! |
I can add this as a todo to the global issue I'll write tomorrow. Would that work ? |
I'm focusing on what was requested in #17001 to avoid bloating this PR for now, and |
0eaff36 to
16bf302
Compare
|
I almost forgot to also update documentation, but it's done now |
1ade8bd to
a2bc0c5
Compare
mhvk
left a comment
There was a problem hiding this comment.
Thanks! One larger worry about the use of the unit in arange, otherwise small bits & pieces.
| } | ||
|
|
||
| new_values, out_unit = _quantities2arrays(*qty_kwargs.values(), unit_from_first=True) | ||
| new_values, _ = _quantities2arrays(*qty_kwargs.values()) |
There was a problem hiding this comment.
Thinking about this, I think you still need unit_from_first, otherwise if stop has no unit and start has one, that unit from start will be taken but discarded here. I think it may be best to have an explicit test that if stop has no unit, all other values also have no unit. That probably can then remain a separate branch, with this piece only happening for the case that stop has a unit.
There was a problem hiding this comment.
I'm not sure I got what you mean here. I made an attempt (which I'm about to push), but it still doesn't require unit_from_first.
| else: | ||
| return np_core.shape_base._atleast_nd(arrays, result_ndim) | ||
|
|
||
| _UnitFromLikeArg = object() |
There was a problem hiding this comment.
Why the camel case? I thought that was meant more for classes. Just unit_from_like or UNIT_FROM_LIKE (no real reason for it to be private, I think; none of these are exported up).
There was a problem hiding this comment.
Why the camel case?
I don't know 😅
| def empty(shape, dtype=None, order='C', *, device=None): | ||
| return empty_impl(shape, dtype, order, device=device) | ||
|
|
||
| def empty_impl(shape, dtype=None, order='C', *, device=None): |
There was a problem hiding this comment.
This implementation function is super short and still has to distinguish between numpy versions - just put them directly in the functions above?
| def ones(shape, dtype=None, order='C', *, device=None): | ||
| return ones_impl(shape, dtype, order, device=device) | ||
|
|
||
| def ones_impl(shape, dtype=None, order='C', *, device=None): |
There was a problem hiding this comment.
Same as above. Alternatively, one could have an implementation for all those creation functions (i.e., also pass a function=np.ones along).
| return ret_arr, _UnitFromLikeArg, None | ||
|
|
||
| @dispatched_function | ||
| def zeros(shape, dtype=None, order='C'): |
There was a problem hiding this comment.
Does zeros not have the device argument?
There was a problem hiding this comment.
It doesn't. Otherwise we'd be seeing at least one test fail, but I admit I too was surprised that its signature differs from that of ones. It's probably either an oversight or reflects that ones is part of the Array API and zeros isn't.
| def full(shape, fill_value, dtype=None, order='C', *, device=None): | ||
| return full_impl(shape, fill_value, dtype, order, device=device) | ||
|
|
||
| def full_impl(shape, fill_value, dtype=None, order='C', *, device=None): |
There was a problem hiding this comment.
Here one could still call the overall implementation, and then get the unit after.
There was a problem hiding this comment.
I don't feel comfortable generalizing early as I worry we could get trapped when later expanding the list of implemented function, so I'd like to avoid designing a general implementation for now if that's okay.
|
@neutrinoceros - I made a quick PR to yours with an implementation using |
…d full) with like=Quantity(...)
a2bc0c5 to
c286244
Compare
|
@mhvk here's my attempt to reply to your latest review, but there may still be a couple points to discuss. |
|
@neutrinoceros - I rebased my PR to your PR |
Use function_helper
|
aaaand merged. Thanks ! |
mhvk
left a comment
There was a problem hiding this comment.
All looks good now, but one suggestion still in-line (though fine to merge without it).
| assert b.unit == u.min | ||
| assert_array_equal(a.value, b.value) | ||
|
|
||
| def test_empty(self): |
There was a problem hiding this comment.
You could factor out the test by making a new class
class TestArrayCreation(BasicTestSetup):
def check(self, func, *args, **kwargs):
o = func(*args, **kwargs, like=self.q)
assert type(o) is type(self.q)
assert o.unit == self.q.unit
expected = func(*args, **kwargs) << self.q.unit
assert_array_equal(o, expected)
Then only for full do you need to keep the test with fill_value with a unit.
There was a problem hiding this comment.
for reference, I've integrated this suggestion into #17125
|
If that's okay I'd prefer to apply your suggestion in a follow up PR, so I can rebase #17125 on main sooner rather than later and avoid stacking any further. Thanks ! |
|
OK, let's get this one in! |
Description
Follow up to #17059, fixes #17001
Note that this is not a complete implementation of NEP 35 functions, but it does cover everything that was listed on #17001. I plan to open another issue to track what's left to do.
While working on these functions, I found that the mechanism suggested by @mhvk in #17059 for defaulting the output unit to that of the
likeargument made most sense for functions that don't take other arguments for which a quantity is meaningful, so I implemented it here and retro-fitted it in my implementation fornp.arangetoo, with the particularity that I'm passing it under a different, private name (_like) to better signal that this argument is internal and not passed down by numpy itself.