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

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
@Jammy2211
Jammy2211 deleted the claude/autoarray-pixel-scales-int-tuple-wfxlnj branch August 25, 2026 18:14
@Jammy2211 Jammy2211 removed the pending-release PR queued for the next release build label Sep 4, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

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