Skip to content

Describe raster windows with a gWCS built from per-exposure pointing - #182

Draft
nabobalis wants to merge 10 commits into
mainfrom
gwcs_raster_v2
Draft

nabobalis wants to merge 10 commits into
mainfrom
gwcs_raster_v2

Conversation

@nabobalis

@nabobalis nabobalis commented Sep 28, 2026 •

Copy link
Copy Markdown
Member

Each spectral window is now one SpectrogramCube with a gWCS built from the AUX pointing, roll and T_OBS time of every exposure:

  • a single raster file or a sit-and-stare observation is a 3D cube;
  • a multi-file observation is a 4D cube with a leading raster scan axis.

This replaces SpectrogramCubeSequence. docs/migration.rst shows the old and new versions of the common patterns, and the changelog/182.* fragments list the breaking changes.

Reviewing

The pieces that did not depend on the rewrite are already on main: #184 (minimum versions and figure hashes), #185 (SGMeta.observer) and #186 (negative indices, SJICube.fits_wcs). What is left is four commits, each of which passes the tests on its own:

  1. Add a gWCS builder for raster windows (irispy/_spectrograph_wcs.py, with the tests that need no reader). This is where the coordinates come from, including sit-and-stare.
  2. Read each spectral window into one SpectrogramCube: the reader, combining files along the scan axis, the cube class, and moving the tests, tutorial doctests and gallery examples to the new API.
  3. Plotting: slider labels for the step and scan axes, axis positions, and nearest-neighbour images.
  4. Migration guide.

What changes for users

  • raster_slice(k) and split_rasters() give individual rasters. cube[0] now indexes the first array axis: raster step 0 on a single file, scan 0 on a combined cube.
  • cube.fits_wcs is main's FITS-TAB WCS, built per file.
  • cube.time gives the exposure times, with shape (scan, step) on a combined cube.
  • cube.celestial_frame works on spectrograph cubes, as it already does on SJI cubes, and on any slice.
  • Crops can use any subset of world coordinates: sky only, time only, steps only, or any combination. Sky crops respect each exposure's slit width. These partial crops need ndcube's crop-bounds hook (see below).
  • Removed without deprecation: SpectrogramCubeSequence and IRISSequencePlotter.
  • A window whose WCS cannot be built now raises an error naming the window and file, instead of being skipped with a log message.
  • AUX rows with all-zero or non-finite pointing or PC values, or non-finite times, are interpolated from the neighbouring exposures with a warning, for rasters and sit-and-stare alike. Rows at either end are extrapolated from the two nearest good rows. A file whose rows are all unusable raises an error, except that an all-NaN exposure-time column falls back to the planned start times.
  • The pixel scales of a rebinned cube, and so its radiometric calibration, are those of the native pixels. radiometric_calibration also accepts a cube that is already in units per second.

Coordinates

I compared the gWCS against main's FITS-TAB WCS on every exposure, several slit rows and 3 wavelengths. On 7 cached observations the largest difference is 1.25e-5″ and 0 non-finite values:

  • a 400-step raster near the limb and a 320-step raster;
  • a 99-file raster and an 8-file v34 raster;
  • two sit-and-stares, with 1020 and 1600 exposures.

At the slit centre, each exposure matches the AUX XCENIX/YCENIX to 2.4e-10″. For sit-and-stare, the along-slit offsets equal CDELT2·dy·(PC3_2, PC2_2) to 5e-7″. Tests guard both at 0.01″ and 0.001″.

dkist picks each exposure's table row with np.round, which rounds half to even, so the far pixel edge of an even number of steps had no coordinates. The builder repeats the last table row, which makes those pixel corners finite. Every other coordinate is unchanged bitwise.

ndcube

Partial crops need ndcube's _get_crop_bounds hook. That hook isn't in a release yet, and it waits on sunpy/ndcube#977. Until then:

Known cost: plots across exposures are slow

dkist's varying celestial transforms loop over every exposure, and dkist 1.18.1 still does. The figures below are for the 1600-exposure sit-and-stare, main vs this branch:

Operation main this branch
axis_world_coords() 0.04 s 1.3 s
Default plot (slit × wavelength), plot+draw 0.18 s 0.30 s
Default plot, slider step 0.05 s 0.12 s
Exposure × slit image, plot+draw 0.09 s 7.9 s
Exposure × slit image, slider step 0.10 s 14.2 s

We are accepting this for the merge and will decide before the release. The fix is a vectorised _map_transform in dkist.

Tests

Every commit passes all of these environments, and none of them changes a figure. The counts are for the last commit:

  • irispy-devdeps with remote data: 433 passed, including the doc tests.
  • Released ndcube 2.4.2: 391 passed, with 42 skips shown in the summary (the 41 partial-crop tests and the remote-data test).
  • Minimum versions (Python 3.12, astropy 8.0.0, ndcube 2.4.0 and the other declared minimums): 387 passed and the same 42 skips, without the doc tests.
  • The strict docs build (-W) passes, and all 20 gallery examples run from commit 2 onwards.

