Skip to content

Commit c25d218

Browse files
authored
Merge pull request #456 from PyAutoLabs/claude/autoarray-numba-psf-garbage-hfxnjv
fix: bounds-guard the kernel gather in psf_weighted_data_from
2 parents 1c33850 + 05f2506 commit c25d218

2 files changed

Lines changed: 67 additions & 3 deletions

File tree

autoarray/inversion/inversion/imaging_numba/inversion_imaging_numba_util.py

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -61,9 +61,28 @@ def psf_weighted_data_from(
6161

6262
for k0_y in range(kernel_native.shape[0]):
6363
for k0_x in range(kernel_native.shape[1]):
64-
weight_value = weight_map_native[
65-
ip0_y + k0_y + kernel_shift_y, ip0_x + k0_x + kernel_shift_x
66-
]
64+
iy = ip0_y + k0_y + kernel_shift_y
65+
ix = ip0_x + k0_x + kernel_shift_x
66+
67+
# numba @jit() does not bounds-check array reads. Without this
68+
# guard, a kernel position that lands off the weight-map array
69+
# (e.g. a mask pixel within `kernel_shift` of the array edge)
70+
# silently reads uninitialized memory, producing astronomical
71+
# or non-finite contributions that poison `psf_weighted_data`
72+
# and the data vector computed from it. A negative index is
73+
# just as unsafe: it wraps to the opposite edge and quietly
74+
# convolves in unrelated pixels. Positions off the array
75+
# contribute zero, matching the zero-padded reference in
76+
# `inversion_imaging_util.psf_weighted_data_from`.
77+
if (
78+
iy < 0
79+
or iy >= weight_map_native.shape[0]
80+
or ix < 0
81+
or ix >= weight_map_native.shape[1]
82+
):
83+
continue
84+
85+
weight_value = weight_map_native[iy, ix]
6786

6887
if not np.isnan(weight_value):
6988
value += kernel_native[k0_y, k0_x] * weight_value

test_autoarray/inversion/inversion/imaging/test_inversion_imaging_util.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,51 @@ def test__psf_weighted_noise_imaging_from():
3636
)
3737

3838

39+
def test__psf_weighted_data_from__unmasked_pixels_on_array_edge():
40+
"""
41+
Regression test: an unmasked pixel within `kernel_shape // 2` of the array
42+
edge drives the kernel off the weight map.
43+
44+
numba `@jit()` does not bounds-check array reads, so those positions
45+
silently returned uninitialized memory (values of order 1e299) rather than
46+
raising, poisoning `psf_weighted_data` and the data vector built from it.
47+
Because the values read depend on whatever the allocator left next to the
48+
weight map, the corruption was heap-state dependent: deterministic on the
49+
first call after a cold-cache compile, and intermittent in forked
50+
multiprocessing workers.
51+
52+
The zero-padded numpy implementation is the reference — kernel positions
53+
off the array contribute zero. Every other test in this module masks a
54+
one-pixel border, so none of them exercise this path.
55+
"""
56+
57+
image = np.arange(1.0, 26.0).reshape(5, 5)
58+
noise_map = np.ones((5, 5))
59+
60+
kernel = np.array([[0.0, 1.0, 2.0], [3.0, 4.0, 1.0], [2.0, 0.0, 1.0]])
61+
62+
# Every pixel unmasked, so the border pixels push the kernel off the array.
63+
native_index_for_slim_index = np.array(
64+
[[y, x] for y in range(5) for x in range(5)]
65+
)
66+
67+
psf_weighted_data = aa.util.inversion_imaging_numba.psf_weighted_data_from(
68+
image_native=image,
69+
noise_map_native=noise_map,
70+
kernel_native=kernel,
71+
native_index_for_slim_index=native_index_for_slim_index,
72+
)
73+
74+
psf_weighted_data_numpy = aa.util.inversion_imaging.psf_weighted_data_from(
75+
weight_map_native=image / noise_map**2.0,
76+
kernel_native=kernel,
77+
native_index_for_slim_index=native_index_for_slim_index,
78+
)
79+
80+
assert np.all(np.isfinite(psf_weighted_data))
81+
assert psf_weighted_data == pytest.approx(psf_weighted_data_numpy, 1.0e-8)
82+
83+
3984
def test__psf_weighted_data_from():
4085

4186
mask = aa.Mask2D(

0 commit comments

Comments
 (0)