Skip to content

Adaptive pixelisations (Delaunay, KNNBarycentric) crash in FitImaging; ConstantSplit broken on RectangularUniform #332

Closed

Description

@rhayes777

Title: Adaptive pixelisations (Delaunay, KNNBarycentric) crash in FitImaging; ConstantSplit regularization broken on RectangularUniform

Repo: Jammy2211/PyAutoArray

Tag: @Jammy2211


While auditing the released 2026.5.21.1 libraries against the documented public
API, I found two related defects in the inversion path. Both are reproducible with
the snippets below in a fresh uv venv (uv pip install autolens, Python 3.12.8).

1. Delaunay and KNNBarycentric raise AttributeError inside FitImaging

import numpy as np
import autolens as al

psf = al.Convolver.from_gaussian(shape_native=(11, 11), sigma=0.1,
                                 pixel_scales=0.05, normalize=True)
grid = al.Grid2D.uniform(shape_native=(50, 50), pixel_scales=0.05, over_sample_size=2)
tracer_true = al.Tracer(galaxies=[
    al.Galaxy(redshift=0.5,
              mass=al.mp.Isothermal(centre=(0, 0), ell_comps=(0.1, 0.0),
                                    einstein_radius=1.2)),
    al.Galaxy(redshift=1.0,
              bulge=al.lp.Sersic(centre=(0.05, 0), intensity=0.3,
                                 effective_radius=0.1, sersic_index=2)),
])
sim = al.SimulatorImaging(exposure_time=300, background_sky_level=0.1, psf=psf,
                          add_poisson_noise_to_data=True, noise_seed=1)
dataset = sim.via_tracer_from(tracer=tracer_true, grid=grid)
mask = al.Mask2D.circular(shape_native=dataset.shape_native,
                          pixel_scales=dataset.pixel_scales, radius=2.0)
dataset = dataset.apply_mask(mask=mask)

lens = al.Galaxy(redshift=0.5,
                 mass=al.mp.Isothermal(centre=(0, 0), ell_comps=(0.1, 0),
                                       einstein_radius=1.2))
pix  = al.Pixelization(
    mesh=al.mesh.Delaunay(pixels=100),                # or KNNBarycentric(pixels=100)
    regularization=al.reg.Constant(coefficient=1.0),
)
source = al.Galaxy(redshift=1.0, pixelization=pix)
tracer = al.Tracer(galaxies=[lens, source])

fit = al.FitImaging(dataset=dataset, tracer=tracer)
fit.log_evidence    # AttributeError
File "autoarray/inversion/mesh/border_relocator.py", line 446, in relocated_mesh_grid_from
    grid.array[self.border_slim], xp=xp
AttributeError: 'NoneType' object has no attribute 'array'

The Optional[BorderRelocator] guard at mesh/abstract.py:90 is in place, but the
grid argument passed into BorderRelocator.relocated_mesh_grid_from() is
None, so the failure happens one level deeper at
border_relocator.py:446 (grid.array[self.border_slim]).

Effect: in the released wheel Delaunay and KNNBarycentric are unusable in
FitImaging. RectangularUniform works fine.

2. ConstantSplit regularization is broken on RectangularUniform

Same simulation as above, but swap the regularization:

pix = al.Pixelization(
    mesh=al.mesh.RectangularUniform(shape=(15, 15)),
    regularization=al.reg.ConstantSplit(coefficient=1.0),
)
AttributeError: 'InterpolatorRectangularUniform' object has no attribute
'_mappings_sizes_weights_split'

Constant works on the same mesh; only the Split variant is broken.

Environment

  • autolens / autogalaxy / autoarray / autofit / autoconf == 2026.5.21.1
  • Python 3.12.8, macOS arm64, fresh uv venv.

What I tried next

  • Read the upstream Jammy2211/PyAutoArray:autoarray/inversion/mesh/mesh/abstract.py
    on main; the if border_relocator is not None guard is identical to the
    installed file. The issue is that callers in autogalaxy / autolens now invoke
    the new mesh API (without the old image_mesh= kwarg) without setting up a
    source_plane_data_grid, leaving it None.

  • The new Delaunay(pixels=N) / KNNBarycentric(pixels=N) constructors no
    longer take an explicit image-plane mesh. If that's intentional, the wiring
    to set up the data grid / border relocator probably needs to move inside the
    mesh classes themselves.

