Skip to content

fix(wcs): reject negative slice start in WCS.slice - #20323

Closed
IMGillusion wants to merge 3 commits into
astropy:mainfrom
IMGillusion:fix-8771-wcs-slice-negative
Closed

IMGillusion wants to merge 3 commits into
astropy:mainfrom
IMGillusion:fix-8771-wcs-slice-negative

Conversation

@IMGillusion

@IMGillusion IMGillusion commented Sep 5, 2026 •

Copy link
Copy Markdown
Contributor

Description

WCS.slice (and therefore wcs[...]) silently misinterpreted a negative
slice start as a pixel offset into the unsliced WCS. For example, with
crpix=[5,5], crval=[0,1], cdelt=[1,1]:

wcs[4:, 4:][-0,0]  # -> [0, 1]  (correct, start=4 lands on crpix)
wcs[-6:, -6:][-0,0]  # -> [-10, -9]  (nonsense, no error)

This is the legacy high-level WCS.slice code path; the wcsapi
SlicedLowLevelWCS wrapper rejects negative starts since #15557. This PR
makes the two paths consistent: a negative slice start (or a negative start
combined with a step) now raises IndexError with a message explaining
why, instead of producing garbage. This closes #8771, whose reporter listed
"raise an exception if it gets a negative index" as one of the acceptable
solutions.

Motivation and context

Related issue number: #8771

Checklist

  • By submitting this pull request, is this proposal not primarily
    about a code review request?
  • The PR title is a meaningful review title.
  • The PR has a meaningful description.
  • The PR makes no breaking changes or adds a deprecation warning.
  • The PR doesn't change any behavior in Astropy.
  • I've added the relevant tests etc.
  • I've updated the documentation to cover the new function/class.

Tests

Added test_slice_getitem_negative_start_raises in
astropy/wcs/tests/test_utils.py. Verified locally: with the pristine
wcs.py the new test fails with "DID NOT RAISE" (reproducing #8771),
after the fix it passes; the full test_utils.py slice family shows no
regression (only pre-existing network-dependent failures unrelated to this
change).

AI Disclosure

An AI coding agent assisted with this pull request: it helped diagnose the issue from the linked report, draft the code fix and the regression test, and write this PR description. The change was validated locally against the project's own test suite (red/green runs plus full-module regression) before submission.

A negative slice start (e.g. ``wcs[-6:]``) was silently interpreted as a
pixel offset into the unsliced WCS, so slicing produced nonsensical world
coordinates instead of raising. Reject it with an ``IndexError``, consistent
with the negative-index handling added for SlicedLowLevelWCS. Closes astropy#8771.
@github-actions github-actions Bot added the wcs label Sep 5, 2026
@github-actions

github-actions Bot commented Sep 5, 2026

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.

@astrofrog astrofrog left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Small comment below, this is ok otherwise

Comment thread docs/changes/wcs/20323.bugfix.rst Outdated
@@ -0,0 +1,5 @@
Fixing the slicing of a high-level ``WCS`` with a negative slice start (for
example ``wcs[-6:]``) now raises ``IndexError`` instead of silently

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
example ``wcs[-6:]``) now raises ``IndexError`` instead of silently
example ``wcs[-6:]``). This now raises ``IndexError`` instead of silently

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 — applied as suggested (d7e5ded). Thanks for the quick pass!

@astrofrog astrofrog left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The following review comment was generated by Claude Fable, and I agree with the points raised (I suggested some of them)

Thanks! This is the same situation as #20320 though, so I would apply the same reasoning here: rather than rejecting negative starts unconditionally, we should support them properly when the shape is known and only raise when it is not. The case is even stronger on this path, because WCS.slice already uses the shape to resolve negative stops correctly, via nitems = len(range(self._naxis[wcs_index])[iview]) a few lines below your check. The only thing actually broken when _naxis is known is the crpix/distortion-shift arithmetic: today wcs[:, :, -30:] on a WCS with pixel_shape = (100, 100, 50) produces the correct _naxis of 30 but shifts crpix by +30 instead of -70, so the returned WCS is internally inconsistent rather than obviously wrong. So instead of the up-front loop, I would normalize each slice per axis just before the crpix arithmetic:

n = self._naxis[wcs_index]                                                                                                                                                   
if n > 0:
    iview = slice(*iview.indices(n))
elif iview.start is not None and iview.start < 0:
    raise IndexError(
        "Negative indices are not supported when slicing a WCS with unknown shape (NAXIS not set)."                                                                          
    )

slice.indices(n) returns a non-negative start/stop and preserves the step, so it should compose with the existing downsampling branch (negative steps are already rejected earlier), and since the SIP crpix and distortion table crval shifts all use iview.start, normalizing before the arithmetic fixes them in the same place. For a header-derived WCS, NAXIS* is set, so wcs[-6:, -6:] would then slice the WCS the same way numpy slices the data, and the error only remains for WCS objects that genuinely
have no shape information.

One more reason to keep the two PRs in sync: as soon as the view contains an integer index (e.g. wcs[0, -6:]), slice silently reroutes to SlicedFITSWCS, i.e. the code path #20320 touches. If the two adopt different policies, the same slice will behave differently depending on whether a dimension is dropped, so whatever we converge on there (see my
comment on that PR) should be mirrored here with the same semantics and error message.

For tests, I would add: negative start with pixel_shape set gives results identical to the equivalent positive slice (including crpix and array_shape); negative start without shape information raises IndexError; and a case with a SIP or distortion-corrected WCS to confirm the auxiliary shifts come out right.

Note the phrase "see my comment on that PR" assumes the #20320 comment is posted first (or around the same time) — post them in that order, or drop the parenthetical if you only comment here.

@pllim pllim added this to the v8.1.0 milestone Sep 8, 2026
@pllim pllim closed this Sep 12, 2026
@pllim pllim added the invalid label Sep 12, 2026
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.

WCS handles negative slice index incorrectly

3 participants