Skip to content

PointSolver.solve crashes on single-image sources and on loose pixel_scale_precision #531

Closed

Description

@rhayes777

Title: PointSolver.solve crashes on single-image sources and on loose pixel_scale_precision

Repo: Jammy2211/PyAutoLens

Tag: @Jammy2211


Two crashes in PointSolver.solve that look like missing edge-case handling.
Both reproduce in a fresh venv with autolens == 2026.5.21.1, Python 3.12.

1. Source outside the caustic → single image → numpy.AxisError

import autolens as al

grid = al.Grid2D.uniform(shape_native=(80, 80), pixel_scales=0.05)
lens = al.Galaxy(
    redshift=0.5,
    mass=al.mp.Isothermal(centre=(0, 0), ell_comps=(0.1, 0), einstein_radius=1.0),
)
src_pos = (5.0, 5.0)                     # comfortably outside the caustic
src = al.Galaxy(redshift=1.0, point_0=al.ps.Point(centre=src_pos))
tracer = al.Tracer(galaxies=[lens, src])

solver = al.PointSolver.for_grid(grid=grid, pixel_scale_precision=0.001)
solver.solve(tracer=tracer, source_plane_coordinate=src_pos)
# numpy.exceptions.AxisError: axis 1 is out of bounds for array of dimension 1

A source far from the lens should produce a single image (approximately at the
source position). Instead the solver bails out with a NumPy axis error from
deep inside the iterative refinement.

2. Loose pixel_scale_precision → IndexError: list index out of range

src_pos = (0.05, 0.02)                   # well inside the caustic
src = al.Galaxy(redshift=1.0, point_0=al.ps.Point(centre=src_pos))
tracer = al.Tracer(galaxies=[lens, src])
solver = al.PointSolver.for_grid(grid=grid, pixel_scale_precision=0.1)
solver.solve(tracer=tracer, source_plane_coordinate=src_pos)
# IndexError: list index out of range

Same input as a working pixel_scale_precision=0.001 call (which returns
four images), only the precision was loosened. The solver doesn't gracefully
degrade — it indexes an empty candidate list.

Why it matters

Both inputs are realistic: surveys with many simulated lenses commonly have
configurations outside the caustic (especially during model exploration), and
"loose precision for a quick first pass" is exactly how one would script the
solver. A clear error ("source outside the caustic; only one image", or
"pixel_scale_precision is too large to resolve any image") would be
massively better than a raw AxisError / IndexError.

Environment

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

Full reproducer at work/edge_case_tests/08_point_solver.py in the audit
workspace.

Activity

  1. Jammy2211 commented on Jul 28, 2026

    @Jammy2211
    Collaborator

    Thanks @rhayes777, and sorry this went unanswered for so long.

    Both crashes reproduce on current main (autolens @ 6567b3b1c), unchanged:

    • Source outside the caustic — src_pos = (5.0, 5.0), pixel_scale_precision=0.001: numpy.exceptions.AxisError: axis 1 is out of bounds for array of dimension 1.
    • Loose precision — src_pos = (0.05, 0.02) inside the caustic: pixel_scale_precision=0.001 returns 4 images as expected, and simply loosening it to 0.1 gives IndexError: list index out of range. Same tracer, same source, only the precision changed.

    Your point about why it matters is the part that lands hardest for us. Both of these are things a user does on purpose — configurations outside the caustic show up constantly during model exploration, and "loosen the precision for a quick first pass" is exactly how someone would script the solver. Getting a raw AxisError from inside the iterative refinement gives no hint that the input was the problem, let alone which input.

    So we're treating these as genuine bugs rather than validation niceties: the single-image case should return the one image it finds, and the too-loose-precision case should raise a clear error naming pixel_scale_precision and saying it can't resolve any image at that scale. Degrading gracefully where we can, failing legibly where we can't.

    This is being tracked with the rest of your audit (PyAutoArray#332, PyAutoArray#333, PyAutoGalaxy#440, PyAutoLens#532) as a single campaign — these two sit in the crash-bug bucket alongside the ConstantSplit failure in #332, which is where we'll start. I'll link the tracking issue here shortly and keep you posted as it lands.

  2. Jammy2211 commented on Jul 28, 2026

    @Jammy2211
    Collaborator

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

    The findings from this issue are in phase 1 (PyAutoLabs/PyAutoArray#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

    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.

  4. Jammy2211 commented on Jul 28, 2026

    @Jammy2211
    Collaborator

    Both crashes from this report are fixed and merged in #662
    (with PyAutoLabs/PyAutoArray#417).

    • Source outside the tiled region — no longer raises AxisError. Returns an empty grid and
      logs why. (As noted above, the tiling genuinely finds zero images for your (5,5) case rather
      than the single image you predicted, so returning nothing clearly is the honest fix.)
    • Loose pixel_scale_precision — no longer raises IndexError. Raises a clear ValueError
      naming pixel_scale_precision, reporting the triangle scale, and suggesting a workable value.
      The root cause was an off-by-one: n_steps goes negative when precision exceeds the triangle
      scale, and the existing guard only tested == 0.

    Chasing the first one also turned up a defect you didn't hit: with zero images
    FitPositionsImagePairAll produced a NaN chi-squared, which the sampler machinery converts
    into a silent resample-reject. Its two sibling fit classes already had a finite no_image_residual
    floor for exactly this case; it now matches them.

    Closing this one. Your other three issues (PyAutoArray#332, PyAutoArray#333, PyAutoGalaxy#440,
    PyAutoLens#532) remain open and tracked in PyAutoArray#415 — the constructor-validation sweep and
    the adapt_images error-legibility fix are next. Thanks again for the report, and sorry once more
    that it sat as long as it did.

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