Happy to dig further if useful. Full reproducer scripts live at
work/edge_case_tests/06_inversion.py in the audit workspace.

Activity

  1. Jammy2211 commented on Jul 28, 2026

    @Jammy2211
    Collaborator

    Thanks @rhayes777, and sorry for the delay on this one. I've re-run both parts against current main (autoarray @ 616e8b4c, autolens @ 6567b3b1c). Here's what I found.

    Part 2 is confirmed straight out. ConstantSplit on RectangularUniform raises:

    AttributeError: 'InterpolatorRectangularUniform' object has no attribute '_mappings_sizes_weights_split'
    

    while the same regularization works fine on Delaunay (log_evidence 5096.44 on your simulation). That's a real bug with no workaround, and we'll fix it.

    Part 1 is subtler than it first looked. Delaunay and KNNBarycentric aren't broken in the released wheel — they need an image-plane mesh grid supplied via adapt_images, and with it both fit cleanly on your exact simulation:

    Delaunay(pixels=100)       + Constant + adapt_images  ->  log_evidence 5084.7513
    KNNBarycentric(pixels=100) + Constant + adapt_images  ->  log_evidence 5211.7226
    Delaunay(pixels=100)       + Constant, no adapt       ->  AttributeError: 'NoneType' ...
    

    The idiom is:

    adapt_images = al.AdaptImages(
        galaxy_image_plane_mesh_grid_dict={source: image_plane_mesh_grid}
    )
    fit = al.FitImaging(dataset=dataset, tracer=tracer, adapt_images=adapt_images)

    as used in autolens_workspace/scripts/imaging/features/pixelization/delaunay.py. Your snippet omitted it, which is why the grid arrived at the border relocator as None.

    That said, you found a genuine defect and your diagnosis was close to the mark. A missing required precondition should not surface as AttributeError: 'NoneType' object has no attribute 'array' three frames down in border_relocator.py:446 — that's exactly the head-scratch failure mode you describe in your other reports. And your reading that the wiring belongs inside the mesh classes is the direction we'll take: either the mesh sets the grid up itself, or construction fails immediately with a message that names adapt_images. Reproducing this took me straight to the right line, so the report did its job even though the headline was broader than the underlying cause.

    Both items are being tracked alongside the rest of your audit (#333, PyAutoGalaxy#440, PyAutoLens#531, PyAutoLens#532) as a single campaign — the two here are in the "genuine crash / bad error" bucket rather than the constructor-validation sweep. I'll link the tracking issue here shortly.

  2. Jammy2211 commented on Jul 28, 2026

    @Jammy2211
    Collaborator

    As promised above — the tracking issue for this campaign is
    #415, which carries the verification evidence for all 16
    findings and the phase breakdown.

    The findings from this issue are in phase 1 (#416), which
    is where we're starting — they're the ones with real user impact and no workaround.

    One thing worth flagging back to you: while tracing the ConstantSplit failure I
    found it's wider than reported. InterpolatorRectangularUniform is missing
    _mappings_sizes_weights_split entirely (InterpolatorRectangular and
    InterpolatorDelaunay both have it, and the abstract base doesn't supply it) — and
    three regularizations consume that property, not one:

    constant_split.py:67
    adapt_split.py:104
    adapt_split_zeroth.py:105
    

    So AdaptSplit and AdaptSplitZeroth fail on RectangularUniform for the same
    reason. You found one of three. The fix and its tests cover all of them.

  3. Jammy2211 commented on Jul 28, 2026

    @Jammy2211
    Collaborator

    Correction to my note above, and it changes the outcome for you — worth flagging
    rather than leaving you to discover it from a changelog.

    Rectangular meshes do not support Split regularization. That's the design
    intent, confirmed with @Jammy2211. So the fix here is not to make
    ConstantSplit work on RectangularUniform — it's to raise a clear exception
    saying the combination is unsupported
    , pointing you at a mesh that does support it.

    Digging into it turned up more than the one case you hit. Nine combinations are
    affected, in two different failure modes — three rectangular meshes x three Split
    regularizations:

    Mesh Today
    RectangularUniform AttributeError: ... has no attribute '_mappings_sizes_weights_split' (your case)
    RectangularAdaptDensity IndexError: index 4 is out of bounds for axis 0 with size 4
    RectangularAdaptImage same IndexError (inherits)

    The second mode is the one I'd have missed by reading only your report.
    InterpolatorRectangular does define _mappings_sizes_weights_split, but as a
    pass-through with this comment:

    # Rectangular pixelizations use bilinear interpolation which already factors
    # in the 4-corner neighbourhood, so no separate split-cross calculation is
    # needed — split regularization reuses the same mappings.
    return self._mappings_sizes_weights

    That's aspirational rather than true — it hands 4-corner mappings to
    reg_split_from, which expects the split-cross structure, and crashes one frame
    later. It's going away in favour of the explicit error.

    Delaunay + ConstantSplit works fine (log_evidence 5096.4420), so if you need
    split regularization that's the direction. Tracked in #416.

  4. Jammy2211 commented on Jul 28, 2026

    @Jammy2211
    Collaborator

    Phase 1 is up for review — the crash-level findings from your audit.

    Two things worth reporting back, since both change what you'd expect to see.

    Your ConstantSplit finding was one of nine. InterpolatorRectangular (used by
    RectangularAdaptDensity and RectangularAdaptImage) does define
    _mappings_sizes_weights_split, but as a pass-through commented "split regularization reuses the
    same mappings". It doesn't — reg_split_from expects the split-cross structure, so those two
    meshes failed with IndexError rather than your AttributeError. Three meshes × three split
    schemes, two failure modes. All nine now raise one clear error at Pixelization construction
    naming both sides, and the misleading pass-through is deleted.

    Your outside-the-caustic case yields zero images, not one. You predicted a single image near
    the source position; the tiling finds none, because the grid spans ±2″ and the source sits at
    (5,5). So rather than fabricate an image, solve() now returns an empty grid and logs why.
    Your underlying point stands — a raw AxisError was useless — but the fix is "return nothing,
    clearly" rather than "return the one image".

    Chasing the second one turned up something adjacent that you didn't hit but would have: with zero
    images, FitPositionsImagePairAll computed -log(0) against an infinite sum and produced a NaN
    chi-squared. fitness.py converts NaN likelihoods into resample-rejects, so a search would have
    silently discarded those models instead of scoring them. Its two sibling fit classes already had a
    finite no_image_residual floor for exactly this; PairAll was the one missing it. Fixed in the
    same PR.

    Still to come: the constructor-validation sweep (#333, PyAutoGalaxy#440, #532's B4), the
    adapt_images error-legibility fix, and the z_lens > z_source question I asked you on #532 —
    that one's genuinely open, no rush.

  5. Jammy2211 commented on Aug 9, 2026

    @Jammy2211
    Collaborator

    Fixed and merged in #442 (5dedb5e). Thanks @rhayes777.

    To restate the correction from our earlier reply, since it is what shaped the fix: the meshes are not broken. Delaunay and KNNBarycentric work; they require an image-plane mesh grid, supplied via adapt_images. Re-verified on the branch — with it supplied, Delaunay + Constant, KNNBarycentric + Constant and Delaunay + ConstantSplit all fit, and RectangularUniform + Constant fits with no adapt_images at all.

    So the defect was the error, and that is what changed. Instead of AttributeError: 'NoneType' object has no attribute 'array' from inside border_relocator.py, you now get a MeshException that names adapt_images, shows the AdaptImages idiom, and points out that the rectangular family needs none. The guard sits at Delaunay.interpolator_from, which the whole adaptive family inherits.

    On your suggestion that "the wiring probably needs to move inside the mesh classes themselves" — we went the other way, and it is worth saying why rather than just doing it. Building an image-plane mesh grid requires a weighting policy, and that policy is exactly what adapt_images carries. Having the mesh invent one would make a science choice on the user's behalf, silently, in precisely the case where they have given no indication of what they want. Failing fast keeps that choice explicit. It also matches how the rectangular/split combination was handled in #417. Happy to revisit if you think the intended API really is for Delaunay(pixels=N) to self-wire.

    Small correction to our own notes while we were in there: the None is the mesh_grid argument (border_relocator.py:450), not the grid one at line 446.

    Part 2 of this issue (ConstantSplit on RectangularUniform) shipped back in #417 and is untouched.

    The regression tests assert the clear failure plus controls that each adaptive mesh still builds with its grid supplied — so if these meshes ever genuinely do break, those controls fail and the correction above gets revisited rather than quietly becoming wrong.

    That closes phases 2 and 3 of the audit. Epic #415 stays open for the one remaining item, the z_lens > z_source question on PyAutoLens#532, which is still with you.


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions