Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions autoarray/inversion/mesh/mesh/abstract.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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],
Expand Down
8 changes: 8 additions & 0 deletions autoarray/inversion/mesh/mesh/delaunay.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Loading