Skip to content

test: make the MGE bitwise sigma-ladder guards portable across CPUs #550

Description

@Jammy2211

Overview

test__mge_model_from__default_sigma_list_is_bitwise_unchanged and
test__mge_point_model_from__default_sigma_list_is_bitwise_unchanged
(test_autogalaxy/analysis/test_model_util.py, added with #549) assert exact
equality between two different numpy code paths. The implementation builds each
sigma with a per-element scalar power (gaussian.sigma = 10 ** log10_sigma_list[i],
autogalaxy/analysis/model_util.py:190 and :271), while the tests build their
expectation with a vectorised 10 ** np.linspace(...). numpy does not guarantee the
scalar and SIMD power loops agree bit for bit, so on AVX-512 hardware they differ by
1 ULP and the tests fail; on GitHub's runners they agree and the tests pass.

The effect is cosmetic, not functional — the PyAutoFit identifier quantises floats at
RESOLUTION = 1e-8 and this drift is ~1e-16 relative, so no identifier moves and no
archived fit is orphaned. But the regression guard added with #549 is not portable:
green in CI, red on an AVX-512 developer machine.

Confirmed reproducible on an AVX-512 host with numpy 2.4.6 — both tests fail, at
mask_radius=3.0, total_gaussians=20 index 18 (the index pytest reports) and at
pixel_scales=0.1, total_gaussians=10 index 4.

Plan

  • Keep the exact-equality guarantee. pytest.approx(rel=1e-8) is explicitly ruled out
    by the test docstring and its reasoning still holds — it would only fail once the
    ladder has already moved past the identifier's quantisation.
  • Build the expected ladder element by element, the way the implementation does, so
    both sides take the same numpy scalar-power path. The comparison stays bitwise; only
    the code path used to produce the expectation changes.
  • Keep the existing docstring reasoning intact and add a note recording the portability
    trap, so a future tidy-up does not re-vectorise the expectation.
  • Verify: both tests fail before the change and pass after, on the same AVX-512 host;
    full test_autogalaxy/ suite stays green.
Detailed implementation plan

Work Classification

Library

Affected Repositories

  • PyAutoGalaxy (primary) — test-only change, no library source touched

Branch Survey

Repository Current Branch Dirty?
./PyAutoGalaxy claude/pyautogalaxy-mge-sigma-test-3neq07 (at origin/main = 13d3023) clean

Branch: claude/pyautogalaxy-mge-sigma-test-3neq07 (session-mandated)
Worktree root: n/a — cloud session, worked in the canonical /home/user/PyAutoGalaxy checkout

Implementation Steps

  1. test_autogalaxy/analysis/test_model_util.py,
    test__mge_model_from__default_sigma_list_is_bitwise_unchanged (~L169): replace

    assert sigma_list == list(
        10 ** np.linspace(-4, np.log10(mask_radius), total_gaussians)
    )

    with a named log10_sigma_list = np.linspace(-4, np.log10(mask_radius), total_gaussians)
    and an element-wise expectation [10 ** log10_sigma_list[i] for i in range(total_gaussians)].

  2. Same transformation in
    test__mge_point_model_from__default_sigma_list_is_bitwise_unchanged (~L220) for the
    np.linspace(-2.0, np.log10(max_sigma), total_gaussians) ladder.

  3. Extend both docstrings: keep the existing "why not pytest.approx(rel=1e-8)" reasoning
    verbatim, and add the portability note (scalar vs SIMD power loops are not bit-identical;
    the expectation must be built element-wise).

Key Files

  • test_autogalaxy/analysis/test_model_util.py — the two exact-equality guards (only file changed)
  • autogalaxy/analysis/model_util.py:190, :271 — the implementation's per-element
    10 ** log10_sigma_list[i], unchanged; it defines the code path the test must mirror

Notes / non-goals

  • The two neighbouring pytest.approx(..., 1.0e-8) assertions (L129, L237) are tolerance-based
    by design and are left alone.
  • np.log10(1e-4) == -4.0 and np.log10(0.01) == -2.0 exactly, so the test's literal
    endpoints remain a faithful stand-in for the implementation's np.log10(sigma_min).
  • No library source changes, so no downstream workspace impact.

Original Prompt

Click to expand starting prompt
# Bug in PyAutoGalaxy: the MGE bitwise sigma-ladder tests fail on

Type: test
Target: PyAutoGalaxy
Repos:
- PyAutoGalaxy
Difficulty: medium
Autonomy: safe
Priority: high
Status: formalised

Bug in PyAutoGalaxy: the MGE bitwise sigma-ladder tests fail on AVX-512 hardware. In PyAutoGalaxy, test__mge_model_from__default_sigma_list_is_bitwise_unchanged and test__mge_point_model_from__default_sigma_list_is_bitwise_unchanged in test_autogalaxy/analysis/test_model_util.py assert exact equality between two different numpy code paths. The PyAutoGalaxy implementation builds each sigma with a per-element scalar power, gaussian.sigma = 10 ** log10_sigma_list[i] at autogalaxy/analysis/model_util.py line 190, while the test builds its expectation with a vectorised 10 ** np.linspace(...). numpy does not guarantee the scalar and SIMD power loops agree bit for bit; on an x86-64-v4 / AVX-512 CPU they differ by 1 ULP at index 18, exactly the index pytest reports. Reproduced in isolation with numpy 2.4.6. The effect is cosmetic, not functional: the run identifier quantizes at RESOLUTION 1e-8 and this drift is ~1e-16 relative, so no identifier moves and no archived fit is orphaned. But the tests are green on GitHub runners and red on AVX-512 developer machines, so the regression guard added with PyAutoGalaxy#549 is not portable. Fix in PyAutoGalaxy by building the expected ladder element by element the way the implementation does, or by comparing with a tolerance far below the 1e-8 identifier resolution.

Session context added by the human at /start_dev:

Do NOT weaken this into pytest.approx(rel=1e-8) — the test docstring rules that out
explicitly and gives the reason. The guarantee worth keeping is that the ladder stays
stable well inside the identifier's RESOLUTION = 1e-8 quantisation. Preferred fix is to
build the expected ladder element-wise so both sides take the same numpy path; keep the
docstring's reasoning intact and note the portability trap in it.

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