Skip to content

WCS-backed extra coords are unusable after NDCube.rebin #981

Open

Description

@nabobalis

Describe the bug

When an NDCube's extra coords are backed by a WCS (cube.extra_coords.wcs = ... plus mapping) instead of lookup tables, NDCube.rebin returns a cube whose extra coords are either wrong or raise when used. The output below is the same on main (49b70c1) and 2.4.2.

Rebinning lookup-table extra coords has separate bugs, which I'm fixing in a separate PR (linked below once open); it doesn't change any of this output. I couldn't find any downstream use of WCS-backed extra coords (no extra_coords.wcs = in dkist, sunraster or irispy), so this may not be urgent.

What goes wrong (line numbers are for ndcube/extra_coords/extra_coords.py on main):

  1. Factor order. ExtraCoords.resample passes factor and offset in array order to ResampledLowLevelWCS, which expects pixel order. NDCube.rebin reverses bin_shape for the cube WCS (ndcube.py L1375), so with non-square bins the extra WCS has its axes swapped: pixel (0, 0) is (2.0, 1.5) in the cube WCS and (1.5, 2.0) in the extra WCS. No error, just wrong values.
  2. Lost mapping. resample builds the new ExtraCoords without copying mapping, so it is () and combined_wcs raises.
  3. Wrapper type. resample stores HighLevelWCSWrapper(ResampledLowLevelWCS(...)) in ExtraCoords.wcs (L486). Everything else uses .wcs as a low-level WCS, so axis_world_coords(wcs=extra_coords) and extra_coords.keys() raise AttributeError.
  4. Slicing. rebinned[0] fails in ExtraCoords._getitem_wcs at subwcs = self.wcs[item] (L387), for the same reason.
  5. Mapping checks. mapping holds cube pixel dimensions, but the mapping setter checks max(mapping) against the extra WCS's own pixel_n_dim (L279), and the wcs setter does the same if the mapping was set first (L314). So a 1-D extra WCS can only go on cube pixel axis 0: on a 2-D cube, extra_coords.wcs = WCS(naxis=1) then extra_coords.mapping = (1,) raises ValueError: Values in the mapping can not be larger than the number of pixel dimensions in the WCS. test_mapping_setter asserts this for an ExtraCoords with no cube. Is the check meant to apply when there is a cube? Fixing 2 would run into it, and would need both setters changed.
  6. Offset meaning. For lookup tables, resample(offset=...) is the position on the old grid of the centre of the first new pixel. For ResampledLowLevelWCS it is the shift of the lower edge of pixel 0. So resample(2, offset=0) gives [0, 2, 4] m for a lookup table of np.arange(6) * u.m, and [0.5, 2.5, 4.5] m for a WAVE WCS with the same pixel-to-world values. To get bin centres, rebin has to pass (bin_shape - 1) / 2 for lookup tables and 0 for a WCS.

Also: a 1-D extra WCS on a 2-D cube (mapping = (0,)) can't be rebinned at all. rebin((2, 3)) raises ValueError: Length of factor must equal number of dimensions 1., because resample passes one factor per cube axis rather than per mapped axis.

Possibly related: #753, #431.

To Reproduce

import numpy as np
from astropy.wcs import WCS

from ndcube import NDCube


def run(label, f):
    try:
        print(label, f())
    except Exception as e:
        print(label, f"{type(e).__name__}: {e}")


cube = NDCube(np.zeros((4, 6)), wcs=WCS(naxis=2))
extra = WCS(naxis=2)
extra.wcs.ctype = ["WAVE", "FREQ"]
extra.wcs.cunit = ["m", "Hz"]
cube.extra_coords.wcs = extra
cube.extra_coords.mapping = (0, 1)
cube.axis_world_coords(wcs=cube.extra_coords)  # works

rebinned = cube.rebin((2, 3))
run("cube WCS pixel (0, 0):", lambda: rebinned.wcs.low_level_wcs.pixel_to_world_values(0, 0))
run("extra WCS pixel (0, 0):", lambda: rebinned.extra_coords.wcs.low_level_wcs.pixel_to_world_values(0, 0))
run("type(extra_coords.wcs):", lambda: type(rebinned.extra_coords.wcs).__name__)
run("extra_coords.mapping:", lambda: rebinned.extra_coords.mapping)
run("combined_wcs:", lambda: rebinned.combined_wcs)
run("axis_world_coords(wcs=extra_coords):", lambda: rebinned.axis_world_coords(wcs=rebinned.extra_coords))
run("extra_coords.keys():", lambda: rebinned.extra_coords.keys())
run("rebinned[0]:", lambda: rebinned[0])
cube WCS pixel (0, 0): (array(2.), array(1.5))
extra WCS pixel (0, 0): (array(1.5), array(2.))
type(extra_coords.wcs): HighLevelWCSWrapper
extra_coords.mapping: ()
combined_wcs: ValueError: The length of the mapping must equal the total number of pixel dimensions in all input WCSes.
axis_world_coords(wcs=extra_coords): AttributeError: 'HighLevelWCSWrapper' object has no attribute 'world_axis_object_components'
extra_coords.keys(): AttributeError: 'HighLevelWCSWrapper' object has no attribute 'world_axis_names'
rebinned[0]: TypeError: 'HighLevelWCSWrapper' object is not subscriptable

Screenshots

No response

System Details

  • ndcube version: main (49b70c1), also 2.4.2
  • astropy version: 7.2.0
  • relevant package version(s): numpy 2.4.4, gwcs 1.0.3
  • Python version: 3.14.4
  • OS information: macOS

Installation method

git checkout

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions