Skip to content

Commit 9411904

Browse files
Jammy2211Jammy2211claude
authored
fix: reject split regularization on rectangular meshes at construction (#417)
Rectangular meshes do not support split regularization -- their interpolators provide no split-cross mappings. This was already documented in the workspace prose ('Rectangular meshes use `Adapt`, not `AdaptSplit`') but never enforced, so the combination failed deep inside the inversion in two different ways: RectangularUniform -> AttributeError: 'InterpolatorRectangularUniform' has no attribute '_mappings_sizes_weights_split' RectangularAdaptDensity -> IndexError: index 4 is out of bounds for axis 0 RectangularAdaptImage with size 4 Nine combinations were affected (3 rectangular meshes x 3 split schemes), not the one reported. `Pixelization` now rejects the pairing at construction, naming both the mesh and the regularization, via two capability flags: `AbstractMesh.supports_split_regularization` and `AbstractRegularization.is_split_regularization`. Also removes `InterpolatorRectangular._mappings_sizes_weights_split`, which returned the plain 4-corner mappings on the reasoning that bilinear interpolation already covers the neighbourhood. That was not correct -- `reg_split_from` expects the split-cross structure, which is what produced the IndexError one frame later. The exception is `autoarray.exc.PixelizationException`, deliberately NOT a `FitException`: autogalaxy's same-named subclass IS one, and `fitness.py` converts those into resample-rejects, which would make a search silently discard every sample instead of reporting the misconfiguration. Reported by @rhayes777 in #332. Phase 1 of #416 (epic #415). Co-authored-by: Jammy2211 <JNightingale2211@gmail.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
1 parent 24863a2 commit 9411904

9 files changed

Lines changed: 204 additions & 6 deletions

File tree

autoarray/inversion/mesh/interpolator/rectangular.py

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -456,9 +456,13 @@ def _mappings_sizes_weights(self):
456456

457457
return mappings, sizes, weights
458458

459-
@cached_property
460-
def _mappings_sizes_weights_split(self):
461-
# Rectangular pixelizations use bilinear interpolation which already factors
462-
# in the 4-corner neighbourhood, so no separate split-cross calculation is
463-
# needed — split regularization reuses the same mappings.
464-
return self._mappings_sizes_weights
459+
# NOTE: `_mappings_sizes_weights_split` is deliberately NOT implemented here.
460+
#
461+
# It previously returned `self._mappings_sizes_weights` unchanged, on the reasoning that
462+
# bilinear interpolation already factors in the 4-corner neighbourhood. That was not correct:
463+
# `reg_split_from` expects the split-cross structure, so the pass-through raised
464+
# `IndexError: index 4 is out of bounds for axis 0 with size 4` one frame later.
465+
#
466+
# Rectangular meshes do not support split regularization. The combination is now rejected at
467+
# `Pixelization` construction via `AbstractMesh.supports_split_regularization`, which names
468+
# both the mesh and the regularization instead of failing deep inside the inversion.

autoarray/inversion/mesh/mesh/abstract.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,17 @@
99

1010

1111
class AbstractMesh:
12+
supports_split_regularization = True
13+
"""
14+
Whether this mesh supports "split" regularization schemes (e.g. ``ConstantSplit``, ``AdaptSplit``,
15+
``AdaptSplitZeroth``).
16+
17+
Split schemes regularize using a split-cross calculation, which requires the mesh's interpolator to
18+
provide ``_mappings_sizes_weights_split``. The adaptive meshes (``Delaunay``, ``KNNBarycentric``)
19+
compute this; the rectangular meshes do not, and set this to ``False`` so that ``Pixelization``
20+
rejects the combination at construction rather than failing deep inside the inversion.
21+
"""
22+
1223
def __eq__(self, other):
1324
return self.__dict__ == other.__dict__ and self.__class__ is other.__class__
1425

autoarray/inversion/mesh/mesh/rectangular_adapt_density.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,10 @@ def overlay_grid_from(
6262

6363

6464
class RectangularAdaptDensity(AbstractMesh):
65+
# Rectangular meshes do not support split regularization -- their interpolators provide no
66+
# split-cross mappings. Inherited by `RectangularUniform` and `RectangularAdaptImage`.
67+
supports_split_regularization = False
68+
6569
def __init__(
6670
self,
6771
shape: Tuple[int, int] = (3, 3),

autoarray/inversion/pixelization.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,30 @@ def __init__(
152152
model = af.Collection(galaxies=af.Collection(galaxy=galaxy))
153153
"""
154154

155+
if (
156+
regularization is not None
157+
and getattr(regularization, "is_split_regularization", False)
158+
and not getattr(mesh, "supports_split_regularization", True)
159+
):
160+
raise exc.PixelizationException(
161+
f"""
162+
The regularization scheme `{type(regularization).__name__}` is a split regularization
163+
scheme, which is not supported by the mesh `{type(mesh).__name__}`.
164+
165+
Split regularization regularizes using a cross of four points around each pixel centre,
166+
which requires the mesh to provide split-cross mappings. The rectangular meshes
167+
(`RectangularUniform`, `RectangularAdaptDensity`, `RectangularAdaptImage`) do not
168+
provide them.
169+
170+
Use either:
171+
172+
- an adaptive mesh which supports split regularization, e.g. `Delaunay` or
173+
`KNNBarycentric`, with `{type(regularization).__name__}`; or
174+
- a non-split regularization scheme with `{type(mesh).__name__}`, e.g. `Constant`
175+
instead of `ConstantSplit`, or `Adapt` instead of `AdaptSplit`.
176+
"""
177+
)
178+
155179
self.mesh = mesh
156180
self.regularization = regularization
157181

autoarray/inversion/regularization/abstract.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,17 @@
77

88

99
class AbstractRegularization:
10+
is_split_regularization = False
11+
"""
12+
Whether this scheme is a "split" regularization variant, which regularizes using a split-cross
13+
calculation of the mesh's mappings rather than the mappings themselves.
14+
15+
Split schemes require the mesh's interpolator to provide `_mappings_sizes_weights_split`, which
16+
only the adaptive meshes (e.g. ``Delaunay``, ``KNNBarycentric``) do. ``Pixelization`` uses this
17+
flag together with ``AbstractMesh.supports_split_regularization`` to reject unsupported
18+
combinations at construction.
19+
"""
20+
1021
def __init__(self):
1122
"""
1223
Abstract base class for a regularization-scheme, which is applied to a pixelization to enforce a \

autoarray/inversion/regularization/adapt_split.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212

1313
class AdaptSplit(Adapt):
14+
is_split_regularization = True
1415
def __init__(
1516
self,
1617
inner_coefficient: float = 1.0,

autoarray/inversion/regularization/adapt_split_zeroth.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212

1313
class AdaptSplitZeroth(Adapt):
14+
is_split_regularization = True
1415
def __init__(
1516
self,
1617
zeroth_coefficient: float = 1.0,

autoarray/inversion/regularization/constant_split.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212

1313
class ConstantSplit(Constant):
14+
is_split_regularization = True
1415
def __init__(self, coefficient: float = 1.0):
1516
"""
1617
Regularization which uses the derivatives at a cross of four points around each pixel centre and a single
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
"""
2+
Regression tests for @rhayes777's audit finding in PyAutoArray#332.
3+
4+
Rectangular meshes do not support split regularization -- their interpolators provide no
5+
split-cross mappings. Before the fix this surfaced two different ways, both deep inside the
6+
inversion and neither naming the real cause:
7+
8+
- ``RectangularUniform`` -> ``AttributeError: 'InterpolatorRectangularUniform' object has no
9+
attribute '_mappings_sizes_weights_split'``
10+
- ``RectangularAdaptDensity`` / ``RectangularAdaptImage``
11+
-> ``IndexError: index 4 is out of bounds for axis 0 with size 4``
12+
(``InterpolatorRectangular`` returned the plain 4-corner mappings
13+
from a pass-through that claimed split "reuses the same mappings")
14+
15+
``Pixelization`` now rejects the combination at construction. These tests assert the *clear
16+
failure*, not a successful fit -- the capability is deliberately absent.
17+
"""
18+
19+
import pytest
20+
21+
import autoarray as aa
22+
from autoarray import exc
23+
24+
25+
RECTANGULAR_MESHES = [
26+
aa.mesh.RectangularUniform,
27+
aa.mesh.RectangularAdaptDensity,
28+
aa.mesh.RectangularAdaptImage,
29+
]
30+
31+
SPLIT_REGULARIZATIONS = [
32+
aa.reg.ConstantSplit,
33+
aa.reg.AdaptSplit,
34+
aa.reg.AdaptSplitZeroth,
35+
]
36+
37+
ADAPTIVE_MESHES = [
38+
aa.mesh.Delaunay,
39+
aa.mesh.KNNBarycentric,
40+
]
41+
42+
43+
@pytest.mark.parametrize("mesh_cls", RECTANGULAR_MESHES)
44+
@pytest.mark.parametrize("regularization_cls", SPLIT_REGULARIZATIONS)
45+
def test__rectangular_mesh_with_split_regularization__raises(mesh_cls, regularization_cls):
46+
"""All 9 rectangular-mesh x split-regularization combinations are rejected."""
47+
48+
with pytest.raises(exc.PixelizationException) as error:
49+
aa.Pixelization(
50+
mesh=mesh_cls(shape=(15, 15)),
51+
regularization=regularization_cls(),
52+
)
53+
54+
message = str(error.value)
55+
56+
# the message must name both sides, so the user can act on it without a traceback
57+
assert mesh_cls.__name__ in message
58+
assert regularization_cls.__name__ in message
59+
60+
61+
@pytest.mark.parametrize("mesh_cls", RECTANGULAR_MESHES)
62+
def test__rectangular_mesh_with_non_split_regularization__is_allowed(mesh_cls):
63+
"""The guard is specific to split schemes; `Constant` on the same meshes still builds."""
64+
65+
pixelization = aa.Pixelization(
66+
mesh=mesh_cls(shape=(15, 15)),
67+
regularization=aa.reg.Constant(coefficient=1.0),
68+
)
69+
70+
assert isinstance(pixelization.mesh, mesh_cls)
71+
72+
73+
@pytest.mark.parametrize("mesh_cls", ADAPTIVE_MESHES)
74+
@pytest.mark.parametrize("regularization_cls", SPLIT_REGULARIZATIONS)
75+
def test__adaptive_mesh_with_split_regularization__is_allowed(mesh_cls, regularization_cls):
76+
"""Split regularization remains supported on the meshes that implement it."""
77+
78+
pixelization = aa.Pixelization(
79+
mesh=mesh_cls(pixels=100),
80+
regularization=regularization_cls(),
81+
)
82+
83+
assert isinstance(pixelization.mesh, mesh_cls)
84+
85+
86+
@pytest.mark.parametrize("mesh_cls", RECTANGULAR_MESHES)
87+
def test__rectangular_mesh_without_regularization__is_allowed(mesh_cls):
88+
"""`regularization` is optional; a `None` value must not trip the guard."""
89+
90+
pixelization = aa.Pixelization(mesh=mesh_cls(shape=(15, 15)))
91+
92+
assert pixelization.regularization is None
93+
94+
95+
def test__capability_flags():
96+
"""The flags the guard reads, asserted directly so a future mesh can't silently regress."""
97+
98+
assert aa.mesh.RectangularUniform(shape=(15, 15)).supports_split_regularization is False
99+
assert aa.mesh.RectangularAdaptDensity(shape=(15, 15)).supports_split_regularization is False
100+
assert aa.mesh.RectangularAdaptImage(shape=(15, 15)).supports_split_regularization is False
101+
assert aa.mesh.Delaunay(pixels=100).supports_split_regularization is True
102+
assert aa.mesh.KNNBarycentric(pixels=100).supports_split_regularization is True
103+
104+
assert aa.reg.ConstantSplit().is_split_regularization is True
105+
assert aa.reg.AdaptSplit().is_split_regularization is True
106+
assert aa.reg.AdaptSplitZeroth().is_split_regularization is True
107+
assert aa.reg.Constant().is_split_regularization is False
108+
assert aa.reg.Adapt().is_split_regularization is False
109+
110+
111+
def test__interpolator_rectangular_has_no_split_mappings():
112+
"""
113+
The pass-through that produced the `IndexError` is gone and must stay gone -- restoring it
114+
would reintroduce a silent-looking API that fails one frame later.
115+
"""
116+
117+
from autoarray.inversion.mesh.interpolator.rectangular import InterpolatorRectangular
118+
from autoarray.inversion.mesh.interpolator.rectangular_uniform import (
119+
InterpolatorRectangularUniform,
120+
)
121+
from autoarray.inversion.mesh.interpolator.delaunay import InterpolatorDelaunay
122+
123+
assert not hasattr(InterpolatorRectangular, "_mappings_sizes_weights_split")
124+
assert not hasattr(InterpolatorRectangularUniform, "_mappings_sizes_weights_split")
125+
assert hasattr(InterpolatorDelaunay, "_mappings_sizes_weights_split")
126+
127+
128+
def test__pixelization_exception_is_not_a_fit_exception():
129+
"""
130+
An unsupported combination is a configuration error, not a bad model sample.
131+
132+
`autogalaxy.exc.PixelizationException` subclasses `af.exc.FitException`, which
133+
`fitness.py` converts into a resample-reject. If the exception raised here were a
134+
`FitException`, a search would silently reject every sample instead of reporting the
135+
misconfiguration. `autoarray`'s own `PixelizationException` must stay a plain `Exception`.
136+
"""
137+
138+
assert issubclass(exc.PixelizationException, Exception)
139+
140+
af = pytest.importorskip("autofit")
141+
assert not issubclass(exc.PixelizationException, af.exc.FitException)

0 commit comments

Comments
 (0)