Skip to content

BUG: support numpy's mimimummaximum ufunc - #20391

Draft
neutrinoceros wants to merge 2 commits into
astropy:mainfrom
neutrinoceros:bug/np-2.6-minmax
Draft

neutrinoceros wants to merge 2 commits into
astropy:mainfrom
neutrinoceros:bug/np-2.6-minmax

Conversation

@neutrinoceros

Copy link
Copy Markdown
Contributor

Description

xref: numpy/numpy#32231

Opening as a draft with only a partial solution, because it's taking me longer than I anticipated and I must leave for now.

AI Disclosure

None

  • I certify that I am human and that I take full responsibility for this pull request including all interactions with reviewers.

Merge method

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

@neutrinoceros neutrinoceros added this to the v7.2.3 milestone Sep 13, 2026
@neutrinoceros neutrinoceros added no-changelog-entry-needed numpy-dev backport-v7.2.x on-merge: backport to v7.2.x backport-v8.0.x on-merge: backport to v8.0.x labels Sep 13, 2026
@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 github-actions Bot added the units label Sep 13, 2026
@neutrinoceros
neutrinoceros force-pushed the bug/np-2.6-minmax branch 2 times, most recently from 7cdd936 to 1460455 Compare September 13, 2026 18:36
@neutrinoceros

Copy link
Copy Markdown
Contributor Author

Tests are passing locally. I expect to be able to undraft this when CI completes.

@neutrinoceros

Copy link
Copy Markdown
Contributor Author

Whoops, I forgot about masked.

@neutrinoceros

neutrinoceros commented Sep 13, 2026

Copy link
Copy Markdown
Contributor Author

I added test coverage for np.minmax on the masked side but it doesn't work out of the box. It's also not 100% clear how to implement support. An obvious approach would be to re-use np.min and np.max internally, which would output a correct result, at the cost of violating the promise that np.minmax only scans the input array once.

@ikrommyd

Copy link
Copy Markdown

FYI in numpy/numpy#32577 we discovered some edge cases that np.minmax doesn't handle how we'd like it too regarding legacy user dtypes and __array_ufunc__. I don't know if that affects you here, I'm just mentioning it.

@mhvk

mhvk commented Sep 14, 2026

Copy link
Copy Markdown
Contributor

@neutrinoceros - I think you're running into the fact that, by default, a masked reduction always or's all the masks together. This is reasonable for some reductions (like np.add.reduce, np.multiply.reduce, np.logical_or.reduce, etc., where refusing the temptation to guess that a masked element should be 0, 1, or False makes sense), but not for ones where one is effectively selecting items like np.minimum.reduce and np.maximum.reduce. For np.min and np.max, this is avoided by implementing the methods that those will defer to, MaskedNDArray.min and MaskedNDArray.max, each of which create a default where=~self.mask via self._reduce_defaults.

So, I think for np.minmax, we need to add a function override that similarly creates defaults for where and initial.

Note that with that in place, we can also update the ptp method to make use of it (actually, that method should really be deprecated, but that would be for another PR...)

@ikrommyd

ikrommyd commented Sep 14, 2026

Copy link
Copy Markdown

@mhvk yeah in numpy we chose on the PR to not add yet another method to the ndarray. Would you argue that making minmax a method too is helpful? We had settled on doing np.minmax and np.ma.minmax where np.minmax has no knowledge of the mask and will just ignore it as there is no method it can call. But np.ma.minmax is not a fused kernel. It's just an API helper that delegates to separate masked min and max calculations.

@mhvk

mhvk commented Sep 14, 2026

Copy link
Copy Markdown
Contributor

@ikrommyd - thanks for wondering! No, I don't think np.minmax needs a method -- while it is a nice addition, I don't think it is common enough to be a method (I similarly think it was a good idea to deprecate and remove ndarray.ptp()). I mentioned it above only because it explains why in our __array_function__ implementation we do not need to override np.min and np.max, but will need to override np.minmax.

@neutrinoceros

Copy link
Copy Markdown
Contributor Author

think for np.minmax, we need to add a function override that similarly creates defaults for where and initial.

I don't think I understand how np._core.umath.minmummaxim.reduce relates to np.minmax. In fact I already don't understand the implementation details for MaskedNDArray's min and max method and why they are passing reduce kwargs to their superclass (presumably just ndarray ?).

@ikrommyd

ikrommyd commented Sep 14, 2026

Copy link
Copy Markdown

think for np.minmax, we need to add a function override that similarly creates defaults for where and initial.

I don't think I understand how np._core.umath.minmummaxim.reduce relates to np.minmax. In fact I already don't understand the implementation details for MaskedNDArray's min and max method and why they are passing reduce kwargs to their superclass (presumably just ndarray ?).

np._core.umath.minmummaximum is the ufunc whose reduce method is np.minmax at the top level api

@mhvk

mhvk commented Sep 15, 2026

Copy link
Copy Markdown
Contributor

@neutrinoceros - the .max() and .min() methods pass where=~self.mask (as well as an initial value) so that in the reduction any masked elements get ignored (and their mask thus also gets ignored).

@neutrinoceros

Copy link
Copy Markdown
Contributor Author

Thank you both. I think I have a mostly1 correct implementation now:

  • test_no_duplicates is failing
  • it's not called in my test, which I'm sure has the same root cause, but I'm not seeing what's used instead

Footnotes

  1. the mask isn't full proof yet, for now I'm just unconditionally returning unmasked results, but for an array that only has masked values I don't know that it makes sense.

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

Labels

backport-v7.2.x on-merge: backport to v7.2.x backport-v8.0.x on-merge: backport to v8.0.x no-changelog-entry-needed numpy-dev units

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants