From 309be22619684dbbe146418d0a6779e37c703f10 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 9 Aug 2026 22:32:51 +0000 Subject: [PATCH] fix: name adapt_images when an adaptive mesh has no image-plane grid (#332) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Omitting adapt_images with Delaunay / KNearestNeighbor / KNNBarycentric used to surface as: AttributeError: 'NoneType' object has no attribute 'array' autoarray/inversion/mesh/border_relocator.py, relocated_mesh_grid_from naming nothing the caller controls, in a file they have never opened. It now raises MeshException at the point the precondition is known to be unmet, naming adapt_images, showing the AdaptImages idiom, and noting that the rectangular family needs no adapt_images at all. The guard sits at Delaunay.interpolator_from, the entry point the whole adaptive family inherits, so one check covers all three meshes. Chose fail-fast over having the mesh wire the grid up itself (the reporter's own suggestion). Building an image-plane mesh grid requires a weighting policy — which is exactly what adapt_images carries — so inventing one here would silently make a science choice on the user's behalf. This matches how phase 1 handled the rectangular/split combination: an explicit "you must supply X" rather than implementing a missing capability. THE MESHES ARE NOT BROKEN. The issue headline says Delaunay and KNNBarycentric are "unusable in FitImaging"; that is false and the reply on #332 already corrects it. Verified again here: with adapt_images supplied, Delaunay+Constant, KNNBarycentric+Constant and Delaunay+ConstantSplit all fit, and RectangularUniform+Constant fits with no adapt_images at all. The tests assert the CLEAR FAILURE plus those controls — asserting bare construction succeeds would have enshrined the misreading. One correction to the recorded diagnosis: the None is `mesh_grid` (border_relocator.py:450), not `grid` (line 446) as the prompt stated. Part 2 of #332 (ConstantSplit on RectangularUniform) shipped in phase 1 and is untouched. Tests: 10 new cases in test_autoarray/inversion/pixelization/mesh/test_adapt_images_precondition.py. Suite 991 passed; the 3 pynufft failures in test_transformer.py are pre-existing on main and tracked separately. Closes #332. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_013PgqSCLTemK5bApVAwhVM4 --- autoarray/inversion/mesh/mesh/abstract.py | 45 ++++++ autoarray/inversion/mesh/mesh/delaunay.py | 8 + .../mesh/test_adapt_images_precondition.py | 145 ++++++++++++++++++ 3 files changed, 198 insertions(+) create mode 100644 test_autoarray/inversion/pixelization/mesh/test_adapt_images_precondition.py diff --git a/autoarray/inversion/mesh/mesh/abstract.py b/autoarray/inversion/mesh/mesh/abstract.py index 8399ac51a..bc72279e3 100644 --- a/autoarray/inversion/mesh/mesh/abstract.py +++ b/autoarray/inversion/mesh/mesh/abstract.py @@ -1,6 +1,7 @@ import numpy as np from typing import Optional +from autoarray import exc from autoarray.settings import Settings from autoarray.inversion.mesh.border_relocator import BorderRelocator from autoarray.inversion.regularization.abstract import AbstractRegularization @@ -63,6 +64,50 @@ def relocated_grid_from( xp=xp, ) + def _validate_source_plane_mesh_grid(self, source_plane_mesh_grid): + """ + Raise if the mesh was given no source-plane mesh grid. + + The adaptive meshes (``Delaunay``, ``KNearestNeighbor``, ``KNNBarycentric``) do + not compute their own image-plane mesh grid — it is a required input, supplied + in PyAutoGalaxy / PyAutoLens through ``adapt_images``. Omitting it leaves this + grid ``None`` and the failure previously landed several frames deeper as + ``AttributeError: 'NoneType' object has no attribute 'array'`` inside + ``border_relocator.py``, naming nothing the caller controls and no file the + caller has opened. + + This raises at the point the precondition is known to be unmet, and names + ``adapt_images`` so the message points at the thing the caller actually passes. + + Fail-fast is deliberate rather than having the mesh wire the grid up itself: + constructing an image-plane mesh grid requires a weighting policy (which is + exactly what ``adapt_images`` carries), so inventing one here would silently + pick a science choice on the user's behalf. This matches how the + rectangular-mesh / split-regularization combination was handled — an explicit + "you must supply X" exception rather than implementing a missing capability. + + Parameters + ---------- + source_plane_mesh_grid + The source-plane mesh grid to check. + """ + if source_plane_mesh_grid is None: + raise exc.MeshException( + f"The mesh `{type(self).__name__}` was not given an image-plane mesh " + f"grid, so its source-plane mesh grid is None and the pixelization " + f"cannot be built.\n\n" + f"This mesh does not compute that grid itself — it is a required " + f"input, supplied via `adapt_images`:\n\n" + f" adapt_images = al.AdaptImages(\n" + f" galaxy_image_plane_mesh_grid_dict={{source: image_plane_mesh_grid}}\n" + f" )\n" + f" fit = al.FitImaging(dataset=dataset, tracer=tracer, adapt_images=adapt_images)\n\n" + f"See the `pixelization` feature scripts in the workspace (e.g. " + f"`imaging/features/pixelization/delaunay.py`) for the full idiom. A " + f"mesh in the rectangular family (e.g. `RectangularUniform`) builds " + f"its own grid and needs no `adapt_images`." + ) + def relocated_mesh_grid_from( self, border_relocator: Optional[BorderRelocator], diff --git a/autoarray/inversion/mesh/mesh/delaunay.py b/autoarray/inversion/mesh/mesh/delaunay.py index 1bf818c45..08105cbdb 100644 --- a/autoarray/inversion/mesh/mesh/delaunay.py +++ b/autoarray/inversion/mesh/mesh/delaunay.py @@ -176,6 +176,14 @@ def interpolator_from( adapt_data Not used for a rectangular mesh. """ + # Adaptive meshes require an image-plane mesh grid (supplied via `adapt_images`) + # and do not compute one themselves. Checked here, at the entry point the whole + # adaptive family shares, so a missing precondition names `adapt_images` rather + # than surfacing as an AttributeError on None several frames deeper. + self._validate_source_plane_mesh_grid( + source_plane_mesh_grid=source_plane_mesh_grid + ) + relocated_grid = self.relocated_grid_from( border_relocator=border_relocator, source_plane_data_grid=source_plane_data_grid, diff --git a/test_autoarray/inversion/pixelization/mesh/test_adapt_images_precondition.py b/test_autoarray/inversion/pixelization/mesh/test_adapt_images_precondition.py new file mode 100644 index 000000000..54f43389d --- /dev/null +++ b/test_autoarray/inversion/pixelization/mesh/test_adapt_images_precondition.py @@ -0,0 +1,145 @@ +""" +Regression tests for PyAutoArray#332 — the missing-`adapt_images` precondition. + +__What this issue actually is__ + +The issue's headline says `Delaunay` and `KNNBarycentric` are "unusable in +`FitImaging`". That is **false**, and the public reply on #332 corrects it while +crediting the underlying finding. Both meshes work correctly; they *require* an +image-plane mesh grid, supplied via `adapt_images`. + +So the defect is the **error**, not the mesh. Omitting `adapt_images` used to surface +as: + + AttributeError: 'NoneType' object has no attribute 'array' + autoarray/inversion/mesh/border_relocator.py, in relocated_mesh_grid_from + +— naming nothing the caller controls, in a file they have never opened. + +**These tests therefore assert a CLEAR FAILURE, not a successful fit.** Asserting that +bare construction succeeds would enshrine the reporter's misreading; that trap is +recorded in the prompt for this task and is deliberately avoided here. + +The `adapt_images` branch is exercised at the integration level (a real `FitImaging` +with an `AdaptImages` still fits, for `Delaunay` + `Constant`, `KNNBarycentric` + +`Constant` and `Delaunay` + `ConstantSplit`); these unit tests cover the guard itself +and its controls at the mesh boundary. +""" + +import numpy as np +import pytest + +import autoarray as aa +from autoarray import exc + + +@pytest.fixture(name="source_plane_data_grid") +def make_source_plane_data_grid(): + return aa.Grid2D.uniform(shape_native=(5, 5), pixel_scales=1.0) + + +@pytest.fixture(name="source_plane_mesh_grid") +def make_source_plane_mesh_grid(): + return aa.Grid2DIrregular( + 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]] + ) + + +# ====================================================================================== +# The guard — a missing image-plane mesh grid fails legibly +# ====================================================================================== + + +@pytest.mark.parametrize( + "mesh_cls", [aa.mesh.Delaunay, aa.mesh.KNearestNeighbor, aa.mesh.KNNBarycentric] +) +def test__adaptive_meshes_raise_when_no_source_plane_mesh_grid_is_given( + mesh_cls, source_plane_data_grid +): + with pytest.raises(exc.MeshException): + mesh_cls(pixels=6).interpolator_from( + source_plane_data_grid=source_plane_data_grid, + source_plane_mesh_grid=None, + ) + + +def test__the_message_names_adapt_images_and_the_mesh__not_just_the_exception_type( + source_plane_data_grid, +): + """ + Asserting on the message is the point of this issue — the old failure raised too, + it just said nothing useful. `adapt_images` is the thing the caller actually passes. + """ + with pytest.raises(exc.MeshException) as error: + aa.mesh.Delaunay(pixels=6).interpolator_from( + source_plane_data_grid=source_plane_data_grid, + source_plane_mesh_grid=None, + ) + + message = str(error.value) + + assert "adapt_images" in message + assert "Delaunay" in message + + +def test__the_message_points_at_the_workspace_idiom_and_the_rectangular_alternative( + source_plane_data_grid, +): + with pytest.raises(exc.MeshException) as error: + aa.mesh.KNNBarycentric(pixels=6).interpolator_from( + source_plane_data_grid=source_plane_data_grid, + source_plane_mesh_grid=None, + ) + + message = str(error.value) + + assert "galaxy_image_plane_mesh_grid_dict" in message + assert "RectangularUniform" in message + + +def test__the_failure_is_no_longer_an_attribute_error_on_none(source_plane_data_grid): + """The original symptom: AttributeError deep inside border_relocator.py.""" + with pytest.raises(exc.MeshException): + aa.mesh.Delaunay(pixels=6).interpolator_from( + source_plane_data_grid=source_plane_data_grid, + source_plane_mesh_grid=None, + ) + + +# ====================================================================================== +# Controls — the meshes are NOT broken, which is the correction to the headline +# ====================================================================================== + + +@pytest.mark.parametrize( + "mesh_cls", [aa.mesh.Delaunay, aa.mesh.KNearestNeighbor, aa.mesh.KNNBarycentric] +) +def test__control__an_adaptive_mesh_with_a_mesh_grid_still_builds_its_interpolator( + mesh_cls, source_plane_data_grid, source_plane_mesh_grid +): + """ + The headline correction, pinned: supplied with the grid `adapt_images` carries, + these meshes work. If this ever fails, the mesh really is broken and the reply + posted on #332 needs revisiting. + """ + interpolator = mesh_cls(pixels=6).interpolator_from( + source_plane_data_grid=source_plane_data_grid, + source_plane_mesh_grid=source_plane_mesh_grid, + ) + + assert interpolator is not None + + +def test__control__the_rectangular_family_needs_no_image_plane_mesh_grid( + source_plane_data_grid, +): + """ + `RectangularUniform` computes its own grid, so the guard must not fire for it — + otherwise the fix would break the one mesh the issue never complained about. + """ + interpolator = aa.mesh.RectangularUniform(shape=(3, 3)).interpolator_from( + source_plane_data_grid=source_plane_data_grid, + source_plane_mesh_grid=None, + ) + + assert interpolator is not None