Skip to content

fix: widen int / numpy scalar pixel_scales to a tuple - #465

Merged
Jammy2211 merged 1 commit into
mainfrom
claude/autoarray-pixel-scales-int-tuple-wfxlnj
Aug 22, 2026
Merged

fix: widen int / numpy scalar pixel_scales to a tuple#465
Jammy2211 merged 1 commit into
mainfrom
claude/autoarray-pixel-scales-int-tuple-wfxlnj

Conversation

@Jammy2211

Copy link
Copy Markdown
Collaborator

Summary

convert_pixel_scales_1d and convert_pixel_scales_2d widened a scalar pixel_scales to the
tuple form using type(pixel_scales) is float — an exact-type check. Only a literal Python
float was widened; an int, an np.floating or an np.integer fell through unconverted.

These 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. On current main,
Array2D.no_mask(values=np.ones((5, 5)), pixel_scales=1) constructs successfully and stores the
bare 1
on the mask; the TypeError: 'int' object is not subscriptable then surfaces on first
use (.pixel_scale, .derive_grid, anything touching geometry), naming nothing the caller passed.
Grid2D.uniform and Mask2D.circular raise it outright. An int pixel scale is a natural thing
to type by hand, and an np.floating is what indexing a numpy array or reading a FITS header
returns (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 casts
the widened value to a Python float, so the returned tuple matches the Tuple[float, ...]
annotation regardless of input type. That cast 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 — 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_scales now accepts an int or a NumPy real scalar where previously
only an exact float worked, and the value stored is always a Python float tuple. The
ty.PixelScales alias was widened to state what is actually accepted. Purely additive in
practice — no input that worked before behaves differently, and the full suite confirms nothing
downstream relied on a non-float scalar passing through unconverted.
See full details below.

Test Plan

  • python -m pytest test_autoarray/ — 1145 passed / 1 skipped, plus the 3 pre-existing
    test_transformer.py pynufft 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) and
    aa.Mask2D.circular(shape_native=(5, 5), radius=2.0, pixel_scales=1) build.
  • A tuple input is returned unchanged and a tracer still passes through untouched.
  • pixel_scales=0 / -1 / nan are still rejected, in scalar, integer, NumPy and
    tuple-entry forms.

Validation. 27 new tests, each confirmed to fail without the source change (stashing only
geometry_util.py + type.py produces 12 failures across the new cases; 0 with the fix). The 3
failing tests in test_autoarray/operators/test_transformer.py are pre-existing and unrelated
— a pynufft/scipy incompatibility (pynufft/src/_helper/helper.py:1055: AttributeError), tracked
separately. 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:

  • Tuple entries are not normalisedpixel_scales=(1, 1) still returns ints. The tuple path
    is required to return its input unchanged.
  • convert_shape_native_1d carries the identical defecttype(shape_native) is int at
    geometry_util.py:27, so np.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 real
    scalar (int, float, np.integer, np.floating) to (float,), not only an exact float.
    The widened entry is cast to a Python float. bool, JAX tracers, and tuples are returned
    unchanged, as before.
  • autoarray.util.geometry.convert_pixel_scales_2d(pixel_scales) — same, to (float, float).
  • Every public constructor funnelling through the two above now accepts an int or NumPy real
    scalar pixel_scales, and stores it as a Python float tuple. Affects the Mask2D factories,
    Grid2D.uniform, Grid1D, Array2D, Array1D and VectorYX2D.
  • autoarray.type.PixelScales — alias widened from
    Union[Tuple[float], Tuple[float, float], float] to additionally include int, np.floating
    and 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-tuple pixel_scales) now work:

  • Before: aa.Array2D.no_mask(values=..., pixel_scales=1) → stored 1; later use raised
    TypeError: 'int' object is not subscriptable.
  • After: aa.Array2D.no_mask(values=..., pixel_scales=1)pixel_scales == (1.0, 1.0).

Generated by the PyAutoLabs agent workflow.


Generated by Claude Code

`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 Jammy2211 added the pending-release PR queued for the next release build label Aug 22, 2026 — with Claude
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
@Jammy2211
Jammy2211 merged commit a6b07cd into main Aug 22, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

pending-release PR queued for the next release build

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fix: widen int / numpy scalar pixel_scales to a tuple

2 participants