fix: widen int / numpy scalar pixel_scales to a tuple - #465
Merged
Conversation
`convert_pixel_scales_1d` and `convert_pixel_scales_2d` tested `type(pixel_scales) is float`, an exact-type check, so only a literal Python `float` was widened to the tuple form both functions promise. An `int`, an `np.floating` or an `np.integer` fell through unconverted. The two functions are the single chokepoint that every `Mask2D` factory, `Grid2D.uniform` and the `Array2D`/`Array1D`/`Grid1D` constructors funnel `pixel_scales` through — 16 call sites — so the broken promise was repeated across the public API. `Array2D.no_mask(values=..., pixel_scales=1)` stored the bare `1` on the mask and every later use of it raised `TypeError: 'int' object is not subscriptable`, naming nothing the caller passed; `Grid2D.uniform` and `Mask2D.circular` raised it outright. An `int` is a natural thing to type by hand, and an `np.floating` is what indexing an array or reading a FITS header returns, so both arrive on ordinary paths. Test any concrete real scalar instead, via `validate.is_concrete_scalar`, and cast the widened value to a Python `float` so the returned tuple matches the `Tuple[float, ...]` annotation regardless of input type — this also keeps NumPy scalars out of stored geometry and out of JSON serialisation. `is_concrete_scalar` excludes `bool` and JAX tracers, so a traced value still passes through untouched and the function stays safe inside a `jax.jit`. The validation guards run ahead of the widening and are unchanged, so `0`, `-1` and `nan` are still rejected — now in their integer and NumPy forms too. Applied to the 1D and 2D siblings together so they cannot drift apart. Pre-existing defect, not a regression; the follow-up #440 pointed at. Not fixed here, each needing its own change: tuple entries are still returned unnormalised (`(1, 1)` stays ints), and `convert_shape_native_1d` carries the identical `type(x) is int` exact-type check for `np.integer`. Validation: 1145 passed / 1 skipped / 3 failed; the 3 are the known pre-existing pynufft failures in `test_transformer.py`, baselined by re-running them on a clean tree (identical 3). 27 new tests, all confirmed to fail without the source change. Closes #464 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013th2YEQwwnoiA7aEufSutT
Jammy2211
pushed a commit
to PyAutoLabs/PyAutoMind
that referenced
this pull request
Aug 22, 2026
Library PR PyAutoLabs/PyAutoArray#465 open with pending-release; status moves to library-shipped, awaiting-merge. Records the gate honestly: PyAutoHeart is absent from this web session, so the WORKFLOW.md test-suite fallback stood in for the readiness verdict — local suite 1145 passed / 1 skipped with zero unexplained failures, the 3 pre-existing pynufft failures baselined identical on a clean tree. Completion record is deliberately not written yet — merge is a human act and CI has not reported. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013th2YEQwwnoiA7aEufSutT
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
convert_pixel_scales_1dandconvert_pixel_scales_2dwidened a scalarpixel_scalesto thetuple form using
type(pixel_scales) is float— an exact-type check. Only a literal Pythonfloatwas widened; anint, annp.floatingor annp.integerfell through unconverted.These two functions are the single chokepoint that every
Mask2Dfactory,Grid2D.uniformandthe
Array2D/Array1D/Grid1Dconstructors funnelpixel_scalesthrough — 16 call sites —so the broken promise was repeated across the public API. On current
main,Array2D.no_mask(values=np.ones((5, 5)), pixel_scales=1)constructs successfully and stores thebare
1on the mask; theTypeError: 'int' object is not subscriptablethen surfaces on firstuse (
.pixel_scale,.derive_grid, anything touching geometry), naming nothing the caller passed.Grid2D.uniformandMask2D.circularraise it outright. Anintpixel scale is a natural thingto type by hand, and an
np.floatingis what indexing a numpy array or reading a FITS headerreturns (
pixel_scales=header["CD2_2"]), so both arrive on paths a user would consider ordinary.The fix tests any concrete real scalar via
validate.is_concrete_scalar(landed by #440) and caststhe widened value to a Python
float, so the returned tuple matches theTuple[float, ...]annotation regardless of input type. That cast also keeps NumPy scalars out of stored geometry and
out of JSON serialisation.
is_concrete_scalarexcludesbooland JAX tracers, so a traced valuestill passes through untouched and the function stays safe inside a
jax.jit. The validationguards run ahead of the widening and are unchanged, so
0,-1andnanare still rejected —now in their integer and NumPy forms too.
Applied to the 1D and 2D siblings together so they cannot drift apart.
Pre-existing defect, not a regression — this is the follow-up #440 pointed at in its "Out of
scope" section.
Closes #464
API Changes
Behaviour of an existing public conversion widens; nothing is removed or renamed. Every
constructor taking
pixel_scalesnow accepts anintor a NumPy real scalar where previouslyonly an exact
floatworked, and the value stored is always a Pythonfloattuple. Thety.PixelScalesalias was widened to state what is actually accepted. Purely additive inpractice — no input that worked before behaves differently, and the full suite confirms nothing
downstream relied on a non-
floatscalar passing through unconverted.See full details below.
Test Plan
python -m pytest test_autoarray/— 1145 passed / 1 skipped, plus the 3 pre-existingtest_transformer.pypynufft failures (see below).aa.Array2D.no_mask(values=np.ones((5, 5)), pixel_scales=1).pixel_scales == (1.0, 1.0),and the same for
np.float64(1.0)/np.int32(1).aa.Grid2D.uniform(shape_native=(5, 5), pixel_scales=1)andaa.Mask2D.circular(shape_native=(5, 5), radius=2.0, pixel_scales=1)build.pixel_scales=0/-1/nanare still rejected, in scalar, integer, NumPy andtuple-entry forms.
Validation. 27 new tests, each confirmed to fail without the source change (stashing only
geometry_util.py+type.pyproduces 12 failures across the new cases; 0 with the fix). The 3failing tests in
test_autoarray/operators/test_transformer.pyare pre-existing and unrelated— a pynufft/scipy incompatibility (
pynufft/src/_helper/helper.py:1055: AttributeError), trackedseparately. Baselined by stashing the whole change and re-running on a clean tree: identical 3.
Zero regressions.
Deliberately out of scope
Each needs its own change rather than widening this one:
pixel_scales=(1, 1)still returns ints. The tuple pathis required to return its input unchanged.
convert_shape_native_1dcarries the identical defect —type(shape_native) is intatgeometry_util.py:27, sonp.int64(5)is never widened either. Same exact-type mistake,different parameter.
Full API Changes (for automation & release notes)
Changed Behaviour
autoarray.util.geometry.convert_pixel_scales_1d(pixel_scales)— widens any concrete realscalar (
int,float,np.integer,np.floating) to(float,), not only an exactfloat.The widened entry is cast to a Python
float.bool, JAX tracers, and tuples are returnedunchanged, as before.
autoarray.util.geometry.convert_pixel_scales_2d(pixel_scales)— same, to(float, float).intor NumPy realscalar
pixel_scales, and stores it as a Pythonfloattuple. Affects theMask2Dfactories,Grid2D.uniform,Grid1D,Array2D,Array1DandVectorYX2D.autoarray.type.PixelScales— alias widened fromUnion[Tuple[float], Tuple[float, float], float]to additionally includeint,np.floatingand
np.integer. Annotation-only; no runtime behaviour depends on it.Removed
None.
Added
None.
Renamed
None.
Changed Signature
None.
Migration
None required — no input that previously worked behaves differently. Inputs that previously
produced a
TypeError(or silently stored a non-tuplepixel_scales) now work:aa.Array2D.no_mask(values=..., pixel_scales=1)→ stored1; later use raisedTypeError: 'int' object is not subscriptable.aa.Array2D.no_mask(values=..., pixel_scales=1)→pixel_scales == (1.0, 1.0).Generated by the PyAutoLabs agent workflow.
Generated by Claude Code