Skip to content

Count CRPIX from zero, as named_arrays does - #45

Merged
roytsmart merged 1 commit into
mainfrom
fix/crpix-origin
Aug 15, 2026
Merged

Count CRPIX from zero, as named_arrays does#45
roytsmart merged 1 commit into
mainfrom
fix/crpix-origin

Conversation

@roytsmart

Copy link
Copy Markdown
Contributor

SpectrographObservation.from_fits put the CRPIX keywords into
named_arrays.AbstractWcsVector exactly as astropy.wcs reported them,
and astropy reports them as the header writes them, which FITS counts
from one.

The formula they are used in is the one in the WCS paper and is written
out correctly:

p = na.CartesianNdVectorArray(na.indices(shape_wcs)) - 0.5
q = m @ (p - r)
x = s * q + crval

but p comes from named_arrays.indices and so counts from zero, while
r arrived counting from one. Subtracting one from the other mixes the
two conventions and moves everything by a pixel. The - 0.5 already
there is a separate thing: it turns cell centers into the cell edges this
grid is built on, and says nothing about where counting starts.

Measured on iris_l2_20210923_...:

axis one pixel
wavelength, Si IV 1394 0.0127 Å = 2.76 km/s
solar x, across the raster 0.349″
solar y, along the slit 0.166″

cdelt is positive, so wavelengths came out one pixel low and velocities
carried a systematic blue bias of about 3 km/s. That is the one worth
noting: an offset in arcseconds looks like an offset, while an offset in
wavelength looks like a Doppler shift.

Testing

test_inputs_against_astropy_wcs asks astropy.wcs the same question on
all three axes, at four places in the cube. It is built with from_fits
from one file rather than from a time range, so the coordinates and the
keywords they are checked against come from the same file.

The projection is taken off before comparing, since AbstractWcsVector is
the linear part of the transformation and does not apply one. Left in, it
contributes about 2.5e-5 arcsec, which is nowhere near the pixel being
looked for, but taking it out makes the two answers the same answer
rather than nearly the same.

Reverting the fix fails the new test by 9.8e-5 deg, which is 0.353″, one
pixel across the raster. The 16 tests in iris/sg/_spectrograph_test.py
pass with it.

Related

The same bug is open against AIA as sun-data/solar-dynamics-observatory#21,
and was fixed for HMI in sun-data/solar-dynamics-observatory#20. Those
three are the only places in the group's packages that read CRPIX from a
header.

Anything already derived from these coordinates moves by a pixel, so
results built on this will shift.

🤖 Generated with Claude Code

https://claude.ai/code/session_011q4461XE8hCcZCKsViMQC1

@codecov

codecov Bot commented Aug 14, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 100.00%. Comparing base (233bcf6) to head (f426345).

Additional details and impacted files
@@            Coverage Diff            @@
##              main       #45   +/-   ##
=========================================
  Coverage   100.00%   100.00%           
=========================================
  Files           16        16           
  Lines          536       561   +25     
=========================================
+ Hits           536       561   +25     
Flag Coverage Δ
unittests 100.00% <100.00%> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@roytsmart
roytsmart force-pushed the fix/crpix-origin branch 3 times, most recently from 05316c6 to e616b94 Compare August 15, 2026 16:25
`astropy.wcs` hands back the keyword as the header writes it, and FITS
counts pixels from one. The formula it is put into is the one in the WCS
paper and is written out correctly, but the pixel coordinates given to it
come from `named_arrays.indices` and so start at zero. Measuring the
reference point from one end of the first pixel and the pixels themselves
from the other moves every observation by one pixel.

On a raster that is a third of an arcsecond across and a sixth along the
slit. On the wavelength axis it is about 3 km/s of Doppler shift, which
is the one that would go unremarked, since it looks like a velocity
rather than like a mistake.

The coordinates are now checked against `astropy.wcs` on all three axes,
built from the same file so that there is nothing else the two could
disagree about. The projection is taken off first: `AbstractWcsVector` is
the linear part of the transformation, and over a field this size the
projection is worth a few times ten to the minus five arcseconds, far
below the pixel this is looking for.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011q4461XE8hCcZCKsViMQC1
@roytsmart
roytsmart merged commit b09a55b into main Aug 15, 2026
11 checks passed
@roytsmart
roytsmart deleted the fix/crpix-origin branch August 15, 2026 17:26
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