@@ -121,6 +121,92 @@ def test__psf_precision_operator_sparse_from():
121121 assert psf_weighted_noise_lengths == pytest .approx (np .array ([4 , 3 , 2 , 1 ]), 1.0e-4 )
122122
123123
124+ def test__psf_precision_operator_sparse_from__edge_pixels ():
125+ # Regression test: every slim pixel sits at a corner of the 4x4 noise map,
126+ # so the kernel walk in psf_precision_value_from indexes off the array.
127+ # numba.jit() does not bounds-check, so without the explicit guard added
128+ # in the function those reads return uninitialized memory and produce
129+ # astronomically large or non-finite operator entries.
130+ noise_map = np .array (
131+ [
132+ [1.0 , 1.0 , 1.0 , 1.0 ],
133+ [1.0 , 2.0 , 2.0 , 1.0 ],
134+ [1.0 , 2.0 , 2.0 , 1.0 ],
135+ [1.0 , 1.0 , 1.0 , 1.0 ],
136+ ]
137+ )
138+ kernel = np .array ([[1.0 , 1.0 , 0.0 ], [1.0 , 2.0 , 1.0 ], [0.0 , 1.0 , 1.0 ]])
139+ native_index_for_slim_index = np .array ([[0 , 0 ], [0 , 3 ], [3 , 0 ], [3 , 3 ]])
140+
141+ (
142+ op ,
143+ indexes ,
144+ lengths ,
145+ ) = aa .util .inversion_imaging_numba .psf_precision_operator_sparse_from (
146+ noise_map_native = noise_map ,
147+ kernel_native = kernel ,
148+ native_index_for_slim_index = native_index_for_slim_index ,
149+ )
150+
151+ # Sanity: no inf/nan in the operator.
152+ assert np .isfinite (op ).all ()
153+ assert int (lengths .sum ()) == op .shape [0 ]
154+
155+ # Independent reference: a pure-numpy bounds-checked re-implementation of
156+ # psf_precision_value_from. The numba version with the fix applied must
157+ # match this byte-for-byte.
158+ def _reference_value (ip0_y , ip0_x , ip1_y , ip1_x ):
159+ h , w = noise_map .shape
160+ kh , kw = kernel .shape
161+ kernel_shift_y = - (kw // 2 )
162+ kernel_shift_x = - (kh // 2 )
163+ ip_y_offset = ip0_y - ip1_y
164+ ip_x_offset = ip0_x - ip1_x
165+ if (
166+ ip_y_offset < 2 * kernel_shift_y
167+ or ip_y_offset > - 2 * kernel_shift_y
168+ or ip_x_offset < 2 * kernel_shift_x
169+ or ip_x_offset > - 2 * kernel_shift_x
170+ ):
171+ return 0.0
172+ total = 0.0
173+ for k0_y in range (kh ):
174+ for k0_x in range (kw ):
175+ iy = ip0_y + k0_y + kernel_shift_y
176+ ix = ip0_x + k0_x + kernel_shift_x
177+ if iy < 0 or iy >= h or ix < 0 or ix >= w :
178+ continue
179+ v = noise_map [iy , ix ]
180+ if v > 0.0 :
181+ k1_y = k0_y + ip_y_offset
182+ k1_x = k0_x + ip_x_offset
183+ if 0 <= k1_y < kh and 0 <= k1_x < kw :
184+ total += kernel [k0_y , k0_x ] * kernel [k1_y , k1_x ] / v ** 2
185+ return total
186+
187+ n_pix = native_index_for_slim_index .shape [0 ]
188+ expected = []
189+ expected_indexes = []
190+ expected_lengths = []
191+ for ip0 in range (n_pix ):
192+ ip0_y , ip0_x = native_index_for_slim_index [ip0 ]
193+ count = 0
194+ for ip1 in range (ip0 , n_pix ):
195+ ip1_y , ip1_x = native_index_for_slim_index [ip1 ]
196+ v = _reference_value (ip0_y , ip0_x , ip1_y , ip1_x )
197+ if ip0 == ip1 :
198+ v /= 2.0
199+ if v > 0.0 :
200+ expected .append (v )
201+ expected_indexes .append (ip1 )
202+ count += 1
203+ expected_lengths .append (count )
204+
205+ assert op == pytest .approx (np .array (expected ), 1.0e-4 )
206+ assert indexes == pytest .approx (np .array (expected_indexes ), 1.0e-4 )
207+ assert lengths == pytest .approx (np .array (expected_lengths ), 1.0e-4 )
208+
209+
124210def test__data_vector_via_blurred_mapping_matrix_from ():
125211 blurred_mapping_matrix = np .array (
126212 [
0 commit comments