Skip to content

reproject_adaptive collapses for very large shape_out: returns all-NaN + zero footprint #579

Description

@Joshuaalbert

I’m seeing a deterministic size-dependent failure in reproject_adaptive when shape_out is very large (~1.8e9 pixels). Even with a trivial source plane (all ones) and clearly overlapping WCS, the function returns either:

  1. plane all-NaN and footprint all-false (sum=0), or
  2. plane all-zero and footprint has exactly 1 true pixel (sum=1)

No exception is raised.

This makes it hard to detect upstream because the output looks “valid” from an API perspective but is physically wrong.

Expected behavior

Either, A) a correct reprojection with nonzero footprint and finite plane values, or B) a raised exception indicating internal limitation (memory / indexing / shape limit)

Ideally A

Minimal verifiable complete example

This reproducer constructs a target WCS that clearly overlaps with source WCS, and uses an all-ones source plane.

import sys
import numpy as np
import astropy
from astropy import wcs
from reproject import reproject_adaptive
import reproject


def make_target_wcs(num_l: int, num_m: int, dl_deg: float, dm_deg: float, ra0_deg: float, dec0_deg: float):
    # 4D WCS like our pipeline, then use .celestial
    target_w = wcs.WCS(naxis=4)
    target_w.wcs.ctype = ["RA---SIN", "DEC--SIN", "FREQ", "STOKES"]
    target_w.wcs.cunit = ["deg", "deg", "Hz", ""]
    # NOTE: reference pixel placement is intentional and matches our pipeline:
    target_w.wcs.crpix = [num_l / 2.0, num_m / 2.0 + 1.0, 1.0, 1.0]
    target_w.wcs.cdelt = [-abs(dl_deg), +abs(dm_deg), 1.0, 1.0]
    target_w.wcs.crval = [ra0_deg, dec0_deg, 1.0, 1.0]  # STOKES=I code is 1
    return target_w.celestial


def make_source_wcs(num_l_src: int, num_m_src: int, dl_deg: float, dm_deg: float, ra0_deg: float, dec0_deg: float):
    src_w = wcs.WCS(naxis=2)
    src_w.wcs.ctype = ["RA---SIN", "DEC--SIN"]
    src_w.wcs.cunit = ["deg", "deg"]
    src_w.wcs.crpix = [(num_l_src + 1) / 2.0, (num_m_src + 1) / 2.0]
    src_w.wcs.cdelt = [-abs(dl_deg), +abs(dm_deg)]
    src_w.wcs.crval = [ra0_deg, dec0_deg]
    return src_w


def run_case(num_l_out: int, num_m_out: int, label: str):
    ra0_deg = 60.0
    dec0_deg = 0.0

    # ~1 arcsec/pixel
    dl_deg = (1.009918463060501 / 3600.0)
    dm_deg = (1.009918463060501 / 3600.0)

    # Big-enough source plane so target overlap is guaranteed
    num_l_src = 81920
    num_m_src = 81920

    src_wcs = make_source_wcs(num_l_src, num_m_src, dl_deg, dm_deg, ra0_deg, dec0_deg)
    tgt_wcs = make_target_wcs(num_l_out, num_m_out, dl_deg, dm_deg, ra0_deg, dec0_deg)

    # All ones input (dec, ra)
    src_plane = np.ones((num_m_src, num_l_src), dtype=np.float32)

    out_plane, footprint = reproject_adaptive(
        (src_plane, src_wcs),
        tgt_wcs,
        kernel="gaussian",
        conserve_flux=True,
        shape_out=(num_m_out, num_l_out),
        return_footprint=True,
        parallel=False,
    )

    fp_sum = float(np.sum(footprint))
    fp_size = int(footprint.size)
    out_nan = int(np.isnan(out_plane).sum())
    out_max = np.nanmax(out_plane)
    out_min = np.nanmin(out_plane)

    print(f"\n[{label}] shape_out=(num_m,num_l)=({num_m_out},{num_l_out})")
    print(f"  footprint sum/size: {fp_sum}/{fp_size}")
    print(f"  out NaN count: {out_nan} / {out_plane.size}")
    print(f"  out min/max: {out_min} / {out_max}")


def main():
    print("Python:", sys.version)
    print("astropy:", astropy.__version__)
    print("reproject:", reproject.__version__)

    # Control case: smaller (works)
    run_case(17856, 17856, "small-square")

    # Large cases: failures observed (either fp_sum==0 and NaNs, or fp_sum==1 and zeros)
    run_case(43274, 42739, "large-nonsquare-odd")
    run_case(43274, 42738, "large-nonsquare-even")


if __name__ == "__main__":
    main()

Output

Python: 3.11.14 (main, Dec  8 2025, 23:47:06) [GCC 12.2.0]
astropy: 7.2.0
reproject: 0.19.0

[small-square] shape_out=(num_m,num_l)=(17856,17856)
  footprint sum/size: 318836736.0/318836736
  out NaN count: 0 / 318836736
  out min/max: 0.9999999998799467 / 1.0000000001127773
/tmp/ipykernel_1664238/1971168938.py:62: RuntimeWarning: All-NaN slice encountered
  out_max = np.nanmax(out_plane)
/tmp/ipykernel_1664238/1971168938.py:63: RuntimeWarning: All-NaN slice encountered
  out_min = np.nanmin(out_plane)

[large-nonsquare-odd] shape_out=(num_m,num_l)=(42739,43274)
  footprint sum/size: 0.0/1849487486
  out NaN count: 1849487486 / 1849487486
  out min/max: nan / nan

[large-nonsquare-even] shape_out=(num_m,num_l)=(42738,43274)
  footprint sum/size: 1.0/1849444212
  out NaN count: 1849444211 / 1849444212
  out min/max: 0.0 / 0.0

Observe

  1. small-square: nonzero footprint and finite values.
  2. large-nonsquare-odd: footprint sum == 0, output all NaNs.
  3. large-nonsquare-even: footprint sum == 1, output max == 0.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions