Repository navigation
Description
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__throughgeometry_util.convert_pixel_scales_1d, the same
chokepointMask2D.__init__already uses — closing a 1D/2D divergence rather than
duplicating a check. - Widen
convert_shape_native_1dto accept any concrete integer scalar (int,
np.integer), cast to a Pythonint, withboolexcluded. - Add the integer predicate to
autoarray/validate.pybesideis_concrete_scalar, so the
function delegates its type test the way itsconvert_pixel_scales_*siblings do. - Test both sites, each new test confirmed to fail without the source change, and run the
fulltest_autoarraysuite because site 1 changes theMask1Dcontract. - 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_1dwidensint
andnp.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_maximano
longer raises; a(1.0,)tuple is unchanged;ValueErroron0/-1/nan; a JAX
tracer still passes through untouched soMask1Dstaysjit-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 -qFull 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 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.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 fornp.float64(1.0)
andnp.int32(1);.geometry.scaled_maximano longer raises.- A
(1.0,)tuple is returned unchanged; a JAX tracer still passes through untouched,
soMask1Dstaysjit-safe. Mask1DraisesValueErroronpixel_scalesof0,-1andnan, matchingMask2D.convert_shape_native_1dwidensintandnp.integerto(int,)with a Pythonint
entry; aboolis 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_autoarraysuite, not just the touched files — site 1 changes theMask1D
contract. Baseline the known pre-existing pynufft failures intest_transformer.py
against 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
- Follow-up of:
complete/2026/08/autoarray-pixel-scales-scalar-widening.md(PyAutoArray#464) - Grandparent:
complete/2026/08/autoarray-input-validation-guards.md(PyAutoArray#440 / Missing input validation across Array2D, Grid2D, Mask2D, Imaging, regularization #333) - Not part of the @rhayes777 audit campaign (
planned.md§rhayes-audit-validation-phases-2-4).
Activity
Jammy2211 commentedon Aug 23, 2026
Library PR Created
PR: #485 (
pending-release)Branch:
feature/mask1d-shape-native-scalar-widening@c0c00b0eBoth sites fixed as planned.
Mask1D.__init__now routes throughconvert_pixel_scales_1d;convert_shape_native_1duses a newvalidate.is_concrete_integerand casts to a Pythonint.Verification
test_autoarray(full)test_autocti(full)jitwith tracedpixel_scalesThe 3 pynufft failures
8298d74ebaselined no longer occur, so there was nothing tobaseline against. The one
test_autoctifailure(
test_serial_eper.py::test__region_list_from__array_2d_list_from, 2D serial EPERextraction) was confirmed pre-existing by running it against the canonical PyAutoArray
maincheckout — it fails identically there. My first attempt to prove that used agit stashwhich was a silent no-op on an already-committed tree, so the run wasactually with the change; the result above is the corrected run.
Test-quality fix folded in
#464's own widening tests asserted value only, and
np.float64(1.0) == (1.0,)NumPy-broadcasts to
array([True])— truthy. Four of six parametrisations passed on anunwidened NumPy scalar and tested nothing. They now assert tuple-ness before the value,
confirmed by reverting to the pre-#464 source and watching them fail.
Workspace impact: none (option iii)
PyAutoGalaxy and PyAutoLens only re-export
Mask1Dand construct none.autocti_workspaceuses
ac.Mask1D, which subclassesaa.Mask1Dand inherits the fix — it now widens scalarstoo, where before it stored them bare. No migration needed. No
/start_workspacefollow-up.Not merged — Heart RED
pyauto-heart readinessis red on one reason, verbatim:It reports
failures: [],262 passed / 0 failed / 46 skipped, and its only recorded stage(
integrate) passed — an incomplete report timestamped before this task started, againstan older PyAutoArray sha. Unrelated to this change, and this PR does not repair it, so the
AUTONOMY.mdcorrective-PR exception does not apply. The PR was opened under an explicithuman override, and merge remains blocked pending a clean readiness verdict.
(The other RED reason,
PyAutoFit: 2 commit(s) behind origin, was cleared by afast-forward pull; score 40 → 60.)
Still open
Tuple entries are returned unnormalised —
convert_pixel_scales_2d((1, 1))→(1, 1),contradicting the
Tuple[float, float]annotation. Deliberately unfixed and unfiled: italters return values on paths that work today.
Jammy2211 commentedon Aug 23, 2026
Shipped
PR: #485 — MERGED 2026-08-23
Merge commit:
9e475052· Feature commit:c0c00b0eBoth sites the #464 sweep missed are closed on
main:Mask1D.__init__routes throughconvert_pixel_scales_1dinstead of hand-rollingtype(x) is float, closing the 1D/2D divergence withMask2D. It also inheritsvalidate.validate_pixel_scales, soMask1Dnow rejects0, negative andnanpixelscales — a deliberate contract change, matching
Mask2D.convert_shape_native_1dtests the newvalidate.is_concrete_integerand casts to aPython
int. Integer-only on purpose:is_concrete_scalarwould silently widen afloat, andshape_nativecounts pixels rather than measuring them.Verified before merge
test_autoarray(full)test_autocti(full)mainjit, tracedpixel_scalesc0c00b0eAlso tightened #464's own widening tests, which were passing vacuously:
np.float64(1.0) == (1.0,)NumPy-broadcasts to a truthyarray([True]), so four of sixparametrisations tested nothing. They now assert tuple-ness before value, confirmed by
reverting to the pre-#464 source and watching them fail.
Merged under an explicit human override
pyauto-heart readinesswas red at merge time on one reason, verbatim:That report carries
failures: [],262 passed / 0 failed / 46 skipped, and its onlyrecorded stage (
integrate) passed — an incomplete artifact timestamped before thistask began, against an older PyAutoArray sha. It is unrelated to this change and this PR
did not repair it, so the
AUTONOMY.mdcorrective-PR exception did not apply. The mergewas a human override, recorded here rather than left implicit.
The other RED reason,
PyAutoFit: 2 commit(s) behind origin, was cleared by afast-forward pull (score 40 → 60).
Follow-up left open, deliberately unfiled
Tuple entries are still returned unnormalised —
convert_pixel_scales_2d((1, 1))→(1, 1),contradicting the
Tuple[float, float]annotation. It alters return values on paths thatwork today, so it needs its own change and its own suite read.