Skip to content

Commit b898820

Browse files
authored
Merge pull request #551 from PyAutoLabs/claude/pyautogalaxy-mge-sigma-test-3neq07
test: make the MGE bitwise sigma-ladder guards portable across CPUs
2 parents 13d3023 + 91eb878 commit b898820

1 file changed

Lines changed: 24 additions & 6 deletions

File tree

test_autogalaxy/analysis/test_model_util.py

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,14 @@ def test__mge_model_from__default_sigma_list_is_bitwise_unchanged():
157157
`pytest.approx(rel=1e-8)` is deliberately NOT used: it only fails once the ladder
158158
has moved by a relative ~1e-7, which is already past the point where the
159159
identifier changes.
160+
161+
PORTABILITY TRAP -- the expected ladder is built element by element, exactly as
162+
`mge_model_from` builds it (`10 ** log10_sigma_list[i]`), and must stay that way.
163+
A vectorised `10 ** np.linspace(...)` is a DIFFERENT numpy code path: numpy does
164+
not guarantee its scalar and SIMD power loops agree bit for bit, and on AVX-512
165+
hardware they differ by 1 ULP, so writing the expectation vectorised makes this
166+
test pass on GitHub's runners and fail on an AVX-512 developer machine. Comparing
167+
like for like keeps the assertion exact without measuring the host's CPU.
160168
"""
161169
for mask_radius, total_gaussians in [(3.0, 20), (3.5, 30), (7.5, 10), (1.0, 5)]:
162170
model = ag.model_util.mge_model_from(
@@ -166,9 +174,11 @@ def test__mge_model_from__default_sigma_list_is_bitwise_unchanged():
166174
instance = model.instance_from_prior_medians()
167175
sigma_list = [profile.sigma for profile in instance.profile_list]
168176

169-
assert sigma_list == list(
170-
10 ** np.linspace(-4, np.log10(mask_radius), total_gaussians)
171-
)
177+
log10_sigma_list = np.linspace(-4, np.log10(mask_radius), total_gaussians)
178+
179+
assert sigma_list == [
180+
10 ** log10_sigma_list[i] for i in range(total_gaussians)
181+
]
172182

173183

174184
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():
207217
As for `mge_model_from`, the default `sigma_min=0.01` must reproduce the
208218
hardcoded `min_log10_sigma = -2.0` ladder that predates the argument EXACTLY,
209219
so the identifier of an existing point-source fit does not change.
220+
221+
The same portability trap applies here: build the expected ladder element by
222+
element, the way `mge_point_model_from` does, so both sides take numpy's scalar
223+
power path. A vectorised `10 ** np.linspace(...)` disagrees with it by 1 ULP on
224+
AVX-512 hardware -- see `test__mge_model_from__default_sigma_list_is_bitwise_unchanged`
225+
for the full reasoning, including why `pytest.approx(rel=1e-8)` is not the fix.
210226
"""
211227
for pixel_scales, total_gaussians in [(0.1, 10), (0.05, 5), (0.2, 3), (0.001, 4)]:
212228
model = ag.model_util.mge_point_model_from(
@@ -217,9 +233,11 @@ def test__mge_point_model_from__default_sigma_list_is_bitwise_unchanged():
217233

218234
max_sigma = max(2.0 * pixel_scales, 10**-2.0)
219235

220-
assert sigma_list == list(
221-
10 ** np.linspace(-2.0, np.log10(max_sigma), total_gaussians)
222-
)
236+
log10_sigma_list = np.linspace(-2.0, np.log10(max_sigma), total_gaussians)
237+
238+
assert sigma_list == [
239+
10 ** log10_sigma_list[i] for i in range(total_gaussians)
240+
]
223241

224242

225243
def test__mge_point_model_from__sigma_min_input_sets_smallest_gaussian():

0 commit comments

Comments
 (0)