Overview
The pixel_scales scalar-widening fix shipped in PyAutoArray#464 (8298d74e, 2026-08-22)
did not reach every site of the defect it closed. Two sites of the same class are live on
main, both confirmed by repro: Mask1D.__init__ hand-rolls its own type(x) is float
check and never routes through convert_pixel_scales_1d, and convert_shape_native_1d
keeps type(x) is int (declared out of scope by that commit). Both raise on ordinary user
input, with errors naming nothing the caller passed.
Plan
- Route
Mask1D.__init__ through geometry_util.convert_pixel_scales_1d, the same
chokepoint Mask2D.__init__ already uses — closing a 1D/2D divergence rather than
duplicating a check.
- Widen
convert_shape_native_1d to accept any concrete integer scalar (int,
np.integer), cast to a Python int, with bool excluded.
- Add the integer predicate to
autoarray/validate.py beside is_concrete_scalar, so the
function delegates its type test the way its convert_pixel_scales_* siblings do.
- Test both sites, each new test confirmed to fail without the source change, and run the
full test_autoarray suite because site 1 changes the Mask1D contract.
- Leave tuple-entry normalisation (
(1, 1) staying ints) explicitly unfixed — it changes
return values on paths that work today.
Detailed implementation plan
Affected Repositories
- PyAutoArray (primary, library-only — no workspace follow-up)
Branch Survey
| Repository |
Current Branch |
Dirty? |
| ./PyAutoArray |
main |
clean |
| ./PyAutoMind |
main |
clean |
No worktree claims; worktree_check_conflict returned clean.
Suggested branch: feature/mask1d-shape-native-scalar-widening
Site 1 — Mask1D.__init__ never routes through the chokepoint
autoarray/mask/mask_1d.py:71:
if type(pixel_scales) is float:
pixel_scales = (pixel_scales,)
Confirmed on main:
aa.Mask1D(mask=np.array([False, False, True]), pixel_scales=1).pixel_scales
-> 1 # bare int, not (1.0,)
...that mask's .geometry.scaled_maxima
-> TypeError: 'int' object is not subscriptable
Mask2D.__init__ is unaffected — it calls geometry_util.convert_pixel_scales_2d at
autoarray/mask/mask_2d.py:218. Grid1D.uniform is fine for the same reason. So this is
a 1D/2D divergence, not a design choice.
Replace the hand-rolled widening with the call Mask2D makes, keeping its current position
(after invert handling, before the len(mask.shape) guard) so ordering is unchanged:
pixel_scales = geometry_util.convert_pixel_scales_1d(pixel_scales=pixel_scales)
Behaviour change to state in the PR, not suppress: convert_pixel_scales_1d runs
validate.validate_pixel_scales first, so Mask1D starts rejecting 0, negative and nan
pixel scales — exactly as Mask2D already does. No test in test_autoarray constructs a
Mask1D with such a value, and the 12 library call sites all pass real scales, but any
failure this produces must be read rather than adjusted away.
Site 2 — convert_shape_native_1d keeps an exact-type check
autoarray/geometry/geometry_util.py:27 still has type(shape_native) is int. Reachable
through Array1D.full / zeros / ones, whose sole call site is
autoarray/structures/arrays/uniform_1d.py:143 and which then does shape_native[0]:
aa.Array1D.full(fill_value=1.0, shape_native=np.int32(5))
-> IndexError: invalid index to scalar variable
is_concrete_scalar is the wrong predicate here — shape_native is a pixel count, so
a float must not be silently widened. Use an integer-only test excluding bool, and cast
to int so the result matches the Tuple[int] annotation. Add it as is_concrete_integer
in autoarray/validate.py beside is_concrete_scalar (validate.py:48).
Update the docstring to match, mirroring the wording 8298d74e gave convert_pixel_scales_1d.
Do not add validate.validate_shape_native here — the function performs no validation
today and adding it is a separate change.
Explicitly out of scope
Tuple entries are still returned unnormalised: convert_pixel_scales_2d((1, 1)) → (1, 1),
contradicting the Tuple[float, float] annotation. It changes return values on paths that
currently work, so it needs its own change and its own suite read.
Tests
Mirror the naming convention 8298d74e established
(test__convert_pixel_scales_1d__widens_any_real_scalar).
test_autoarray/geometry/test_geometry_util.py — convert_shape_native_1d widens int
and np.integer to (int,) with a Python int entry; a bool is not widened; a (5,)
tuple is returned unchanged.
test_autoarray/mask/test_mask_1d.py — Mask1D(mask=…, pixel_scales=1) gives
(1.0,), likewise np.float64(1.0) and np.int32(1); .geometry.scaled_maxima no
longer raises; a (1.0,) tuple is unchanged; ValueError on 0 / -1 / nan; a JAX
tracer still passes through untouched so Mask1D stays jit-safe.
- End-to-end:
Array1D.full(fill_value=1.0, shape_native=np.int32(5)) builds.
Every new test must be confirmed to fail without the source change.
Verification
PYAUTO_SKIP_WORKSPACE_VERSION_CHECK=1 NUMBA_CACHE_DIR=/tmp/numba_cache \
MPLCONFIGDIR=/tmp/matplotlib PYAUTO_DISABLE_JAX=1 \
python -m pytest test_autoarray -q
Full suite, not just the touched files. Baseline the known pre-existing pynufft failures in
test_transformer.py by re-running them on a clean tree and comparing counts; 8298d74e
recorded 1145 passed / 1 skipped / 3 failed. Read any new failure rather than adjusting
the test. Then a JAX-enabled pass for the tracer case.
Key Files
autoarray/mask/mask_1d.py — site 1, the hand-rolled widening at line 71
autoarray/geometry/geometry_util.py — site 2, convert_shape_native_1d at line 27
autoarray/validate.py — home of is_concrete_scalar; add is_concrete_integer
autoarray/mask/mask_2d.py:218 — the pattern site 1 mirrors
autoarray/structures/arrays/uniform_1d.py:143 — sole caller of convert_shape_native_1d
test_autoarray/geometry/test_geometry_util.py, test_autoarray/mask/test_mask_1d.py
Original Prompt
Click to expand starting prompt
Scalar widening: the two sites the pixel_scales sweep did not reach
Type: bug
Target: autoarray
Repos:
- @PyAutoArray
Difficulty: small
Autonomy: supervised
Priority: medium
Status: draft
Filed: 2026-08-23
Why this exists
PyAutoArray#464 (8298d74e, 2026-08-22) fixed convert_pixel_scales_1d and
convert_pixel_scales_2d: type(x) is float became validate.is_concrete_scalar,
so int, np.integer and np.floating are all widened and cast to float.
Re-running that prompt's repro on 2026-08-23 found the sweep did not reach every site.
Two defects of the same class are live on main. Both are confirmed, not suspected.
Completion record: complete/2026/08/autoarray-pixel-scales-scalar-widening.md.
Site 1 — Mask1D.__init__ never routes through the chokepoint
autoarray/mask/mask_1d.py:71 still carries the original exact-type check and does its
own widening rather than calling convert_pixel_scales_1d:
if type(pixel_scales) is float:
pixel_scales = (pixel_scales,)
aa.Mask1D(mask=np.array([False, False, True]), pixel_scales=1).pixel_scales
-> 1 # bare int, not (1.0,)
...that mask's .geometry.scaled_maxima
-> TypeError: 'int' object is not subscriptable
This is #464's exact reported symptom, on a public constructor. Mask2D.__init__
is unaffected — it calls geometry_util.convert_pixel_scales_2d at
autoarray/mask/mask_2d.py:218. So this is a 1D/2D divergence, not a design choice,
and Grid1D.uniform is fine because it goes through geometry_util.
Fix: replace the hand-rolled widening with the call Mask2D makes:
pixel_scales = geometry_util.convert_pixel_scales_1d(pixel_scales=pixel_scales)
Consequence to state, not suppress: convert_pixel_scales_1d runs
validate.validate_pixel_scales first, so Mask1D starts rejecting 0, negative and
nan pixel scales — exactly as Mask2D already does. No test in test_autoarray
constructs a Mask1D with such a value, and the 12 library call sites all pass real
scales, but read any failure this produces rather than adjusting the test.
Site 2 — convert_shape_native_1d keeps type(x) is int
autoarray/geometry/geometry_util.py:27. 8298d74e listed this as not-fixed-here.
Reachable through Array1D.full / zeros / ones, whose sole call site is
autoarray/structures/arrays/uniform_1d.py:143 and which then does shape_native[0]:
aa.Array1D.full(fill_value=1.0, shape_native=np.int32(5))
-> IndexError: invalid index to scalar variable
Fix: widen to an integer-only test — is_concrete_scalar is the wrong predicate
here, since shape_native is a pixel count and a float must not be silently widened.
Use isinstance(x, (int, np.integer)) and not isinstance(x, bool), cast to int so the
result matches the Tuple[int] annotation. Prefer adding this as is_concrete_integer
in autoarray/validate.py beside is_concrete_scalar (validate.py:48), so the
function delegates its predicate the way its convert_pixel_scales_* siblings do.
Do not add validate.validate_shape_native here — the function performs no
validation today and adding it is a separate change.
Explicitly out of scope
Tuple entries are still returned unnormalised: convert_pixel_scales_2d((1, 1)) → (1, 1),
contradicting the Tuple[float, float] annotation. This changes return values on paths
that currently work, so it needs its own change and its own suite read. Unfiled.
Verification
Mask1D(mask=…, pixel_scales=1).pixel_scales == (1.0,); same for np.float64(1.0)
and np.int32(1); .geometry.scaled_maxima no longer raises.
- A
(1.0,) tuple is returned unchanged; a JAX tracer still passes through untouched,
so Mask1D stays jit-safe.
Mask1D raises ValueError on pixel_scales of 0, -1 and nan, matching Mask2D.
convert_shape_native_1d widens int and np.integer to (int,) with a Python int
entry; a bool is not widened; a (5,) tuple is unchanged.
Array1D.full(fill_value=1.0, shape_native=np.int32(5)) builds.
- Every new test confirmed to fail without the source change.
- Full
test_autoarray suite, not just the touched files — site 1 changes the Mask1D
contract. Baseline the known pre-existing pynufft failures in test_transformer.py
against a clean tree; 8298d74e recorded 1145 passed / 1 skipped / 3 failed.
Repro environment: PYAUTO_SKIP_WORKSPACE_VERSION_CHECK=1,
NUMBA_CACHE_DIR=/tmp/numba_cache, MPLCONFIGDIR=/tmp/matplotlib,
PYAUTO_DISABLE_JAX=1.
Provenance
Overview
The
pixel_scalesscalar-widening fix shipped in PyAutoArray#464 (8298d74e, 2026-08-22)did not reach every site of the defect it closed. Two sites of the same class are live on
main, both confirmed by repro:Mask1D.__init__hand-rolls its owntype(x) is floatcheck and never routes through
convert_pixel_scales_1d, andconvert_shape_native_1dkeeps
type(x) is int(declared out of scope by that commit). Both raise on ordinary userinput, with errors naming nothing the caller passed.
Plan
Mask1D.__init__throughgeometry_util.convert_pixel_scales_1d, the samechokepoint
Mask2D.__init__already uses — closing a 1D/2D divergence rather thanduplicating a check.
convert_shape_native_1dto accept any concrete integer scalar (int,np.integer), cast to a Pythonint, withboolexcluded.autoarray/validate.pybesideis_concrete_scalar, so thefunction delegates its type test the way its
convert_pixel_scales_*siblings do.full
test_autoarraysuite because site 1 changes theMask1Dcontract.(1, 1)staying ints) explicitly unfixed — it changesreturn values on paths that work today.
Detailed implementation plan
Affected Repositories
Branch Survey
No worktree claims;
worktree_check_conflictreturned clean.Suggested branch:
feature/mask1d-shape-native-scalar-wideningSite 1 —
Mask1D.__init__never routes through the chokepointautoarray/mask/mask_1d.py:71:Confirmed on
main:Mask2D.__init__is unaffected — it callsgeometry_util.convert_pixel_scales_2datautoarray/mask/mask_2d.py:218.Grid1D.uniformis fine for the same reason. So this isa 1D/2D divergence, not a design choice.
Replace the hand-rolled widening with the call
Mask2Dmakes, keeping its current position(after
inverthandling, before thelen(mask.shape)guard) so ordering is unchanged:Behaviour change to state in the PR, not suppress:
convert_pixel_scales_1drunsvalidate.validate_pixel_scalesfirst, soMask1Dstarts rejecting0, negative andnanpixel scales — exactly as
Mask2Dalready does. No test intest_autoarrayconstructs aMask1Dwith such a value, and the 12 library call sites all pass real scales, but anyfailure this produces must be read rather than adjusted away.
Site 2 —
convert_shape_native_1dkeeps an exact-type checkautoarray/geometry/geometry_util.py:27still hastype(shape_native) is int. Reachablethrough
Array1D.full/zeros/ones, whose sole call site isautoarray/structures/arrays/uniform_1d.py:143and which then doesshape_native[0]:is_concrete_scalaris the wrong predicate here —shape_nativeis a pixel count, soa
floatmust not be silently widened. Use an integer-only test excludingbool, and castto
intso the result matches theTuple[int]annotation. Add it asis_concrete_integerin
autoarray/validate.pybesideis_concrete_scalar(validate.py:48).Update the docstring to match, mirroring the wording
8298d74egaveconvert_pixel_scales_1d.Do not add
validate.validate_shape_nativehere — the function performs no validationtoday and adding it is a separate change.
Explicitly out of scope
Tuple entries are still returned unnormalised:
convert_pixel_scales_2d((1, 1))→(1, 1),contradicting the
Tuple[float, float]annotation. It changes return values on paths thatcurrently work, so it needs its own change and its own suite read.
Tests
Mirror the naming convention
8298d74eestablished(
test__convert_pixel_scales_1d__widens_any_real_scalar).test_autoarray/geometry/test_geometry_util.py—convert_shape_native_1dwidensintand
np.integerto(int,)with a Pythonintentry; aboolis not widened; a(5,)tuple is returned unchanged.
test_autoarray/mask/test_mask_1d.py—Mask1D(mask=…, pixel_scales=1)gives(1.0,), likewisenp.float64(1.0)andnp.int32(1);.geometry.scaled_maximanolonger raises; a
(1.0,)tuple is unchanged;ValueErroron0/-1/nan; a JAXtracer still passes through untouched so
Mask1Dstaysjit-safe.Array1D.full(fill_value=1.0, shape_native=np.int32(5))builds.Every new test must be confirmed to fail without the source change.
Verification
Full suite, not just the touched files. Baseline the known pre-existing pynufft failures in
test_transformer.pyby re-running them on a clean tree and comparing counts;8298d74erecorded 1145 passed / 1 skipped / 3 failed. Read any new failure rather than adjusting
the test. Then a JAX-enabled pass for the tracer case.
Key Files
autoarray/mask/mask_1d.py— site 1, the hand-rolled widening at line 71autoarray/geometry/geometry_util.py— site 2,convert_shape_native_1dat line 27autoarray/validate.py— home ofis_concrete_scalar; addis_concrete_integerautoarray/mask/mask_2d.py:218— the pattern site 1 mirrorsautoarray/structures/arrays/uniform_1d.py:143— sole caller ofconvert_shape_native_1dtest_autoarray/geometry/test_geometry_util.py,test_autoarray/mask/test_mask_1d.pyOriginal Prompt
Click to expand starting prompt
Scalar widening: the two sites the
pixel_scalessweep did not reachType: bug
Target: autoarray
Repos:
Difficulty: small
Autonomy: supervised
Priority: medium
Status: draft
Filed: 2026-08-23
Why this exists
PyAutoArray#464 (
8298d74e, 2026-08-22) fixedconvert_pixel_scales_1dandconvert_pixel_scales_2d:type(x) is floatbecamevalidate.is_concrete_scalar,so
int,np.integerandnp.floatingare all widened and cast tofloat.Re-running that prompt's repro on 2026-08-23 found the sweep did not reach every site.
Two defects of the same class are live on
main. Both are confirmed, not suspected.Completion record:
complete/2026/08/autoarray-pixel-scales-scalar-widening.md.Site 1 —
Mask1D.__init__never routes through the chokepointautoarray/mask/mask_1d.py:71still carries the original exact-type check and does itsown widening rather than calling
convert_pixel_scales_1d:This is #464's exact reported symptom, on a public constructor.
Mask2D.__init__is unaffected — it calls
geometry_util.convert_pixel_scales_2datautoarray/mask/mask_2d.py:218. So this is a 1D/2D divergence, not a design choice,and
Grid1D.uniformis fine because it goes throughgeometry_util.Fix: replace the hand-rolled widening with the call
Mask2Dmakes:Consequence to state, not suppress:
convert_pixel_scales_1drunsvalidate.validate_pixel_scalesfirst, soMask1Dstarts rejecting0, negative andnanpixel scales — exactly asMask2Dalready does. No test intest_autoarrayconstructs a
Mask1Dwith such a value, and the 12 library call sites all pass realscales, but read any failure this produces rather than adjusting the test.
Site 2 —
convert_shape_native_1dkeepstype(x) is intautoarray/geometry/geometry_util.py:27.8298d74elisted this as not-fixed-here.Reachable through
Array1D.full/zeros/ones, whose sole call site isautoarray/structures/arrays/uniform_1d.py:143and which then doesshape_native[0]:Fix: widen to an integer-only test —
is_concrete_scalaris the wrong predicatehere, since
shape_nativeis a pixel count and afloatmust not be silently widened.Use
isinstance(x, (int, np.integer)) and not isinstance(x, bool), cast tointso theresult matches the
Tuple[int]annotation. Prefer adding this asis_concrete_integerin
autoarray/validate.pybesideis_concrete_scalar(validate.py:48), so thefunction delegates its predicate the way its
convert_pixel_scales_*siblings do.Do not add
validate.validate_shape_nativehere — the function performs novalidation today and adding it is a separate change.
Explicitly out of scope
Tuple entries are still returned unnormalised:
convert_pixel_scales_2d((1, 1))→(1, 1),contradicting the
Tuple[float, float]annotation. This changes return values on pathsthat currently work, so it needs its own change and its own suite read. Unfiled.
Verification
Mask1D(mask=…, pixel_scales=1).pixel_scales == (1.0,); same fornp.float64(1.0)and
np.int32(1);.geometry.scaled_maximano longer raises.(1.0,)tuple is returned unchanged; a JAX tracer still passes through untouched,so
Mask1Dstaysjit-safe.Mask1DraisesValueErroronpixel_scalesof0,-1andnan, matchingMask2D.convert_shape_native_1dwidensintandnp.integerto(int,)with a Pythonintentry; a
boolis not widened; a(5,)tuple is unchanged.Array1D.full(fill_value=1.0, shape_native=np.int32(5))builds.test_autoarraysuite, not just the touched files — site 1 changes theMask1Dcontract. Baseline the known pre-existing pynufft failures in
test_transformer.pyagainst a clean tree;
8298d74erecorded 1145 passed / 1 skipped / 3 failed.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-pixel-scales-scalar-widening.md(PyAutoArray#464)complete/2026/08/autoarray-input-validation-guards.md(PyAutoArray#440 / Missing input validation across Array2D, Grid2D, Mask2D, Imaging, regularization #333)planned.md§rhayes-audit-validation-phases-2-4).