Skip to content

Commit 44afaa8

Browse files
Jammy2211claude
authored andcommitted
fix: raise a clear exception when adapt data and the data mask disagree
An adapt image defined on a different mask to the data cannot be indexed by the data's slim indexes. Until now this surfaced as a bare `IndexError` several frames deep inside `adaptive_pixel_signals_from`, where the first out-of-range index equals the adapt data's length — which reads as an off-by-one in the indexing rather than the mask mismatch it actually is. Check the lengths up front in `Mapper.pixel_signals_from` and raise an `InversionException` naming the likely cause (a stale adapt-image cache, see PyAutoGalaxy#516) and the remedy. A loud crash, not a silent guard — no fallback, no clamping. Two existing tests passed an unmasked 49-pixel adapt image against a 9-pixel mask, which the check correctly rejects. Both now build the adapt image on the data's own mask, as a real fit does; their assertions are unchanged. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 656be94 commit 44afaa8

3 files changed

Lines changed: 84 additions & 6 deletions

File tree

autoarray/inversion/mappers/abstract.py

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from autonerves import conf
66
from autonerves import cached_property
77

8+
from autoarray import exc
89
from autoarray.inversion.linear_obj.linear_obj import LinearObj
910
from autoarray.inversion.linear_obj.func_list import UniqueMappings
1011
from autoarray.inversion.linear_obj.neighbors import Neighbors
@@ -464,7 +465,30 @@ def pixel_signals_from(self, signal_scale: float, xp=np) -> np.ndarray:
464465
signal_scale
465466
A factor which controls how rapidly the smoothness of regularization varies from high signal regions to
466467
low signal regions.
467-
"""
468+
469+
Raises
470+
------
471+
exc.InversionException
472+
If the `adapt_data` is not defined on the same mask as the data being fitted, which would otherwise
473+
surface as a bare `IndexError` from the slim-index lookup inside `adaptive_pixel_signals_from`.
474+
"""
475+
adapt_data = self.adapt_data.array
476+
477+
data_pixels = self.over_sampler.mask.pixels_in_mask
478+
479+
if adapt_data.shape[0] != data_pixels:
480+
raise exc.InversionException(
481+
f"The adapt image passed to the mapper has {adapt_data.shape[0]} pixels, but the data being "
482+
f"fitted has {data_pixels} pixels, so the adapt image cannot be indexed by the data's pixels.\n\n"
483+
"The adapt image is therefore defined on a different mask to the dataset. The usual cause is a "
484+
"stale adapt-image cache: the per-galaxy adapt images written to a previous search's "
485+
"`files/galaxy_images_*.fits` are keyed by the search identifier, which encodes the model and "
486+
"the search but not the dataset. Re-running with a changed mask, image resolution or "
487+
"`PYAUTO_SMALL_DATASETS` setting, but an unchanged model, reuses that search's output "
488+
"directory and its now-stale cache.\n\n"
489+
"Delete the affected search's output directory (or the `galaxy_images_*.fits` files within it) "
490+
"and re-run so the adapt images are recomputed on the current mask."
491+
)
468492

469493
return mapper_util.adaptive_pixel_signals_from(
470494
pixels=self.pixels,
@@ -473,7 +497,7 @@ def pixel_signals_from(self, signal_scale: float, xp=np) -> np.ndarray:
473497
pix_indexes_for_sub_slim_index=self.pix_indexes_for_sub_slim_index,
474498
pix_size_for_sub_slim_index=self.pix_sizes_for_sub_slim_index,
475499
slim_index_for_sub_slim_index=self.over_sampler.slim_for_sub_slim,
476-
adapt_data=self.adapt_data.array,
500+
adapt_data=adapt_data,
477501
xp=xp,
478502
)
479503

test_autoarray/inversion/pixelization/mappers/test_abstract.py

Lines changed: 51 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ def test__data_weight_total_for_pix_from__multi_pixel_mappings__sums_weights_per
9898
assert data_weight_total_for_pix == pytest.approx([1.2, 0.7, 0.3, 1.4, 4.4], 1.0e-4)
9999

100100

101-
def test__adaptive_pixel_signals_from___matches_util(grid_2d_7x7, image_7x7):
101+
def test__adaptive_pixel_signals_from___matches_util(grid_2d_7x7):
102102
pixels = 6
103103
signal_scale = 2.0
104104
interpolator = aa.m.MockInterpolator(
@@ -110,11 +110,15 @@ def test__adaptive_pixel_signals_from___matches_util(grid_2d_7x7, image_7x7):
110110

111111
over_sampler = aa.OverSampler(mask=grid_2d_7x7.mask, sub_size=1)
112112

113+
# The adapt image is defined on the data's own mask, as it is for a real fit.
114+
115+
adapt_data = aa.Array2D(values=np.ones(9), mask=grid_2d_7x7.mask)
116+
113117
mapper = aa.m.MockMapper(
114118
source_plane_data_grid=grid_2d_7x7,
115119
over_sampler=over_sampler,
116120
interpolator=interpolator,
117-
adapt_data=image_7x7,
121+
adapt_data=adapt_data,
118122
parameters=pixels,
119123
)
120124

@@ -127,12 +131,56 @@ def test__adaptive_pixel_signals_from___matches_util(grid_2d_7x7, image_7x7):
127131
pix_indexes_for_sub_slim_index=interpolator.mappings,
128132
pix_size_for_sub_slim_index=interpolator.sizes,
129133
slim_index_for_sub_slim_index=over_sampler.slim_for_sub_slim,
130-
adapt_data=np.array(image_7x7),
134+
adapt_data=np.array(adapt_data),
131135
)
132136

133137
assert (pixel_signals == pixel_signals_util).all()
134138

135139

140+
def test__pixel_signals_from__adapt_data_on_a_different_mask__raises_clear_exception(
141+
grid_2d_7x7,
142+
):
143+
"""
144+
An adapt image defined on a different mask to the data cannot be indexed by the data's slim indexes.
145+
146+
The usual cause is a stale adapt-image cache being reused after the dataset's mask changed, and before
147+
this guard it surfaced as a bare `IndexError` several frames deep in `adaptive_pixel_signals_from`,
148+
which reads as an off-by-one rather than a mask mismatch (PyAutoGalaxy#516).
149+
"""
150+
interpolator = aa.m.MockInterpolator(
151+
mappings=np.array([[1], [1], [4], [0], [0], [3], [0], [0], [3]]),
152+
sizes=np.array([1, 1, 1, 1, 1, 1, 1, 1, 1]),
153+
weights=np.ones(9),
154+
)
155+
156+
over_sampler = aa.OverSampler(mask=grid_2d_7x7.mask, sub_size=1)
157+
158+
# The data has 9 unmasked pixels, the adapt image only 8.
159+
160+
adapt_mask = np.full(fill_value=True, shape=(7, 7))
161+
adapt_mask[2:5, 2:5] = False
162+
adapt_mask[2, 2] = True
163+
164+
adapt_data = aa.Array2D(
165+
values=np.ones(8),
166+
mask=aa.Mask2D(mask=adapt_mask, pixel_scales=1.0),
167+
)
168+
169+
mapper = aa.m.MockMapper(
170+
source_plane_data_grid=grid_2d_7x7,
171+
over_sampler=over_sampler,
172+
interpolator=interpolator,
173+
adapt_data=adapt_data,
174+
parameters=6,
175+
)
176+
177+
with pytest.raises(aa.exc.InversionException) as e:
178+
mapper.pixel_signals_from(signal_scale=2.0)
179+
180+
assert "8 pixels" in str(e.value)
181+
assert "9 pixels" in str(e.value)
182+
183+
136184
def test__mapped_to_source_from__delaunay_mapper__matches_mapping_matrix_util(
137185
grid_2d_7x7,
138186
):

test_autoarray/inversion/pixelization/mappers/test_rectangular.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import numpy as np
2+
13
import autoarray as aa
24

35
from autoarray.inversion.mesh.mesh.rectangular_adapt_density import (
@@ -52,7 +54,7 @@ def test__pix_indexes_for_sub_slim_index__rectangular_uniform_mesh__matches_util
5254

5355

5456
def test__pixel_signals_from__rectangular_adapt_density_mesh__matches_util(
55-
grid_2d_sub_1_7x7, image_7x7
57+
grid_2d_sub_1_7x7,
5658
):
5759

5860
mesh_grid = overlay_grid_from(
@@ -61,6 +63,10 @@ def test__pixel_signals_from__rectangular_adapt_density_mesh__matches_util(
6163

6264
mesh = aa.mesh.RectangularAdaptDensity(shape=(3, 3))
6365

66+
# The adapt image is defined on the data's own mask, as it is for a real fit.
67+
68+
image_7x7 = aa.Array2D(values=np.ones(9), mask=grid_2d_sub_1_7x7.mask)
69+
6470
interpolator = mesh.interpolator_from(
6571
source_plane_data_grid=grid_2d_sub_1_7x7,
6672
source_plane_mesh_grid=mesh_grid,

0 commit comments

Comments
 (0)