Add an array-API median fallback for subtract_overscan - #989
Merged
Conversation
subtract_overscan called xp.median(overscan.data, axis=overscan_axis), but median is not part of the array API standard, so it raised AttributeError on backends such as array-api-strict that omit it (numpy, jax and dask all provide it natively and are unaffected). Add ccdproc._nanfuncs.median, a NaN-propagating wrapper around the existing nanmedian fallback: on NaN-free input it is bit-for-bit identical to nanmedian, and a final where() restores numpy.median's NaN-propagating semantics (nanmedian alone would silently ignore NaNs). subtract_overscan now goes through a new _median_fallback helper in core.py, mirroring the existing _percentile_fallback pattern: try the namespace's own median first, and only fall back to the array-API-only implementation when it is missing. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RQMJZUaaxfqGDk41GLSaFK
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RQMJZUaaxfqGDk41GLSaFK
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #989 +/- ##
==========================================
+ Coverage 97.42% 97.44% +0.01%
==========================================
Files 9 9
Lines 1749 1760 +11
==========================================
+ Hits 1704 1715 +11
Misses 45 45
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
54 tasks
codecov flagged the two lines of _median_fallback's AttributeError branch (core.py:182,186) as the only uncovered lines in the patch: every backend that uploads coverage (numpy, jax, dask) has a native median, so the fallback only ran on the strict job, which reports no coverage. Exercise it everywhere with a proxy namespace that hides median and otherwise delegates to the backend under test, following the fake-namespace pattern in test_combiner.py. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RQMJZUaaxfqGDk41GLSaFK
mwcraig
commented
Aug 24, 2026
Review feedback on astropy#989: the paragraph explaining how median relates to nanmedian is implementation detail, and the other functions in this module keep that material under Notes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RQMJZUaaxfqGDk41GLSaFK
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
subtract_overscancallsxp.median(overscan.data, axis=overscan_axis)(
ccdproc/core.py), butmedianis not part of the array API standard, soit raises
AttributeErroron backends such asarray-api-strictthat don'tprovide it (numpy, jax and dask all have a native
medianand areunaffected).
This adds
ccdproc._nanfuncs.median, a small wrapper around the existingnanmedianfallback, and uses it from a new_median_fallbackhelper incore.pythat tries the namespace's ownmedianfirst and only falls backto the array-API-only implementation when it's missing -- mirroring the
existing
_percentile_fallbackpattern already used forpercentile.Why
Part of #971 (array-API migration tracker).
medianjoinsnansum/nanmean/nanstd/nanmedian(#906, #986) as another reduction thatisn't part of the array API standard and needs a pure-array-API fallback.
NaN semantics
numpy.medianpropagates NaN: if any value in the reduced slice is NaN, theresult is NaN.
nanmediandoes the opposite -- it ignores NaNs. Sincesubtract_overscan'smedianfallback needs to matchnumpy.median(not
nanmedian), the newmedianfunction wrapsnanmedianand thenrestores NaN-propagation with a final
xp.where(xp.any(xp.isnan(x), ...)).On NaN-free input
medianis bit-for-bit identical tonanmedian, since itreuses the exact same sort-and-average algorithm.
Testing
ccdproc/tests/test_nanfuncs.py: addedmedian(againstnp.median) tothe differential
_FUNCStable, which already exercises NaN-scattered,all-NaN, single-non-NaN, integer and boolean inputs across a range of
shapes and axes -- this is what pins the NaN-propagating semantics. Also
added
mediantotest_no_warning_on_all_nan_sliceandtest_bad_axis(verified
numpy.median, unlikenumpy.nanmedian, does not warn onall-NaN input).
array-api-strict: all "module 'array_api_strict' has no attribute'median'" errors are eliminated -- confirmed zero occurrences anywhere in
the suite after this change.
ccdprocsuite is unaffected (0 failures, nativexp.medianpath taken, byte-identical to before).
test_nanfuncs.py+test_ccdproc.pypass.Note on the strict-suite failure count: the 4 tests originally cited as
failing due to this bug (
test_ccd_process[*],test_ccd_process_gain_corrected) do not go green with this fix alone.Diffing the full
array-api-strictfailure list before/after shows the same85 tests fail both times -- but for 3 of these 4 tests the reason changes,
from
AttributeError: ... has no attribute 'median'to two separate,pre-existing bugs that this fix now uncovers (they were previously masked
because
subtract_overscanraised first):ccd_process's bad-pixel-mask handling usesxp.asarray(bad_pixel_mask, dtype=bool)with the Python builtinboolinstead of the namespace'sxp.bool(core.py, inccd_process), whicharray-api-strictrejects.gain_correctbuilds its gain array withxp.asarray(gain_value)withoutspecifying
device=, so it ends up on the default device while the imagedata is on
array-api-strict's non-default device, and the subsequentmultiply fails with "Arrays from two different devices".
Both are real, independent array-API incompatibilities, out of scope for
this PR, and worth filing as separate follow-up issues against #971.
🤖 Generated with Claude Code
https://claude.ai/code/session_01RQMJZUaaxfqGDk41GLSaFK