Skip to content

ENH: implement np.arange dispatch for Quantity - #17059

Merged
mhvk merged 11 commits into
astropy:mainfrom
neutrinoceros:units/17001/impl_np_arange
Sep 28, 2024
Merged

mhvk merged 11 commits into
astropy:mainfrom
neutrinoceros:units/17001/impl_np_arange

Conversation

@neutrinoceros

@neutrinoceros neutrinoceros commented Sep 23, 2024 •

Copy link
Copy Markdown
Contributor

Description

Implement a helper function for np.arange with like=Quantity(...).
Since the implementation turned out to be complex with many overrides to take into account, I'm opening this PR at an early stage for visibility.
Ref #17001

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

TODO:

@github-actions github-actions Bot added the units label Sep 23, 2024
@github-actions

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

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/17001/impl_np_arange branch 2 times, most recently from 4013170 to ecd9c72 Compare September 23, 2024 14:33
@pllim pllim added this to the v7.0.0 milestone Sep 23, 2024
@neutrinoceros neutrinoceros changed the title ENH: implement np.arange dispatch for Quantity ENH: implement np.arange dispatch for Quantity Sep 23, 2024
@neutrinoceros
neutrinoceros force-pushed the units/17001/impl_np_arange branch 2 times, most recently from de78fcf to 288f351 Compare September 24, 2024 09:19
@neutrinoceros
neutrinoceros force-pushed the units/17001/impl_np_arange branch 2 times, most recently from 3d5b004 to f61b81e Compare September 24, 2024 16:23
@neutrinoceros

Copy link
Copy Markdown
Contributor Author

I think I may be done with discovering and fixing edge cases 🤞🏻
I will revisit tomorrow and undraft then if I still feel like I covered everything.

@neutrinoceros
neutrinoceros force-pushed the units/17001/impl_np_arange branch 3 times, most recently from c3aee78 to a96e31f Compare September 24, 2024 17:24
@neutrinoceros

Copy link
Copy Markdown
Contributor Author

One more edge case that I'd like to phrase as a question to reviewers:
in #17001, we've settled for "the output should have the unit of the first argument", but it's not clear who's the "first" argument in case they're all passed by name. For instance, should the following calls return the same array ?

np.arange(start=1*u.s, stop=10*u.min)
np.arange(stop=10*u.min, start=1*u.s)

In my current implementation they do, because keywords arguments follow the order they appear in the signature, which is invariant.

I guess another question that emerges from this illustration is: should it be valid to specify start and stop with different units without specifying a step ?

@neutrinoceros
neutrinoceros force-pushed the units/17001/impl_np_arange branch from a96e31f to b7f0882 Compare September 24, 2024 19:02

# now we can bind positional arguments to their meaningful names
assert len(args) <= 4
match args:

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 think this is the first time the requirements for an implementation are so complex that using match/case feels simpler than the equivalent forest of if/else , but I'm really glad we have it.

@lpsinger lpsinger 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.

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

Wow! Some comments inline, but also a larger point: I wonder if we need to keep track of the unit of the argument of like (i.e., self in Quantity.__array_function__). For np.zeros and np.ones, it would certainly make most sense for the output to have the same unit as the argument of like. For np.arange, this is less clear since I think we want something like np.arange(0*u.m, 10*u.m, like=0*u.s) to succeed. But perhaps it should set the default unit?

I think we need some special logic in Quantity.__array_function__ that assigns the unit to self.unit if it is not already set, i.e., if the output unit is not a unit but some signal (cannot be None, which is already used for truly unit-less output, like from comparisons, but could perhaps be False or True or something else recognizable)

if start is not None and stop is None:
start, stop = stop, start

has_out_unit = False

@mhvk mhvk Sep 25, 2024 •

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.

The loop looks a bit like _quantities2arrays, except it has to deal with None. One perhaps can reverse the operations here, as in

kwargs = {k: v for k, v in (("start", start), ("stop", stop), ("step", step)) if v is not None}
new_values = _quantities2arrays(kwargs.values())
kwargs = {k: v for k, v in zip(kwargs.keys(), new_values)}
kwargs["dtype"] = dtype

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.

