Inconsistent behavior of world_to_pixel in SlicedLowLevelWCS
Verdict
Change line 254 of astropy/astropy/wcs/wcsapi/wrappers/sliced_wcs.py. Right now world_to_pixel_values fills every world axis the slice dropped with the constant 1.. It should use that axis's real world value at the slice position. Then add a regression test built from the reporter's header that includes a non-zero spectral slice.
First step this week: In sliced_wcs.py, add the guard before line 249 (if len(self._world_keep) < self._wcs.world_n_dim: then compute sliced_out_world_coords = self._pixel_to_world_values_all(*[0] * len(self._pixel_keep))). Then replace line 254 with world_arrays_new.append(sliced_out_world_coords[iworld]).
Confidence
Medium. All four labs agree on the root cause and the line, but nobody ran the reproduction, the labs disagree on where the replacement value should come from, and Grok's challenge raised edge cases nobody has tested.
Agreement
2 of 4 agree; 2 partly agree (Gemini, Grok).
What changed under review
- Claude first warned that a WCS with a single world axis could return a plain number and suggested wrapping it in a tuple; after re-reading lines 152-154 (which raise if no world axis is kept), Claude dropped that warning and GPT withdrew the same concern.
- Claude first ran the extra pixel-to-world transform on every call; in its second answer it added a guard so the transform only runs when at least one world axis is dropped, and GPT adopted the same guard.
- Claude first said the PC2_3 term pushes the error into spatial axis 2; Grok's review worked through the matrix and showed the error actually lands in the first returned pixel (pixel axis 0), with the second staying at 12.
Why
world_to_pixel_valuesrebuilds the full world vector and appends the constant1.for every world axis the slice dropped. The underlying inverse transform then runs with a wrong wavelength. With the reporter's PC matrix, which mixes wavelength into a spatial axis, that wrong value spills into a pixel axis you kept. (astropy/astropy/wcs/wcsapi/wrappers/sliced_wcs.py, lines 249-254)- The pixel-to-world direction already fills each dropped pixel axis with the real slice index,
self._slices_pixel[ipix]. That is whypixel_to_worldgives the right answer on the slice and onlyworld_to_pixelis wrong. (astropy/astropy/wcs/wcsapi/wrappers/sliced_wcs.py, lines 212-227 (line 218)) - The fix computes the dropped values at kept pixel position 0. That is safe because the code drops a world axis only when it depends on none of the kept pixel axes, so the placeholder 0s cannot change its value. (astropy/astropy/wcs/wcsapi/wrappers/sliced_wcs.py, lines 149-150)
Where they disagreed
- Where the replacement value for dropped world axes should come from. Claude and GPT: recompute with
_pixel_to_world_values_all(*[0] * len(self._pixel_keep)), guarded, and index it byiworld. Gemini and Grok: read the cachedself.dropped_world_dimensions["value"](lines 158-174) inside the else branch and index it with a counter of dropped axes, because that list holds only the dropped axes, not the full world vector. Not settled. To settle it: Checkdropped_world_dimensions["value"]against the forward transform at the slice position, including axis order and units. Then run the same regression tests against both patches. - Whether to wrap the recomputed result in a tuple. Gemini: wrap it in case the underlying WCS has a single world axis. Claude, GPT and Grok: not needed. Settled against the tuple wrap: lines 152-154 raise if no world axis is kept, so a single world axis is always kept and the indexing branch never runs.
- Whether the low-level interface works in SI units, which would explain why
1.means 1 metre. Claude said the low-level interface works in SI. GPT said this is not a general rule. Grok said it is not proven from these files, becauseworld_axis_unitsreturns CUNIT (Angstrom) and the conversion would happen somewhere the panel could not see. Not settled. To settle it: Run the reporter's script and look at the raw values passed to and returned by the underlyingworld_to_pixel_valuesfor the wavelength axis. The fix takes its value from the same transform either way, so the patch does not depend on the answer.
The case against
Grok: the patch is plausible but not yet safe to apply as written. The dependency table only describes the forward direction. If it under-reports how axes are linked, evaluating at kept pixel 0 could quietly install a plausible but wrong constant, and that is worse than an obvious 1e11. A hard-coded 0 could also fall outside the valid range for TAN, SIP, TAB or log axes and give an error or NaN. Grok says CRPIX or the stored slice value would be safer. Nobody checked units, axis order, broadcasting, or range and step slices. The proposed (49.5, 12) test can pass for the wrong reason. Grok's advice is to run the reproduction and compare against the unsliced WCS at a non-zero and a non-integer slice before merging.
Not verified
- No lab ran the reproduction. The 1.81818182e+11 ≈ 1 / 5.5e-12 match is arithmetic on values copied from the bug report, not an observed result.
- The claim that the dropped
1.is read as 1 metre rather than 1 Angstrom. Where the unit conversion happens was not checked. - Grok's claim that at spectral slice 1 the coupling moves the spatial answer by +1.
- Whether evaluating at kept pixel 0 is inside the valid range for every projection type (raised by Grok, not checked).
- Behaviour with array broadcasting, range or step slices, and other callers of
world_to_pixel_valuesoutside the wcsapi files.
What would change the answer
- After the patch, running the reporter's header still does not reproduce the unsliced (49.5, 12.) result, or does not match the unsliced WCS at a non-zero slice.
- A WCS where a dropped world axis actually depends on a kept pixel axis but the code's axis-dependency table does not report it.
- Evaluating at kept pixel 0 returns NaN or an error for a non-linear projection, which would favour CRPIX or the cached slice value instead.
dropped_world_dimensions["value"]turns out to disagree with the forward transform in order or units, or the reverse.
Next steps
- This week: in sliced_wcs.py, add the guard before line 249 (
if len(self._world_keep) < self._wcs.world_n_dim:then computesliced_out_world_coords = self._pixel_to_world_values_all(*[0] * len(self._pixel_keep))). Then replace line 254 withworld_arrays_new.append(sliced_out_world_coords[iworld]). - Add a regression test in wrappers/tests/test_sliced_wcs.py using the reporter's coupled 3D header. Assert that slice 0 maps world (0, 0) to about (49.5, 12.).
- In the same test, add a non-zero spectral slice and a pixel→world→pixel round trip, and compare against the unsliced WCS at the same slice index. This shows the fix uses the real slice value and not just CRVAL.
- Check
dropped_world_dimensions["value"]against the recomputed values. If they match, decide whether to use the cached property instead. - Run the full astropy wcs test suite and downstream ndcube checks before merging.
Each model's position (4)
Claude (Anthropic): Line 254 is the bug. The dropped wavelength becomes 1 instead of its real value, and the off-diagonal PC term carries that error into a spatial pixel. The reverse direction is right because it uses the real slice index (line 218). Fix: compute the dropped world values by running pixel-to-world at the slice position and use them instead of 1. Only do this when an axis is actually dropped. Treats the 1.8e11 arithmetic as plausible, not confirmed.
Gemini (Google): Agrees the root cause is world_arrays_new.append(1.). Its own patch takes the dropped values from the existing dropped_world_dimensions["value"], using a counter of dropped axes. Later it backed the recompute approach too, but kept saying the result should be wrapped in a tuple in case the underlying WCS has only one world axis.
GPT (OpenAI): Same root cause and same patch: a guarded recompute before the loop, then line 254 replaced. Withdrew its own worry about a plain-number return. Says the reported header fits the numbers, but did not run it. Disputes that the low-level interface always works in SI units. Wants tests for the reported case, a round trip, and a non-zero spectral slice.
Grok (xAI): Agrees on line 254 and on the cause. Would not ship the patch or the test as written. Prefers reading the cached dropped_world_dimensions["value"] inside the else branch, indexed by a counter of dropped axes. Says the reported-case test is too weak because slice 0 sits on the reference pixel. Says the error lands in the first returned pixel, not spatial axis 2. Disputes the SI-units claim as unproven.