Other notes

  • Built on main at v0.9.1. It uses main's FITS-TAB builder unchanged.
  • SpectrogramCube.axis_world_coords returns C-ordered Time arrays. It works around ndcube returning N-D coordinates transposed, together with an astropy bug that prints non-C-ordered Time arrays out of order (issue to follow).

nabobalis added a commit that referenced this pull request Sep 28, 2026
Comment thread irispy/tests/test_sji_fake_data.py Fixed
Comment thread irispy/tests/test_sji_fake_data.py Fixed
Comment thread irispy/_spectrograph_wcs.py Fixed
from ndcube import NDCube
from sunpy.coordinates import HeliographicStonyhurst, Helioprojective

import irispy.io.spectrograph as spectrograph_io

from ndcube.utils.exceptions import NDCubeUserWarning

import irispy.io._raster_combine as raster_combine
Comment thread irispy/tests/test_spectrograph.py Fixed
cube = read_spectrograph_lvl2(raster_sg_file, spectral_windows="Si IV 1403")["Si IV 1403"]

with pytest.raises((IndexError, ValueError)):
cube[item]
_create_raster_gwcs describes one spectral window with dkist's varying
celestial transforms. Each exposure has its own pointing (XCENIX, YCENIX),
PC matrix and T_OBS time from the auxiliary table. Its reference pixel is
its own step and the 0-based slit reference pixel CRPIX2 - 1. For
sit-and-stare windows the AUX PC is an unscaled rotation, so the step axis
uses the slit scale CDELT2. The last table row is repeated once, because
dkist rounds half to even and would otherwise give the far pixel edge of
an even number of steps no coordinates.

Rows with all-zero or non-finite pointing or PC values, and non-finite
times, are interpolated from the neighbouring exposures, or extrapolated
from the two nearest ones at the ends, with a warning; only a table with
no usable row raises. _time_lookup extends the time table to the outer
pixel edges and gives NaN beyond them.

The module also holds the crop-bounds hook that the next commit attaches
to SpectrogramCube. Nothing calls the builder yet; the tests here cover
the parts that need no reader.
read_spectrograph_lvl2 and read_files now return one SpectrogramCube per
spectral window instead of a SpectrogramCubeSequence: a 3D cube for a
single raster file or a sit-and-stare observation, and a 4D cube with a
leading raster scan axis for a multi-file observation. Each window's WCS
comes from the builder of the previous commit. The FITS -TAB WCS that main
built stays available as cube.fits_wcs, derived from the slice of each
file's WCS kept in meta.

- The reader reverses a v34 file's AUX table once and reads the flipped
  data, so the mask, uncertainty and PC step column follow from it. AUX
  rows with unusable pointing, PC or times are interpolated with a
  warning. A file whose exposure times are all non-finite keeps the
  planned start times. A window whose WCS cannot be built raises an error
  naming the window and file instead of being dropped.
- io/_raster_combine.py stacks the per-file cubes and their WCS tables
  along the scan axis, padding a short final raster, and with memmap=True
  gives one lazy, dask-backed cube that reads each file on demand.
- SpectrogramCube gains raster_slice, split_rasters, time,
  celestial_frame, the crop-bounds hook for partial crops, native pixel
  scales for rebinned cubes and exposure-time corrections with two
  exposure axes. axis_world_coords returns C-ordered Time arrays, which
  works around an astropy bug that prints non-C-ordered times out of order.
- radiometric_calibration uses apply_exposure_time_correction and accepts
  a cube that is already in units per second.
- SpectrogramCubeSequence and IRISSequencePlotter are removed.

The partial-crop tests need ndcube's crop-bounds hook and skip visibly
without it; the devdeps and docs envs use nabobalis/ndcube@irispy-devdeps,
which has the hook. The tests, the tutorial doctests and the gallery
examples move to the new API.
…mages

With a gWCS, raster plots gain step, scan and time axes. The sliders for
them now read "Raster step" and "Scan number", and a time offset axis is
labelled "Seconds from Start". When axes_coordinates asks for longitude,
it goes on the bottom edge (and a requested latitude on the left), the
time, step and scan coordinates that were not asked for are hidden, and
this survives moving the slider. Axes are matched by physical type as
well as by label, so a cube built from a user WCS with its own axis names
is styled too. Images and animations default to nearest-neighbour
interpolation, so each slit position shows as a pixel.
docs/migration.rst shows the old and new versions of the common raster
patterns: indexing, getting individual rasters, crops, times, the FITS
WCS, the removed sequence and SJI names, and memmapped data.

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