Repository navigation
Description
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):
- Factor order.
ExtraCoords.resamplepassesfactorandoffsetin array order toResampledLowLevelWCS, which expects pixel order.NDCube.rebinreversesbin_shapefor 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. - Lost mapping.
resamplebuilds the newExtraCoordswithout copyingmapping, so it is()andcombined_wcsraises. - Wrapper type.
resamplestoresHighLevelWCSWrapper(ResampledLowLevelWCS(...))inExtraCoords.wcs(L486). Everything else uses.wcsas a low-level WCS, soaxis_world_coords(wcs=extra_coords)andextra_coords.keys()raiseAttributeError. - Slicing.
rebinned[0]fails inExtraCoords._getitem_wcsatsubwcs = self.wcs[item](L387), for the same reason. - Mapping checks.
mappingholds cube pixel dimensions, but themappingsetter checksmax(mapping)against the extra WCS's ownpixel_n_dim(L279), and thewcssetter 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)thenextra_coords.mapping = (1,)raisesValueError: Values in the mapping can not be larger than the number of pixel dimensions in the WCS.test_mapping_setterasserts this for anExtraCoordswith 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. - Offset meaning. For lookup tables,
resample(offset=...)is the position on the old grid of the centre of the first new pixel. ForResampledLowLevelWCSit is the shift of the lower edge of pixel 0. Soresample(2, offset=0)gives[0, 2, 4] mfor a lookup table ofnp.arange(6) * u.m, and[0.5, 2.5, 4.5] mfor a WAVE WCS with the same pixel-to-world values. To get bin centres,rebinhas to pass(bin_shape - 1) / 2for 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.
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