You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
geometry_util.convert_pixel_scales_1d and convert_pixel_scales_2d widen a
scalar pixel_scales to a tuple using type(pixel_scales) is float — an exact-type test. Only a literal Python float is widened; an int, an np.float64 or an np.int32 falls through unconverted, and the caller then
subscripts a scalar. Array2D.no_mask(values=np.ones((5,5)), pixel_scales=1)
fails with TypeError: 'int' object is not subscriptable, which names nothing
the caller passed.
This is pre-existing, not a regression. It was found while implementing #333 and recorded in #440's "Out of scope"
section as the follow-up that PR points at. np.float64 matters as much as int: it is what indexing a numpy array or reading a FITS header gives you, so pixel_scales=header["CD2_2"] hits this on a path that looks perfectly
reasonable. Both functions' docstrings promise the widening, and 16 call sites
across every Mask2D factory, Grid2D.uniform and the Array2D/Grid1D
constructors funnel through them, so the broken promise is repeated across the
public API.
Plan
Replace the exact-type test in both converters with validate.is_concrete_scalar
(landed by fix: reject invalid constructor inputs (#333 — B5-B8, B13) #440), which accepts int, float, np.integer and np.floating
and rejects bool, arrays, None and JAX tracers.
Cast the widened value to Python float, so the functions deliver the Tuple[float, ...] their annotations and docstrings already promise.
Correct both docstrings, which say "float" where they mean "any real scalar".
Widen the ty.PixelScales alias so the type states what is actually accepted.
Change the 1D and 2D siblings together so they cannot drift apart.
Add converter tests and a regression test at the reported entry point, then
run the full test_autoarray suite against a clean-main baseline.
Detailed implementation plan
Work Classification
Library — single-repo, library source only. No workspace changes.
Affected Repositories
PyAutoLabs/PyAutoArray (primary)
Branch Survey
Repository
Current Branch
Dirty?
PyAutoArray
main
clean
No worktree claim on PyAutoArray (worktree_check_conflict exit 0; only
PyAutoHands is claimed, by hands-hygiene-leftovers). No open PRs on the repo.
Suggested branch:claude/autoarray-pixel-scales-int-tuple-wfxlnj Worktree root:~/Code/PyAutoLabs-wt/autoarray-pixel-scales-int-tuple/ (created later by /start_library)
Implementation Steps
autoarray/geometry/geometry_util.py, convert_pixel_scales_1d (line 58) —
replace if type(pixel_scales) is float: with if validate.is_concrete_scalar(pixel_scales):, body pixel_scales = (float(pixel_scales),). validate is already imported at
module top (line 5).
Same file, convert_pixel_scales_2d (line 231) — same predicate, body pixel_scales = (float(pixel_scales),) * 2. Land both in one change.
Update both docstrings: any concrete real scalar (int, float, np.integer, np.floating) is widened and normalised to Python float;
tuples and JAX tracers pass through untouched; bool is deliberately not
treated as a scalar.
autoarray/type.py line 4 — widen the PixelScales alias to admit int.
test_autoarray/geometry/test_geometry_util.py — new test__convert_pixel_scales_1d / test__convert_pixel_scales_2d: 1, 1.0, np.float64(1), np.int32(1) all widen; type(result[0]) is float;
tuple input returned unchanged; True not widened.
test_autoarray/test_validate.py — extend the fix: reject invalid constructor inputs (#333 — B5-B8, B13) #440 guard tests so 0, -1
and nan are covered in the int-scalar form as well as float and tuple.
Add the tracer pass-through case using the JAX-guarded pattern already there.
Regression test at the reported site: Array2D.no_mask(values=np.ones((5,5)), pixel_scales=1) builds with pixel_scales == (1.0, 1.0); a Mask2D factory with np.float64(1.0).
Run the full test_autoarray suite. Baseline first on clean main — the 3 test_transformer.py pynufft failures are known pre-existing (tracked by draft/bug/autoarray/pynufft_scipy_pinv2_dev_extra.md). Read any other
failure rather than adjusting the test: something downstream may rely on a
non-float passing through unconverted.
Key Files
autoarray/geometry/geometry_util.py — the two converters; the single
chokepoint for all 16 call sites.
autoarray/validate.py — supplies is_concrete_scalar and validate_pixel_scales; unchanged by this task.
Cast to Python float.1 becomes (1.0, 1.0), not (1, 1). It matches
the existing return annotation, keeps numpy scalars out of stored Mask2D
geometry and out of JSON serialisation, and avoids int arithmetic and numba
signature variants downstream.
Tuple entries are not normalised.pixel_scales=(1, 1) still returns ints.
Deliberate: the prompt requires tuple input be returned unchanged. The
consistency gap deserves its own prompt.
convert_shape_native_1d has the identical defect — type(shape_native) is int
at line 27, so np.int64(5) is never widened either. Same exact-type mistake,
different parameter; needs its own prompt.
Risk
Low. The change is two predicates plus a cast, at a chokepoint whose behaviour
for the currently-working input (float, tuple, tracer) is unchanged. The one
real risk is code downstream that relies on a non-float scalar passing through
unconverted — the full-suite run against a clean-main baseline is what
establishes that.
Original Prompt
Click to expand starting prompt
pixel_scales given as an int (or np.float64) is never widened to a tuple
Type: bug
Target: autoarray
Repos:
@PyAutoArray
Difficulty: small
Autonomy: supervised
Priority: medium
Status: draft
Why this exists
Found while implementing PyAutoArray#333 (the _validate_* constructor guards,
shipped 2026-08-09 as PyAutoArray#440 / f2f7a4f). Noted in that PR's "Out of
scope" section and in complete/2026/08/autoarray-input-validation-guards.md;
this prompt is the follow-up it points at. Pre-existing — not introduced by
that PR.
type(x) is float is an exact-type test, so only a literal Python float
is widened. Everything else falls through unconverted, and the caller then
subscripts a scalar.
Verified against main @ f2f7a4f (2026-08-09):
convert_pixel_scales_2d(1) -> 1 # not (1.0, 1.0)
convert_pixel_scales_2d(1.0) -> (1.0, 1.0) # OK
convert_pixel_scales_2d(np.float64(1)) -> np.float64(1) # not widened
convert_pixel_scales_1d(1) -> 1 # same bug, 1D sibling
Array2D.no_mask(values=np.ones((5,5)), pixel_scales=1)
-> TypeError: 'int' object is not subscriptable
np.float64 matters as much as int here: it is what you get from indexing a
numpy array or reading a FITS header, so pixel_scales=header["CD2_2"] can hit
this on a path that looks perfectly reasonable.
Why it is worth fixing
The docstring promises the widening — "If this is input as a float, it is
converted to a (float, float) structure" — and every Mask2D factory and Grid2D.uniform funnel through this function, so the promise is repeated across
the public API. An int pixel scale is a natural thing for a user to type.
The resulting TypeError: 'int' object is not subscriptable names nothing the
caller passed — exactly the class of failure the #333 sweep was about, which is
why it is filed rather than fixed inline there (that task was scoped to the five
findings on #333).
Suggested fix
Widen the test to any concrete real scalar rather than the exact float type. autoarray/validate.py (landed by #440) already has the predicate this needs:
is_concrete_scalar accepts int, float, np.integer, np.floating and
rejects bool, arrays, None and JAX tracers — so this both fixes the bug and
keeps the function tracer-safe. Apply to convert_pixel_scales_1d (1-tuple) and convert_pixel_scales_2d (2-tuple) together so the two do not drift.
Check before assuming this is purely additive: something downstream may rely
on a non-float passing through unconverted. Run the full test_autoarray suite
and read any failure rather than adjusting the test.
Verification
Array2D.no_mask(values=..., pixel_scales=1) builds, with pixel_scales == (1.0, 1.0).
Same for np.float64(1.0) and np.int32(1).
Tuple input is returned unchanged; a JAX tracer still passes through untouched.
Overview
geometry_util.convert_pixel_scales_1dandconvert_pixel_scales_2dwiden ascalar
pixel_scalesto a tuple usingtype(pixel_scales) is float— anexact-type test. Only a literal Python
floatis widened; anint, annp.float64or annp.int32falls through unconverted, and the caller thensubscripts a scalar.
Array2D.no_mask(values=np.ones((5,5)), pixel_scales=1)fails with
TypeError: 'int' object is not subscriptable, which names nothingthe caller passed.
This is pre-existing, not a regression. It was found while implementing
#333 and recorded in
#440's "Out of scope"
section as the follow-up that PR points at.
np.float64matters as much asint: it is what indexing a numpy array or reading a FITS header gives you, sopixel_scales=header["CD2_2"]hits this on a path that looks perfectlyreasonable. Both functions' docstrings promise the widening, and 16 call sites
across every
Mask2Dfactory,Grid2D.uniformand theArray2D/Grid1Dconstructors funnel through them, so the broken promise is repeated across the
public API.
Plan
validate.is_concrete_scalar(landed by fix: reject invalid constructor inputs (#333 — B5-B8, B13) #440), which accepts
int,float,np.integerandnp.floatingand rejects
bool, arrays,Noneand JAX tracers.validate.validate_pixel_scalescall ahead of the widening, so thefix: reject invalid constructor inputs (#333 — B5-B8, B13) #440 guards still reject
0,-1andnanin scalar form.float, so the functions deliver theTuple[float, ...]their annotations and docstrings already promise.ty.PixelScalesalias so the type states what is actually accepted.run the full
test_autoarraysuite against a clean-mainbaseline.Detailed implementation plan
Work Classification
Library — single-repo, library source only. No workspace changes.
Affected Repositories
Branch Survey
No worktree claim on PyAutoArray (
worktree_check_conflictexit 0; onlyPyAutoHands is claimed, by
hands-hygiene-leftovers). No open PRs on the repo.Suggested branch:
claude/autoarray-pixel-scales-int-tuple-wfxlnjWorktree root:
~/Code/PyAutoLabs-wt/autoarray-pixel-scales-int-tuple/(created later by/start_library)Implementation Steps
autoarray/geometry/geometry_util.py,convert_pixel_scales_1d(line 58) —replace
if type(pixel_scales) is float:withif validate.is_concrete_scalar(pixel_scales):, bodypixel_scales = (float(pixel_scales),).validateis already imported atmodule top (line 5).
convert_pixel_scales_2d(line 231) — same predicate, bodypixel_scales = (float(pixel_scales),) * 2. Land both in one change.int,float,np.integer,np.floating) is widened and normalised to Pythonfloat;tuples and JAX tracers pass through untouched;
boolis deliberately nottreated as a scalar.
autoarray/type.pyline 4 — widen thePixelScalesalias to admitint.test_autoarray/geometry/test_geometry_util.py— newtest__convert_pixel_scales_1d/test__convert_pixel_scales_2d:1,1.0,np.float64(1),np.int32(1)all widen;type(result[0]) is float;tuple input returned unchanged;
Truenot widened.test_autoarray/test_validate.py— extend the fix: reject invalid constructor inputs (#333 — B5-B8, B13) #440 guard tests so0,-1and
nanare covered in the int-scalar form as well as float and tuple.Add the tracer pass-through case using the JAX-guarded pattern already there.
Array2D.no_mask(values=np.ones((5,5)), pixel_scales=1)builds withpixel_scales == (1.0, 1.0); aMask2Dfactory withnp.float64(1.0).test_autoarraysuite. Baseline first on cleanmain— the 3test_transformer.pypynufft failures are known pre-existing (tracked bydraft/bug/autoarray/pynufft_scipy_pinv2_dev_extra.md). Read any otherfailure rather than adjusting the test: something downstream may rely on a
non-float passing through unconverted.
Key Files
autoarray/geometry/geometry_util.py— the two converters; the singlechokepoint for all 16 call sites.
autoarray/validate.py— suppliesis_concrete_scalarandvalidate_pixel_scales; unchanged by this task.autoarray/type.py— thePixelScalesalias.test_autoarray/geometry/test_geometry_util.py,test_autoarray/test_validate.py— coverage.Decisions pinned
float.1becomes(1.0, 1.0), not(1, 1). It matchesthe existing return annotation, keeps numpy scalars out of stored
Mask2Dgeometry and out of JSON serialisation, and avoids int arithmetic and numba
signature variants downstream.
boolstays rejected.is_concrete_scalarexcludes it deliberately(fix: reject invalid constructor inputs (#333 — B5-B8, B13) #440's reasoning), so
pixel_scales=Trueis still not widened. Pinned by atest so it is deliberate rather than incidental.
Out of scope (follow-ups)
pixel_scales=(1, 1)still returns ints.Deliberate: the prompt requires tuple input be returned unchanged. The
consistency gap deserves its own prompt.
convert_shape_native_1dhas the identical defect —type(shape_native) is intat line 27, so
np.int64(5)is never widened either. Same exact-type mistake,different parameter; needs its own prompt.
Risk
Low. The change is two predicates plus a cast, at a chokepoint whose behaviour
for the currently-working input (
float, tuple, tracer) is unchanged. The onereal risk is code downstream that relies on a non-
floatscalar passing throughunconverted — the full-suite run against a clean-
mainbaseline is whatestablishes that.
Original Prompt
Click to expand starting prompt
pixel_scalesgiven as anint(ornp.float64) is never widened to a tupleType: bug
Target: autoarray
Repos:
Difficulty: small
Autonomy: supervised
Priority: medium
Status: draft
Why this exists
Found while implementing PyAutoArray#333 (the
_validate_*constructor guards,shipped 2026-08-09 as PyAutoArray#440 /
f2f7a4f). Noted in that PR's "Out ofscope" section and in
complete/2026/08/autoarray-input-validation-guards.md;this prompt is the follow-up it points at. Pre-existing — not introduced by
that PR.
The defect
autoarray/geometry/geometry_util.py:type(x) is floatis an exact-type test, so only a literal Pythonfloatis widened. Everything else falls through unconverted, and the caller then
subscripts a scalar.
Verified against
main@f2f7a4f(2026-08-09):np.float64matters as much asinthere: it is what you get from indexing anumpy array or reading a FITS header, so
pixel_scales=header["CD2_2"]can hitthis on a path that looks perfectly reasonable.
Why it is worth fixing
The docstring promises the widening — "If this is input as a
float, it isconverted to a
(float, float)structure" — and everyMask2Dfactory andGrid2D.uniformfunnel through this function, so the promise is repeated acrossthe public API. An
intpixel scale is a natural thing for a user to type.The resulting
TypeError: 'int' object is not subscriptablenames nothing thecaller passed — exactly the class of failure the #333 sweep was about, which is
why it is filed rather than fixed inline there (that task was scoped to the five
findings on #333).
Suggested fix
Widen the test to any concrete real scalar rather than the exact
floattype.autoarray/validate.py(landed by #440) already has the predicate this needs:is_concrete_scalaracceptsint,float,np.integer,np.floatingandrejects
bool, arrays,Noneand JAX tracers — so this both fixes the bug andkeeps the function tracer-safe. Apply to
convert_pixel_scales_1d(1-tuple) andconvert_pixel_scales_2d(2-tuple) together so the two do not drift.Check before assuming this is purely additive: something downstream may rely
on a non-float passing through unconverted. Run the full
test_autoarraysuiteand read any failure rather than adjusting the test.
Verification
Array2D.no_mask(values=..., pixel_scales=1)builds, withpixel_scales == (1.0, 1.0).np.float64(1.0)andnp.int32(1).0,-1andnanin both thescalar and tuple forms.
floator kept in itsinput type —
(1, 1)vs(1.0, 1.0)— since downstream arithmetic differs.Repro environment:
PYAUTO_SKIP_WORKSPACE_VERSION_CHECK=1,NUMBA_CACHE_DIR=/tmp/numba_cache,MPLCONFIGDIR=/tmp/matplotlib,PYAUTO_DISABLE_JAX=1.Provenance
complete/2026/08/autoarray-input-validation-guards.mdplanned.md§rhayes-audit-validation-phases-2-4) — this was not one of his 16 findings.