Skip to content

fix: Basis on irregular grids + settle pixelization eager vs step-by-step log-evidence gap #580

Description

@Jammy2211

Overview

jax_profiling/jit/imaging/pixelization.py prints three "equivalent"
log-evidence values and asserts only two of them. Reproducing the original
report (PyAutoMind bug/autolens/pixelization_eager_vs_jit_divergence.md,
filed 2026-07-08) on clean main today found the surface evidence stale, the
underlying divergence papered over rather than resolved, and the script
unable to run at all against current libraries.

This issue unblocks the script (one genuine PyAutoGalaxy library bug plus the
RectangularAdaptDensityRectangularRTUAdaptDensity rename from #461),
then settles why the eager and step-by-step numbers differ and makes the
script's account of it true.

Plan

  • Fix Basis.image_2d_list_from in PyAutoGalaxy so a basis containing linear
    light profiles can be evaluated on a Grid2DIrregular — it currently assumes
    every grid carries a mask.
  • Probe the live fit to confirm the suspected cause of the divergence: the
    eager inversion solves for a non-negative reconstruction (fnnls) while
    the script's step-by-step rebuild does a plain positive/negative solve.
  • Point this one script at the renamed RectangularRTUAdaptDensity mesh
    (the wider rename fallout belongs to PyAutoArray#461's follow-up campaign).
  • Replace the script's incorrect "floating-point drift" comment with the
    confirmed cause, and turn the printed step-by-step value into a real
    regression assertion.
  • Re-pin EXPECTED_LOG_EVIDENCE_HST, which has drifted ~1e-2 relative.
Detailed implementation plan

What reproducing it found

1. The prompt's surface evidence is stale. The script moved to
autolens_workspace_developer/jax_profiling/jit/imaging/pixelization.py and was
rewritten by PR #60 ("Re-profile rectangular + Delaunay at full production
fiducial"). EXPECTED_LOG_EVIDENCE_HST_EAGER and the FIXME no longer exist;
there is one constant (EXPECTED_LOG_EVIDENCE_HST = 24746.105672366088)
asserted against eager, full-JIT and vmap. The -1.3e9 values in the prompt
came from a superseded, pathological fiducial.

2. The divergence was papered over, not resolved. The correctness assertion
(pixelization.py:806-830) compares log_evidence_check — rebuilt from
inversion.reconstruction and inversion.curvature_matrix — against the
reference. The genuinely independent step-by-step value is printed at line 804
and never asserted. The comment at lines 761-764 blames "cumulative
floating-point differences between JIT-compiled and eager paths (especially
through ill-conditioned solves)".

That explanation is almost certainly wrong.
PyAutoArray/autoarray/config/general.yaml:5 ships
use_positive_only_solver: true, so Inversion.reconstruction
(PyAutoArray/autoarray/inversion/inversion/abstract.py:493-545) is an fnnls
non-negative
solve — optionally on an edge-zeroed subset via
use_edge_zeroed_pixels / zeroed_ids_to_keep. The script's
compute_reconstruction is a plain positive/negative solve(F + H, D). Those
are two different reconstructions s, so χ² and sᵀHs legitimately differ —
a real modelling difference, not round-off.

Prompt suspects 1, 2 and 4 are falsified: FitDataset.figure_of_merit
log_evidence (PyAutoArray/autoarray/fit/fit_dataset.py:324-366) uses exactly
the same five-term formula the script implements.

3. The script does not run on main at all.

  • al.mesh.RectangularAdaptDensity (line 233) was renamed by PyAutoArray
    f9aceea3 (perf: vectorize MGE potential over components #461) → RectangularRTUAdaptDensity (pure rename, values
    unchanged) with a new RectangularBilinearAdaptDensity sibling. "Breaking
    rename — no back-compat aliases; downstream workspaces update in follow-up
    PRs." The PyAuto API gate blocks the script on this.
  • With that name shimmed, the run dies at Step 2:
    AttributeError: Grid2DIrregular does not have attribute mask at
    PyAutoGalaxy/autogalaxy/profiles/basis.py:164. Basis.image_2d_list_from
    builds its zero placeholder for LightProfileLinear members as
    aa.Array2D(values=xp.zeros(...), mask=grid.mask), assuming a masked
    Grid2D. The script deliberately wraps grids as Grid2DIrregular for
    JIT-traceability, and the lens light is an MGE-60 linear basis.

Also observed with the value-preserving RTU shim: eager
figure_of_merit = 25004.719 against the pinned 24746.106 — 1.05e-2
relative, so the pinned constant has drifted too.

Affected Repositories

  • PyAutoGalaxy (primary, library)
  • autolens_workspace_developer (script follow-up)
  • PyAutoArray — only if the Step 2 probe redirects the investigation

Branch Survey

Repository Current Branch Dirty?
./PyAutoArray main clean
./PyAutoGalaxy main clean
./PyAutoLens main clean
./autolens_workspace_developer main clean

worktree_check_conflict → no conflict.

Suggested branch: feature/pixelization-eager-jit-divergence

Implementation Steps

  1. PyAutoGalaxy — irregular-grid-safe Basis placeholder.
    autogalaxy/profiles/basis.py, image_2d_list_from (~lines 160-165). The
    binned is False branch already returns a bare xp.zeros(...); the default
    branch must stop assuming grid.mask. Return an Array2D only when the grid
    actually carries a mask, otherwise the bare zero array. Check the sibling
    accessors in the same file (convergence_2d_from, any other grid.mask use)
    for the same assumption before settling the shape of the fix.
    Unit test in test_autogalaxy/profiles/test_basis.py: a Basis containing a
    LightProfileLinear evaluated on a Grid2DIrregular returns zeros of the
    right length instead of raising. No JAX in unit tests.

  2. Confirm the solver hypothesis before touching the script. With step 1 in
    place, probe the live fit once: inversion.settings.use_positive_only_solver,
    inversion.settings.use_edge_zeroed_pixels, and
    max|inversion.reconstruction − solve(F + H, D)|. If the reconstructions
    differ materially, the verdict is "genuine difference". If they agree to
    round-off, the hypothesis is dead and the investigation reopens on
    log_det_curvature_reg_matrix_term / curvature_reg_matrix construction
    (prompt suspect 2) instead. Do not skip this — the whole write-up depends
    on it.

  3. autolens_workspace_developer — make the script honest. In
    jax_profiling/jit/imaging/pixelization.py:

    • Line 233 and the line-1079 comment: RectangularAdaptDensity
      RectangularRTUAdaptDensity (this file only).
    • Replace the false "cumulative floating-point differences" comment
      (761-764) with the confirmed cause from step 2.
    • Assert the step-by-step value: build the step-by-step reconstruction
      through the same solver the inversion uses — reuse
      autoarray.inversion.inversion.inversion_util.reconstruction_positive_only_from
      rather than hand-rolling — so log_evidence (step-by-step) becomes a real
      regression check instead of a printed number. If matching the solver is not
      practical inside the JIT-timing path, keep the direct solve for timing and
      assert the solver-matched rebuild separately, with the reason stated in the
      comment.
    • Re-pin EXPECTED_LOG_EVIDENCE_HST to the value the green run produces,
      noting the fiducial and library version, and confirm eager / full-JIT /
      vmap / step-by-step all sit inside rtol=1e-4.

Out of scope (flag, don't do)

  • The ~40 other files still naming RectangularAdaptDensity /
    RectangularAdaptImage (autogalaxy_workspace, autogalaxy_workspace_test,
    HowToLens, euclid pipeline, prior config YAMLs) — that is PyAutoArray#461's
    announced follow-up campaign. File a prompt for it.
  • jax_profiling/jit/imaging/delaunay.py and the interferometer siblings —
    touch only if the step-1 library fix changes their numbers.

Key Files

  • PyAutoGalaxy/autogalaxy/profiles/basis.pyimage_2d_list_from, the
    grid.mask assumption at line 164.
  • PyAutoGalaxy/test_autogalaxy/profiles/test_basis.py — new regression test.
  • autolens_workspace_developer/jax_profiling/jit/imaging/pixelization.py
    mesh name (233), the false FP-drift comment (761-764), the step-by-step
    print (804), the assertion block (806-830), the pinned constant (1082).
  • PyAutoArray/autoarray/inversion/inversion/abstract.pyreconstruction,
    the positive-only / edge-zeroed branch (493-545).
  • PyAutoArray/autoarray/fit/fit_dataset.pylog_evidence /
    figure_of_merit (324-366), confirms the five-term formula.
  • PyAutoArray/autoarray/config/general.yamluse_positive_only_solver: true.

Verification

source ~/Code/PyAutoLabs-wt/pixelization-eager-jit-divergence/activate.sh
cd PyAutoGalaxy && pytest test_autogalaxy/profiles/test_basis.py -q
cd ../autolens_workspace_developer
NUMBA_CACHE_DIR=/tmp/numba_cache MPLCONFIGDIR=/tmp/matplotlib \
  python jax_profiling/jit/imaging/pixelization.py

Green means: the script runs to completion; figure_of_merit,
log_evidence (inv matrices), log_evidence (step-by-step), full-JIT and vmap
all agree with the re-pinned EXPECTED_LOG_EVIDENCE_HST at rtol=1e-4 (or the
step-by-step gap is asserted against its own solver-matched constant with the
reason recorded in the comment). Then run the Delaunay and interferometer
pixelization siblings to confirm the PyAutoGalaxy fix moved nothing there.

Original Prompt

Click to expand starting prompt

Investigate eager FitImaging.figure_of_merit vs JIT/step-by-step divergence in rectangular pixelization

Type: bug
Target: PyAutoLens
Difficulty: too-large
Autonomy: supervised
Priority: high
Status: formalised

Context

Surfaced by the eager-numpy regression assertions added in
jax_profiling/imaging/pixelization.py (PR from
PyAutoPrompt/issued/eager_numpy_regression_assertions.md).

When the rectangular pixelization script runs, it computes the same
log_evidence via three supposedly-equivalent numpy/JAX paths and gets two
different answers:

figure_of_merit (log_evidence) = -1338814172.1831784   ← eager FitImaging
log_evidence (step-by-step)    = -1338521802.3596904   ← JIT-equivalent numpy rebuild
log_evidence (inv matrices)    = -1338814172.1831782   ← numpy rebuild from eager matrices
log_evidence (reference)       = -1338814172.1831784   ← same as figure_of_merit
full log_likelihood (JIT)      = -1338521802.3596945   ← JIT full pipeline

The split is:

  • Cluster A (eager): FitImaging.figure_of_merit, log_evidence (reference),
    and log_evidence (inv matrices) all agree on -1338814172.18.
  • Cluster B (JIT / step-by-step): log_evidence (step-by-step) and
    full log_likelihood (JIT full pipeline) agree on -1338521802.36.

The two clusters differ by ~292k in absolute value (~0.02% relative) — well
above float64 round-off and well above the rtol=1e-4 used in the regression
assertions. The script's own internal assertion

Assertion PASSED: inversion-matrix log_evidence matches FitImaging.log_evidence

confirms the eager and "inv matrices" numpy rebuilds agree with each other.
The JIT-style rebuild (step-by-step) disagrees with that pair and instead
matches the JIT full-pipeline result.

So something is happening in FitImaging-eager that is not the same as
re-running the same math in a JIT-style numpy pipeline. Currently the
pixelization script pins two separate regression constants — the original
EXPECTED_LOG_EVIDENCE_HST for the JIT path, and a new
EXPECTED_LOG_EVIDENCE_HST_EAGER for the eager path (with a FIXME pointing
at this prompt).

Task

Identify which cluster is correct (or whether they're both reasonable results
of a numerically-legitimate-but-subtle difference in accumulation order /
regularization), then either:

  1. Fix the divergent path so both clusters converge, and collapse the two
    constants back into one.
  2. Document the divergence as genuine (e.g. one path includes a term the
    other legitimately omits), update code comments accordingly, and keep the
    two constants but remove the FIXME framing.

Either outcome is fine — the goal is to understand why the two numpy
computations of the same quantity disagree and to make the code's treatment
of them honest.

Where to look

The disagreement is specifically in the rectangular pixelization log-evidence
stack. Likely suspects (in rough order of plausibility):

  1. FitImaging.figure_of_merit for Inversion-based fits — does it use a
    different formulation (Bayesian evidence with full regularization term)
    than the step-by-step reconstruction in the script does?

    • PyAutoLens/autolens/imaging/fit_imaging.py
    • PyAutoArray/autoarray/inversion/inversion/abstract.py::log_evidence
      and related helper properties.
  2. Regularization matrix handling — the eager path may be building the
    full regularization contribution via log_det_regularization_matrix_term
    whereas the step-by-step may be using a shortcut that agrees with what
    the JIT path does.

  3. Mapping matrix numerical accumulation order — check whether
    mapping_matrix and blurred_mapping_matrix are computed with different
    intermediate dtypes or summation orders in the eager vs step-by-step
    pipelines.

  4. inversion.log_evidence vs fit.figure_of_merit — check whether
    FitImaging.figure_of_merit returns
    inversion.log_evidence_with_regularization or just
    inversion.log_likelihood. The script's "step-by-step" may be comparing
    against a different decomposition.

Not suspected:

  • The simulator is deterministic (seeded), and imaging/delaunay.py (using
    a different mesh but same overall fit pattern) does NOT show this
    divergence — its eager figure_of_merit matches its EXPECTED_LOG_EVIDENCE_HST
    bit-for-bit. So the bug is specifically in the rectangular-pixelization
    code path, not in the general FitImaging + Inversion framework.

  • The interferometer pixelization script (jax_profiling/interferometer/pixelization.py)
    also passes the eager assertion cleanly — figure_of_merit_ref ≈ EXPECTED_LOG_EVIDENCE_SMA. So it's not a universal pixelization issue,
    it's imaging + rectangular specifically.

Verification

After fixing:

source ~/Code/PyAutoLabs-wt/<task-name>/activate.sh
cd autolens_workspace_developer
python jax_profiling/imaging/pixelization.py

Expect:

  • figure_of_merit (log_evidence) == log_evidence (step-by-step) to within
    float64 round-off (or a documented, justified gap).
  • EXPECTED_LOG_EVIDENCE_HST_EAGER can be removed and the eager assertion
    can revert to using EXPECTED_LOG_EVIDENCE_HST alongside the JIT/vmap
    assertions.
  • Delete the FIXME comment block.

Affected repos

  • PyAutoLens and/or PyAutoArray (library — most likely PyAutoArray
    inversion code, possibly PyAutoLens/fit_imaging.py)
  • autolens_workspace_developer (script — remove the FIXME and the second
    constant once the library fix lands)

Suggested branch

feature/pixelization-eager-jit-divergence

Notes

  • Don't start by bumping either constant. The script's internal
    "inv matrices vs step-by-step" split is the debugging starting point —
    narrow down which numpy computation is authoritative, then fix the other.
  • The divergence is ~0.02%, which is well within the regime where Bayesian
    model comparison decisions could flip, so treat this as a real bug until
    proven otherwise (not just a rounding artefact).

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