Skip to content

Fix crop for WCSes with interleaved world objects - #977

Open
nabobalis wants to merge 5 commits into
sunpy:mainfrom
nabobalis:crop-object-order
Open

nabobalis wants to merge 5 commits into
sunpy:mainfrom
nabobalis:crop-object-order

Conversation

@nabobalis

@nabobalis nabobalis commented Sep 27, 2026 •

Copy link
Copy Markdown
Member

PR Description

crop takes one entry per world object, with None for objects that aren't cropped. It ordered the objects by where each one last appears in the WCS, but pixel_to_world and world_to_pixel use where each first appears. So on WCSes with interleaved world objects (e.g. HPLN, WAVE, HPLT, or DKIST VISP), crop rejected points in pixel_to_world order, including partial ones. The error for a point of the wrong length also didn't say what it expected.

import numpy as np

from astropy.wcs import WCS

from ndcube import NDCube

w = WCS(naxis=3)
w.wcs.ctype = ["HPLN-TAN", "WAVE", "HPLT-TAN"]
w.wcs.cunit = ["deg", "m", "deg"]
w.wcs.cdelt = [0.1, 1e-10, 0.1]
w.wcs.crpix = [1, 1, 1]
w.wcs.crval = [0, 5e-7, 0]
w.wcs.dateobs = "2020-01-01"
w.wcs.set()
cube = NDCube(np.zeros((4, 5, 6)), wcs=w)
sky0, wl0 = cube.wcs.pixel_to_world(1, 1, 1)
sky1, wl1 = cube.wcs.pixel_to_world(3, 3, 2)

for label, points in {
    "[sky, wl]": ([sky0, wl0], [sky1, wl1]),
    "[sky, None]": ([sky0, None], [sky1, None]),
    "[None, wl]": ([None, wl0], [None, wl1]),
    "[wl, None]": ([wl0, None], [wl1, None]),
    "[wl, sky]": ([wl0, sky0], [wl1, sky1]),
    "[sky]": ([sky0], [sky1]),
}.items():
    try:
        result = cube.crop(*points).shape
    except Exception as e:
        result = f"{type(e).__name__}: {e}"
    print(f"{label:12} -> {result}")
points main this PR
[sky, wl] (pixel_to_world order) TypeError: <class '...SkyCoord'> of component 0 in point 0 is incompatible with WCS component spectral <class '...Quantity'>. (2, 3, 3)
[sky, None] same TypeError (2, 5, 3)
[None, wl] TypeError: <class '...SpectralCoord'> of component 1 in point 0 is incompatible with WCS component celestial <class '...SkyCoord'>. (4, 3, 6)
[wl, None] ValueError: Expected the following order of world arguments: SkyCoord (4, 3, 6)
[wl, sky] (old order) (2, 3, 3) (2, 3, 3)
[sky] ValueError: 1 components in point 0 do not match WCS with 2 components. same, followed by Each point must have one entry per world object (use None for a component that should not be cropped), in order: celestial (SkyCoord), spectral (Quantity).

Changes:

  • Objects are ordered by first appearance (utils.misc.unique_sorted, as in axis_world_coords).
  • If each non-None object matches exactly one world object class, objects are matched by class, as in astropy's world_to_pixel. Otherwise they are matched by position.
  • The length and type errors list the expected objects in order (replaces Name expected world objects in crop component-count error #939).
  • get_crop_item_from_points sorts the pixel axes instead of using set order, which swapped some bounds on cubes with nine or more dimensions.

User-visible changes:

  • Points in pixel_to_world order work, and so does the [spec, sky] example in Allow greater flexibility in crop bounds order #608.
  • Points in the old order still work when every world object has its own class (none a subclass of another), as on DKIST VISP. dkist's crop tests pass, two of them only because of the class matching.
  • Matching is all or nothing for each point. If classes are shared (FITS WAVE and LINEAR are both Quantity) or one is a subclass of another (SpectralCoord and Quantity), the point is matched by position, in first-appearance order. So the old-order point [wl, sky, None] on HPLN, WAVE, HPLT, LINEAR now raises.
  • Some inputs that main rejected with a TypeError now land in the wrong slot:

AI Assistance Disclosure

AI tools were used for:

  • Code generation (e.g., when writing an implementation or fixing a bug)
  • Test/benchmark generation
  • Documentation (including examples)
  • Research and understanding
  • No AI tools were used

Regardless of AI use, the human contributor remains fully responsible for correctness, design choices, licensing compatibility, and long-term maintainability.

K

When a crop point has the wrong number of entries, or an entry of the
wrong class, the error now lists the expected world objects in order,
so users migrating from a WCS with fewer world objects, or passing
objects in the wrong order, learn the fix from the error itself.
The component dedup in NDCube._get_crop_item popped entries while
enumerating, which kept the last occurrence of each world object. WCSes
whose object components are not adjacent (e.g. HPLN, WAVE, HPLT, or
DKIST VISP's lon, wl, lat, time, stokes) then rejected points given in
the order pixel_to_world returns, including partial points with None.
Keep the first occurrence instead.

As astropy's world_to_pixel does, also match objects to components by
class when each object in a point matches exactly one class and no two
objects match the same one (sunpy#608). Points in the old order therefore
keep working when the world objects have distinct classes, none a
subclass of another, as in dkist's VISP crop tests. Otherwise, for
example when objects share a class or their classes are related by
subclassing (Quantity and SpectralCoord), the whole point is matched by
position.

Also sort the pixel axes with input in get_crop_item_from_points:
iterating a set swapped the bounds of pixel axes (e.g. 3 and 8) on
cubes with nine or more dimensions.
@nabobalis nabobalis changed the title Fix crop world-object order for WCSes with non-contiguous components Fix crop for WCSes with interleaved world objects Oct 1, 2026

This branch has not been deployed

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant