Skip to content

Commit 5dedb5e

Browse files
authored
fix: name adapt_images when an adaptive mesh has no image-plane grid (#332) (#442)
Closes #332. Phase 3 of the @rhayes777 API audit epic #415. Omitting adapt_images with Delaunay / KNearestNeighbor / KNNBarycentric surfaced as AttributeError: 'NoneType' object has no attribute 'array' inside border_relocator.py, naming nothing the caller controls. It now raises MeshException naming adapt_images, showing the AdaptImages idiom, and noting the rectangular family needs none. The guard sits at Delaunay.interpolator_from, inherited by the whole adaptive family. Chose fail-fast over having the mesh wire the grid up itself (the reporter's suggestion): building an image-plane mesh grid needs a weighting policy, which is exactly what adapt_images carries, so inventing one would silently make a science choice for the user. Matches phase 1's handling of the rectangular/split case. THE MESHES ARE NOT BROKEN — the issue headline is false and the posted reply already corrects it. Re-verified: with adapt_images, Delaunay+Constant, KNNBarycentric+Constant and Delaunay+ConstantSplit all fit; RectangularUniform+Constant fits with none. Tests assert the clear failure plus those controls, never that bare construction succeeds. Diagnosis correction: the None is mesh_grid (border_relocator.py:450), not grid (446). 991 passed, +10 new tests, zero regressions.
1 parent 72fc49e commit 5dedb5e

3 files changed

Lines changed: 198 additions & 0 deletions

File tree

autoarray/inversion/mesh/mesh/abstract.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import numpy as np
22
from typing import Optional
33

4+
from autoarray import exc
45
from autoarray.settings import Settings
56
from autoarray.inversion.mesh.border_relocator import BorderRelocator
67
from autoarray.inversion.regularization.abstract import AbstractRegularization
@@ -64,6 +65,50 @@ def relocated_grid_from(
6465
xp=xp,
6566
)
6667

68+
def _validate_source_plane_mesh_grid(self, source_plane_mesh_grid):
69+
"""
70+
Raise if the mesh was given no source-plane mesh grid.
71+
72+
The adaptive meshes (``Delaunay``, ``KNearestNeighbor``, ``KNNBarycentric``) do
73+
not compute their own image-plane mesh grid — it is a required input, supplied
74+
in PyAutoGalaxy / PyAutoLens through ``adapt_images``. Omitting it leaves this
75+
grid ``None`` and the failure previously landed several frames deeper as
76+
``AttributeError: 'NoneType' object has no attribute 'array'`` inside
77+
``border_relocator.py``, naming nothing the caller controls and no file the
78+
caller has opened.
79+
80+
This raises at the point the precondition is known to be unmet, and names
81+
``adapt_images`` so the message points at the thing the caller actually passes.
82+
83+
Fail-fast is deliberate rather than having the mesh wire the grid up itself:
84+
constructing an image-plane mesh grid requires a weighting policy (which is
85+
exactly what ``adapt_images`` carries), so inventing one here would silently
86+
pick a science choice on the user's behalf. This matches how the
87+
rectangular-mesh / split-regularization combination was handled — an explicit
88+
"you must supply X" exception rather than implementing a missing capability.
89+
90+
Parameters
91+
----------
92+
source_plane_mesh_grid
93+
The source-plane mesh grid to check.
94+
"""
95+
if source_plane_mesh_grid is None:
96+
raise exc.MeshException(
97+
f"The mesh `{type(self).__name__}` was not given an image-plane mesh "
98+
f"grid, so its source-plane mesh grid is None and the pixelization "
99+
f"cannot be built.\n\n"
100+
f"This mesh does not compute that grid itself — it is a required "
101+
f"input, supplied via `adapt_images`:\n\n"
102+
f" adapt_images = al.AdaptImages(\n"
103+
f" galaxy_image_plane_mesh_grid_dict={{source: image_plane_mesh_grid}}\n"
104+
f" )\n"
105+
f" fit = al.FitImaging(dataset=dataset, tracer=tracer, adapt_images=adapt_images)\n\n"
106+
f"See the `pixelization` feature scripts in the workspace (e.g. "
107+
f"`imaging/features/pixelization/delaunay.py`) for the full idiom. A "
108+
f"mesh in the rectangular family (e.g. `RectangularUniform`) builds "
109+
f"its own grid and needs no `adapt_images`."
110+
)
111+
67112
def relocated_mesh_grid_from(
68113
self,
69114
border_relocator: Optional[BorderRelocator],

autoarray/inversion/mesh/mesh/delaunay.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,14 @@ def interpolator_from(
177177
adapt_data
178178
Not used for a rectangular mesh.
179179
"""
180+
# Adaptive meshes require an image-plane mesh grid (supplied via `adapt_images`)
181+
# and do not compute one themselves. Checked here, at the entry point the whole
182+
# adaptive family shares, so a missing precondition names `adapt_images` rather
183+
# than surfacing as an AttributeError on None several frames deeper.
184+
self._validate_source_plane_mesh_grid(
185+
source_plane_mesh_grid=source_plane_mesh_grid
186+
)
187+
180188
relocated_grid = self.relocated_grid_from(
181189
border_relocator=border_relocator,
182190
source_plane_data_grid=source_plane_data_grid,
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
"""
2+
Regression tests for PyAutoArray#332 — the missing-`adapt_images` precondition.
3+
4+
__What this issue actually is__
5+
6+
The issue's headline says `Delaunay` and `KNNBarycentric` are "unusable in
7+
`FitImaging`". That is **false**, and the public reply on #332 corrects it while
8+
crediting the underlying finding. Both meshes work correctly; they *require* an
9+
image-plane mesh grid, supplied via `adapt_images`.
10+
11+
So the defect is the **error**, not the mesh. Omitting `adapt_images` used to surface
12+
as:
13+
14+
AttributeError: 'NoneType' object has no attribute 'array'
15+
autoarray/inversion/mesh/border_relocator.py, in relocated_mesh_grid_from
16+
17+
— naming nothing the caller controls, in a file they have never opened.
18+
19+
**These tests therefore assert a CLEAR FAILURE, not a successful fit.** Asserting that
20+
bare construction succeeds would enshrine the reporter's misreading; that trap is
21+
recorded in the prompt for this task and is deliberately avoided here.
22+
23+
The `adapt_images` branch is exercised at the integration level (a real `FitImaging`
24+
with an `AdaptImages` still fits, for `Delaunay` + `Constant`, `KNNBarycentric` +
25+
`Constant` and `Delaunay` + `ConstantSplit`); these unit tests cover the guard itself
26+
and its controls at the mesh boundary.
27+
"""
28+
29+
import numpy as np
30+
import pytest
31+
32+
import autoarray as aa
33+
from autoarray import exc
34+
35+
36+
@pytest.fixture(name="source_plane_data_grid")
37+
def make_source_plane_data_grid():
38+
return aa.Grid2D.uniform(shape_native=(5, 5), pixel_scales=1.0)
39+
40+
41+
@pytest.fixture(name="source_plane_mesh_grid")
42+
def make_source_plane_mesh_grid():
43+
return aa.Grid2DIrregular(
44+
values=[[0.1, 0.1], [1.1, 0.6], [2.1, 0.1], [0.4, 1.1], [1.1, 2.1], [2.1, 1.1]]
45+
)
46+
47+
48+
# ======================================================================================
49+
# The guard — a missing image-plane mesh grid fails legibly
50+
# ======================================================================================
51+
52+
53+
@pytest.mark.parametrize(
54+
"mesh_cls", [aa.mesh.Delaunay, aa.mesh.KNearestNeighbor, aa.mesh.KNNBarycentric]
55+
)
56+
def test__adaptive_meshes_raise_when_no_source_plane_mesh_grid_is_given(
57+
mesh_cls, source_plane_data_grid
58+
):
59+
with pytest.raises(exc.MeshException):
60+
mesh_cls(pixels=6).interpolator_from(
61+
source_plane_data_grid=source_plane_data_grid,
62+
source_plane_mesh_grid=None,
63+
)
64+
65+
66+
def test__the_message_names_adapt_images_and_the_mesh__not_just_the_exception_type(
67+
source_plane_data_grid,
68+
):
69+
"""
70+
Asserting on the message is the point of this issue — the old failure raised too,
71+
it just said nothing useful. `adapt_images` is the thing the caller actually passes.
72+
"""
73+
with pytest.raises(exc.MeshException) as error:
74+
aa.mesh.Delaunay(pixels=6).interpolator_from(
75+
source_plane_data_grid=source_plane_data_grid,
76+
source_plane_mesh_grid=None,
77+
)
78+
79+
message = str(error.value)
80+
81+
assert "adapt_images" in message
82+
assert "Delaunay" in message
83+
84+
85+
def test__the_message_points_at_the_workspace_idiom_and_the_rectangular_alternative(
86+
source_plane_data_grid,
87+
):
88+
with pytest.raises(exc.MeshException) as error:
89+
aa.mesh.KNNBarycentric(pixels=6).interpolator_from(
90+
source_plane_data_grid=source_plane_data_grid,
91+
source_plane_mesh_grid=None,
92+
)
93+
94+
message = str(error.value)
95+
96+
assert "galaxy_image_plane_mesh_grid_dict" in message
97+
assert "RectangularUniform" in message
98+
99+
100+
def test__the_failure_is_no_longer_an_attribute_error_on_none(source_plane_data_grid):
101+
"""The original symptom: AttributeError deep inside border_relocator.py."""
102+
with pytest.raises(exc.MeshException):
103+
aa.mesh.Delaunay(pixels=6).interpolator_from(
104+
source_plane_data_grid=source_plane_data_grid,
105+
source_plane_mesh_grid=None,
106+
)
107+
108+
109+
# ======================================================================================
110+
# Controls — the meshes are NOT broken, which is the correction to the headline
111+
# ======================================================================================
112+
113+
114+
@pytest.mark.parametrize(
115+
"mesh_cls", [aa.mesh.Delaunay, aa.mesh.KNearestNeighbor, aa.mesh.KNNBarycentric]
116+
)
117+
def test__control__an_adaptive_mesh_with_a_mesh_grid_still_builds_its_interpolator(
118+
mesh_cls, source_plane_data_grid, source_plane_mesh_grid
119+
):
120+
"""
121+
The headline correction, pinned: supplied with the grid `adapt_images` carries,
122+
these meshes work. If this ever fails, the mesh really is broken and the reply
123+
posted on #332 needs revisiting.
124+
"""
125+
interpolator = mesh_cls(pixels=6).interpolator_from(
126+
source_plane_data_grid=source_plane_data_grid,
127+
source_plane_mesh_grid=source_plane_mesh_grid,
128+
)
129+
130+
assert interpolator is not None
131+
132+
133+
def test__control__the_rectangular_family_needs_no_image_plane_mesh_grid(
134+
source_plane_data_grid,
135+
):
136+
"""
137+
`RectangularUniform` computes its own grid, so the guard must not fire for it —
138+
otherwise the fix would break the one mesh the issue never complained about.
139+
"""
140+
interpolator = aa.mesh.RectangularUniform(shape=(3, 3)).interpolator_from(
141+
source_plane_data_grid=source_plane_data_grid,
142+
source_plane_mesh_grid=None,
143+
)
144+
145+
assert interpolator is not None

0 commit comments

Comments
 (0)