Skip to content

ENH: add support for NEP 35 array constructors (empty, ones, zeros and full) with like=Quantity(...)) - #17120

Merged
mhvk merged 3 commits into
astropy:mainfrom
neutrinoceros:units/enh/impl_nep35_array_constructors
Oct 4, 2024
Merged

mhvk merged 3 commits into
astropy:mainfrom
neutrinoceros:units/enh/impl_nep35_array_constructors

Conversation

@neutrinoceros

Copy link
Copy Markdown
Contributor

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 like argument 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 for np.arange too, 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.

  • By checking this box, the PR author has requested that maintainers do NOT use the "Squash and Merge" button. Maintainers should respect this when possible; however, the final decision is at the discretion of the maintainer that merges the PR.

@github-actions github-actions Bot added the units label Oct 3, 2024
@github-actions

github-actions Bot commented Oct 3, 2024

Copy link
Copy Markdown
Contributor

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.

  • Do the proposed changes actually accomplish desired goals?
  • Do the proposed changes follow the Astropy coding guidelines?
  • Are tests added/updated as required? If so, do they follow the Astropy testing guidelines?
  • Are docs added/updated as required? If so, do they follow the Astropy documentation guidelines?
  • Is rebase and/or squash necessary? If so, please provide the author with appropriate instructions. Also see instructions for rebase and squash.
  • Did the CI pass? If no, are the failures related? If you need to run daily and weekly cron jobs as part of the PR, please apply the "Extra CI" label. Codestyle issues can be fixed by the bot.
  • Is a change log needed? If yes, did the change log check pass? If no, add the "no-changelog-entry-needed" label. If this is a manual backport, use the "skip-changelog-checks" label unless special changelog handling is necessary.
  • Is this a big PR that makes a "What's new?" entry worthwhile and if so, is (1) a "what's new" entry included in this PR and (2) the "whatsnew-needed" label applied?
  • At the time of adding the milestone, if the milestone set requires a backport to release branch(es), apply the appropriate "backport-X.Y.x" label(s) before merge.

@github-actions

github-actions Bot commented Oct 3, 2024

Copy link
Copy Markdown
Contributor

👋 Thank you for your draft pull request! Do you know that you can use [ci skip] or [skip ci] in your commit messages to skip running continuous integration tests until you are ready?

@neutrinoceros
neutrinoceros force-pushed the units/enh/impl_nep35_array_constructors branch from e438875 to 355af1a Compare October 3, 2024 13:39
@neutrinoceros neutrinoceros changed the title ENH: add support for NEP 35 array constructors (empty, ones, zeros and full) with like=Quantity(...) ENH: add support for NEP 35 array constructors (empty, ones, zeros and full) with like=Quantity(...) Oct 3, 2024
@neutrinoceros
neutrinoceros force-pushed the units/enh/impl_nep35_array_constructors branch 2 times, most recently from 057e42c to 97499cd Compare October 3, 2024 14:01
@neutrinoceros
neutrinoceros marked this pull request as ready for review October 3, 2024 14:15
@neutrinoceros
neutrinoceros force-pushed the units/enh/impl_nep35_array_constructors branch from 97499cd to a540f7a Compare October 3, 2024 14:17
@pllim pllim added this to the v7.0.0 milestone Oct 3, 2024
@pllim
pllim requested review from mhvk and nstarman October 3, 2024 15:33
@pllim

pllim commented Oct 3, 2024

Copy link
Copy Markdown
Member

I'll let units maintainers to decide if this needs a What's New. Thanks!

@neutrinoceros neutrinoceros changed the title ENH: add support for NEP 35 array constructors (empty, ones, zeros and full) with like=Quantity(...) ENH: add support for NEP 35 array constructors (empty, ones, zeros and full) with like=Quantity(...)) Oct 3, 2024

@mhvk mhvk left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good, but with a suggestion. Also, how about asarray?

Comment thread astropy/units/quantity.py Outdated

elif function in DISPATCHED_FUNCTIONS:
dispatched_function = DISPATCHED_FUNCTIONS[function]
if function in SUPPORTED_NEP35_FUNCTIONS:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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":

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could go with my suggestion.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not completely though: since re-exposing like doesn't really make sense, I'm still keeping this branch (simplified, though)

@mhvk

mhvk commented Oct 3, 2024

Copy link
Copy Markdown
Contributor

I'll let units maintainers to decide if this needs a What's New. Thanks!

A what's new makes sense, if we support all of them!

@neutrinoceros

Copy link
Copy Markdown
Contributor Author

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 ?

@neutrinoceros

Copy link
Copy Markdown
Contributor Author

how about asarray?

I'm focusing on what was requested in #17001 to avoid bloating this PR for now, and asarray is part of a subset of functions that I think belong together (array, asarray, asanyarray, ascontiguousarray and asfortranarray) and plan to tackle next.

@neutrinoceros
neutrinoceros force-pushed the units/enh/impl_nep35_array_constructors branch 2 times, most recently from 0eaff36 to 16bf302 Compare October 4, 2024 06:08
@neutrinoceros

neutrinoceros commented Oct 4, 2024 •

Copy link
Copy Markdown
Contributor Author

Alright, I implemented @mhvk's suggestion, so this should be ready to go. I'll also write the follow up issue now.
update: here it is #17124

@neutrinoceros neutrinoceros mentioned this pull request Oct 4, 2024
6 of 28 tasks
@neutrinoceros

Copy link
Copy Markdown
Contributor Author

I almost forgot to also update documentation, but it's done now

@mhvk mhvk left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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())

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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'):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does zeros not have the device argument?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here one could still call the overall implementation, and then get the unit after.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@mhvk

mhvk commented Oct 4, 2024

Copy link
Copy Markdown
Contributor

@neutrinoceros - I made a quick PR to yours with an implementation using function_helper - which is really the one that is supposed to deal with cases where functions are very similar. See neutrinoceros#25

@neutrinoceros
neutrinoceros force-pushed the units/enh/impl_nep35_array_constructors branch from a2bc0c5 to c286244 Compare October 4, 2024 15:31
@neutrinoceros

Copy link
Copy Markdown
Contributor Author

@mhvk here's my attempt to reply to your latest review, but there may still be a couple points to discuss.

@mhvk

mhvk commented Oct 4, 2024

Copy link
Copy Markdown
Contributor

@neutrinoceros - I rebased my PR to your PR

@neutrinoceros

Copy link
Copy Markdown
Contributor Author

aaaand merged. Thanks !

@mhvk mhvk left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

for reference, I've integrated this suggestion into #17125

@neutrinoceros

neutrinoceros commented Oct 4, 2024 •

Copy link
Copy Markdown
Contributor Author

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 !

@mhvk

mhvk commented Oct 4, 2024

Copy link
Copy Markdown
Contributor

OK, let's get this one in!

@mhvk
mhvk merged commit a9bcdb3 into astropy:main Oct 4, 2024
@neutrinoceros
neutrinoceros deleted the units/enh/impl_nep35_array_constructors branch October 5, 2024 05:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Investigate enabling array creation routines for quantities

3 participants