Repository navigation
Description
Title: Missing input validation across Array2D, Grid2D, Mask2D, Imaging, regularization
Repo: Jammy2211/PyAutoArray
Tag: @Jammy2211
A handful of inputs that should be rejected at construction are silently
accepted and propagate downstream as confusing NumPy or numba errors. None
of these is performance-critical, but each one converts a clear
ValueError("…") into a head-scratch debugging session for new users.
Found while auditing 2026.5.21.1.
B6. Array2D accepts pixel_scales=0, negative, NaN
import numpy as np, autoarray as aa
aa.Array2D.no_mask(np.ones((4, 4)), pixel_scales=0.0) # accepted
# first .derive_grid call: ZeroDivisionError: float division by zero
aa.Array2D.no_mask(np.ones((4, 4)), pixel_scales=-0.1) # accepted; geometry flipped
aa.Array2D.no_mask(np.ones((4, 4)), pixel_scales=float("nan")) # accepted; NaN propagatesB7. Mask2D.circular_annular(inner > outer) silently returns zero pixels
aa.Mask2D.circular_annular(
shape_native=(20, 20), pixel_scales=0.1,
inner_radius=0.8, outer_radius=0.3,
).pixels_in_mask
# 0Almost always a typo / swapped arguments.
B8. Grid2D.uniform(shape_native=(0, 0)) builds an empty grid
aa.Grid2D.uniform(shape_native=(0, 0), pixel_scales=0.1).shape_slim # 0
aa.Grid2D.uniform(shape_native=(0, 5), pixel_scales=0.1).shape_slim # 0B5 (from autolens). al.Imaging accepts mismatched data / noise_map shapes
import autolens as al
data = al.Array2D.no_mask(np.ones((10, 10)), pixel_scales=0.1)
noise = al.Array2D.no_mask(np.ones(( 5, 5)) * 0.1, pixel_scales=0.1)
psf = al.Convolver.from_gaussian(shape_native=(3, 3), sigma=0.1,
pixel_scales=0.1, normalize=True)
ds = al.Imaging(data=data, noise_map=noise, psf=psf)
ds.shape_native # (10, 10) — mismatch swallowed(Filing here rather than in PyAutoLens since Imaging ultimately lives in
autoarray/dataset/imaging/dataset.py.)
B13. Negative regularization coefficient silently accepted
import autolens as al
al.reg.Constant(coefficient=-1.0) # accepted; produces same log_evidence as +1.0The coefficient appears to be squared internally, so positive and negative
give identical answers — but the value is unphysical and the user gets no
heads-up.
Suggested fix shape
A small _validate_* helper used in each constructor:
if pixel_scales <= 0 or not np.isfinite(pixel_scales):
raise ValueError(f"pixel_scales must be a finite positive number; got {pixel_scales!r}")…and analogous one-liners for the others. Happy to PR if useful.
Environment
autoarray == 2026.5.21.1, Python 3.12.8.
Full repro at work/edge_case_tests/01_core_data_structures.py and
work/edge_case_tests/09_more_edges.py in the audit workspace.
Activity
Jammy2211 commentedon Jul 28, 2026
Thanks for this @rhayes777, and apologies for the long silence — this sat far longer than it should have.
I've re-run every snippet against current
main(autoarray@616e8b4c) and all five still reproduce exactly as you describe:Array2D.no_mask(..., pixel_scales=0.0),-0.1andnanare all accepted; the first then givesZeroDivisionError: float division by zeroon the firstderive_gridcall.Mask2D.circular_annular(inner_radius=0.8, outer_radius=0.3).pixels_in_maskreturns0.Grid2D.uniform(shape_native=(0, 0))and(0, 5)both giveshape_slim == 0.Imagingwith 10x10dataand 5x5noise_mapconstructs, and reportsshape_native == (10, 10)— the mismatch is swallowed.reg.Constant(coefficient=-1.0)is accepted and stored as-1.0.One addition to your B13 analysis: you're right that
constant.py:43doescoefficient * coefficient, which is whylog_evidenceis unchanged between+1.0and-1.0. Butregularization_weights_from(constant.py:127) returns the raw, un-squaredself.coefficient, so a negative value does leak negative regularization weights into anything reading that method. That makes the case for rejecting it at construction stronger than "merely unphysical" — thanks for digging into the mechanism rather than just reporting the symptom.Your suggested fix shape is the one we'll follow — a shared
_validate_*helper with a consistentValueErrormessage convention across the constructors.On the offer to PR: thank you, genuinely, but we'll take these on in-house. The constructors sit on paths that get JAX-traced, and we want to be careful the guards don't cost anything under tracing or break on tracer values — that's fiddly in a way that isn't fair to hand to a contributor.
This is being tracked alongside the other four issues from your audit (#332, PyAutoGalaxy#440, PyAutoLens#531, PyAutoLens#532) as a single validation campaign. I'll link the tracking issue here once it's open, and keep this thread updated as the fixes land.
Jammy2211 commentedon Jul 28, 2026
As promised above — the tracking issue for this campaign is
#415, which carries the verification evidence for all 16
findings and the phase breakdown. Watch that one for overall progress; I'll keep
updating this thread as the parts touching it land.
Phase 1 (the three crash-level findings, which is where we're starting) is
#416.
Jammy2211 commentedon Aug 9, 2026
Fixed and merged in #440 (
f2f7a4f). Thanks again for the audit @rhayes777 — all five findings here are closed.Each now raises at construction with a message naming the parameter. The guards went in at chokepoints rather than at the five reported call sites, so a few of them cover more than was reported:
geometry_util.convert_pixel_scales_{1d,2d}, which everyMask2Dfactory andGrid2D.uniformfunnel through.circular_annularandelliptical_annular; the elliptical sibling had the identical hole.Mask2D.__init__, which every factory returns through.AbstractDataset, soInterferometerand every other dataset subclass are covered, not justImaging.Constant. Thirteen siblings had the same hole.One correction to the B13 write-up, in your favour: you noted the identical
log_evidencefor±1.0and reasonably read the value as inert. It isn't.regularization_matrix_fromsquares the coefficient, which hides the sign — butregularization_weights_fromreturns it unsquared, so a negative coefficient was leaking negative regularization weights into every consumer of that method:So that one was a live bug, not a cosmetic one.
On the JAX point behind our earlier "we'll take this in-house" reply: the guards are gated on a concreteness check and pass tracers through untouched, so they cost nothing inside a trace. Verified against JAX 0.11.0 — construction under
jax.jitworks andjax.gradflows through a traced coefficient, while a concrete negative is still rejected.Zero is still accepted for coefficients, as a degenerate but meaningful "no regularization" request.
Epic #415 stays open for the rest: #332 (
adapt_imagesprecondition legibility), PyAutoGalaxy#440 and PyAutoLens#532 — the last two now unblocked, since they import the sharedautoarray/validate.pyhelper this PR established. Your question on #532 about whetherz_lens > z_sourceshould warn at all is still open and still wanted.Generated by Claude Code