this should be addressed on my next push (but I'm riding a train so I might not succeed before I get off)

@neutrinoceros

neutrinoceros commented Sep 25, 2024 •

Copy link
Copy Markdown
Contributor Author

wonder if we need to keep track of the unit of the argument of like (i.e., self in Quantity.array_function). For np.zeros and np.ones, it would certainly make most sense for the output to have the same unit as the argument of like. For np.arange, this is less clear since I think we want something like np.arange(0u.m, 10u.m, like=0*u.s) to succeed. But perhaps it should set the default unit?

I would agree that using the unit from like by default could make sense, but I currently don't see how we can pass it down to our dispatch function.

Update: and now I read the read of the message I replied to 🤦🏻‍♂️
ok your suggestion makes sense. I can give it a try tomorrow !

@neutrinoceros

Copy link
Copy Markdown
Contributor Author

Ok I think I've addressed all but your larger scope suggestion about capturing like's unit as use it as a default if needed, which I'll come back to tomorrow !

@neutrinoceros

Copy link
Copy Markdown
Contributor Author

@lpsinger thanks for the reminder (#17059 (review)), I somehow didn't see your message yesterday but I'll address it now !

@neutrinoceros

Copy link
Copy Markdown
Contributor Author

Re: #17059 (comment)

Conveniently I missed this suggestion yesterday, but I think it fits pretty well the more general problem I set out to deal with this morning (regarding somehow using the unit from the like argument as a default).
I find that the refactor suggested creates is unfortunately not a strict simplification:
_quantities2array internally uses _as_quantity, which will happily convert pure numbers as dimensionless quantities, so additional logic is still needed to distinguish the arguments (other than like) being trully dimensionless, e.g., 1*u.dimensionless_unscaled, or just plain numbers.

Since I think this also connects with my (currently unanswered) question in #17059 (comment), I think we need to discuss these (edge?) cases before we can move further. Let me summarize the questions to be resolved here:

  1. should it be valid to specify start and stop with different units without specifying a step ?
  2. if so, should the following calls return the same array (or should the order of the keyword argument dictate whice one dictates the output unit) ?
Q = 1 * u.km
np.arange(start=1*u.s, stop=10*u.min, like=Q)
np.arange(stop=10*u.min, start=1*u.s, like=Q)
  1. should the following be valid (and what should the output unit be: dimensionless, or km) ?
Q = 1 * u.km
np.arange(10, like=Q)

@mhvk

mhvk commented Sep 26, 2024

Copy link
Copy Markdown
Contributor

1,2: since stop is the only argument that has to be specified, I think its unit should take precedence, i.e., the output unit should always be the unit of stop.
3: A little trickier. I think by analogy of zeros_like, full_like, etc., the unit of Q should be the default. But then arguably the unit of Q should always be used, so maybe instead we should just refuse the temptation to guess (as you had it already).

So, I think one can still use _quantities2arrays, by ensuring that stop is passed in first and then set unit_from_first=True -- though annoyingly one does have to check stop is a Quantity, so perhaps it doesn't shorten what you have...

@neutrinoceros

Copy link
Copy Markdown
Contributor Author

so maybe instead we should just refuse the temptation to guess

words to live by. I think it makes sense to require units be set on the stop argument for now. We can always add more flexibility in the future (but might regret doing so now). I'll give it another go now !

@neutrinoceros

Copy link
Copy Markdown
Contributor Author

Alright, I've adjusted my test suite to our new requirements and applied your suggested refactor as best I could.
FYI I'll be back to my home-office next Wednesday, so I might not be able to push more changes before that.

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

This looks great! Only nitpicks left.


"""

from __future__ import annotations

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.

Not actually needed?

if v is not None
}

new_values, out_unit = _quantities2arrays(*list(qty_kwargs.values()), unit_from_first=True)

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.

No need for the list, i.e., just *qty_kwargs.values(), unit_from_first=True

assert type(arr) is u.Quantity
assert arr.unit == u.radian
assert arr.dtype == expected.dtype
np.testing.assert_array_almost_equal(arr.to_value(u.arcsec), expected)

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.

Might as well do an from numpy.testing import assert_array_almost_equal, assert_array_equal on top.

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.

ah, actually it's already there. Let me simplify this !

Comment thread docs/changes/units/17059.feature.rst Outdated
@@ -0,0 +1 @@
Added a ``np.arange`` dispatch for ``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.

Maybe expand

Added a ``np.arange`` dispatch for ``Quantity`` (requires one to use ``like=<some_quantity>``).

Just to avoid people getting too excited...

Comment thread docs/known_issues.rst Outdated
<Quantity [0. , 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9] cm>

Also note that the unit of the output array is dictated by that of the ``stop``
argument, and the data has a floating-point dtype. If an integer dtype is

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.

I'd remove the actual example of getting integers out - I really do not want to encourage people to use it. Physical quantities are real! So just, and that, like for quantities generally, the data has floating-point dtype by default.

Comment thread docs/known_issues.rst Outdated
<Quantity [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] mm>

Workarounds include moving the units outside of the call to
Alternatively, one may move the units outside of the call as

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.

I think to was more appropriate here, though frankly fine to just remove, i.e., end with ... call::

@neutrinoceros

Copy link
Copy Markdown
Contributor Author

Thanks Marten ! I think I've addressed everything !

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

Great, all good now, thanks!

@mhvk
mhvk merged commit 10da0ee into astropy:main Sep 28, 2024
@neutrinoceros
neutrinoceros deleted the units/17001/impl_np_arange branch September 29, 2024 08:11
neutrinoceros added a commit to neutrinoceros/astropy that referenced this pull request Oct 3, 2024
kkrutkowski pushed a commit to kkrutkowski/astropy that referenced this pull request Apr 13, 2025
* ENH: implement np.arange dispatch for Quantity

* also test output dtype

* fix a broken case where dtype is passed positionally

* simplify test suite

* DOC: update docs

* TST: adjust test suite to new requirements

* RFC: refactor impl to match new requirements

* TST: add a new test case

* DOC: adjust docs

* cleanup

* address review comments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants