From 91eb878a364877cd3cd98d7bc1d93e71fa793819 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 5 Aug 2026 11:56:22 +0000 Subject: [PATCH] test: make the MGE bitwise sigma-ladder guards portable across CPUs The two `*_default_sigma_list_is_bitwise_unchanged` tests added with #549 asserted 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]`, model_util.py:190 and :271) while the tests built their expectation with a vectorised `10 ** np.linspace(...)`. numpy does not guarantee its scalar and SIMD power loops agree bit for bit, so the two disagree by 1 ULP on AVX-512 hardware -- green on GitHub's runners, red on an AVX-512 developer machine. The guard was therefore not portable. Build the expected ladder element by element instead, so both sides take the same numpy path. The comparison stays exact; only the code path producing the expectation changes. `pytest.approx(rel=1e-8)` is still deliberately not used -- the existing docstring reasoning for that is kept verbatim, and both docstrings now record the portability trap so the expectation is not re-vectorised later. Verified on an AVX-512 host with numpy 2.4.6: both tests failed before this change (mask_radius=3.0/20 at index 18, pixel_scales=0.1/10 at index 4) and pass after. Control-tested by perturbing the implementation defaults by a relative 1e-7, which both tests still catch, so the exactness guarantee is intact. Full test_autogalaxy suite: 1004 passed, 3 skipped. Closes #550 Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_011tJFsEesnF7rZmn2xvfxUe --- test_autogalaxy/analysis/test_model_util.py | 30 ++++++++++++++++----- 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/test_autogalaxy/analysis/test_model_util.py b/test_autogalaxy/analysis/test_model_util.py index 1fa11e52..5a3a352f 100644 --- a/test_autogalaxy/analysis/test_model_util.py +++ b/test_autogalaxy/analysis/test_model_util.py @@ -157,6 +157,14 @@ def test__mge_model_from__default_sigma_list_is_bitwise_unchanged(): `pytest.approx(rel=1e-8)` is deliberately NOT used: it only fails once the ladder has moved by a relative ~1e-7, which is already past the point where the identifier changes. + + PORTABILITY TRAP -- the expected ladder is built element by element, exactly as + `mge_model_from` builds it (`10 ** log10_sigma_list[i]`), and must stay that way. + A vectorised `10 ** np.linspace(...)` is a DIFFERENT numpy code path: numpy does + not guarantee its scalar and SIMD power loops agree bit for bit, and on AVX-512 + hardware they differ by 1 ULP, so writing the expectation vectorised makes this + test pass on GitHub's runners and fail on an AVX-512 developer machine. Comparing + like for like keeps the assertion exact without measuring the host's CPU. """ for mask_radius, total_gaussians in [(3.0, 20), (3.5, 30), (7.5, 10), (1.0, 5)]: model = ag.model_util.mge_model_from( @@ -166,9 +174,11 @@ def test__mge_model_from__default_sigma_list_is_bitwise_unchanged(): instance = model.instance_from_prior_medians() sigma_list = [profile.sigma for profile in instance.profile_list] - assert sigma_list == list( - 10 ** np.linspace(-4, np.log10(mask_radius), total_gaussians) - ) + log10_sigma_list = np.linspace(-4, np.log10(mask_radius), total_gaussians) + + assert sigma_list == [ + 10 ** log10_sigma_list[i] for i in range(total_gaussians) + ] def test__mge_point_model_from__returns_basis_model_with_correct_gaussians(): @@ -207,6 +217,12 @@ def test__mge_point_model_from__default_sigma_list_is_bitwise_unchanged(): As for `mge_model_from`, the default `sigma_min=0.01` must reproduce the hardcoded `min_log10_sigma = -2.0` ladder that predates the argument EXACTLY, so the identifier of an existing point-source fit does not change. + + The same portability trap applies here: build the expected ladder element by + element, the way `mge_point_model_from` does, so both sides take numpy's scalar + power path. A vectorised `10 ** np.linspace(...)` disagrees with it by 1 ULP on + AVX-512 hardware -- see `test__mge_model_from__default_sigma_list_is_bitwise_unchanged` + for the full reasoning, including why `pytest.approx(rel=1e-8)` is not the fix. """ for pixel_scales, total_gaussians in [(0.1, 10), (0.05, 5), (0.2, 3), (0.001, 4)]: model = ag.model_util.mge_point_model_from( @@ -217,9 +233,11 @@ def test__mge_point_model_from__default_sigma_list_is_bitwise_unchanged(): max_sigma = max(2.0 * pixel_scales, 10**-2.0) - assert sigma_list == list( - 10 ** np.linspace(-2.0, np.log10(max_sigma), total_gaussians) - ) + log10_sigma_list = np.linspace(-2.0, np.log10(max_sigma), total_gaussians) + + assert sigma_list == [ + 10 ** log10_sigma_list[i] for i in range(total_gaussians) + ] def test__mge_point_model_from__sigma_min_input_sets_smallest_gaussian():