From 143ab43e1ecbb647166f11c1795acffdbd91d33c Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 18 Aug 2026 01:36:04 +0000 Subject: [PATCH] fix: repair ConstantZeroth dead code (eye(P) shape bug + missing neighbors_sizes) Two independent defects made al.reg.ConstantZeroth unusable through its class API (issue #448, found in the wsdev#104 reg-logdet investigation): - constant_zeroth_regularization_matrix_from built the zeroth-order term as eye(P) where P is the neighbor-column count, not S the mesh pixel count, so `const + zeroth` raised a broadcast ValueError whenever S != P. The zeroth term is now the full S x S scaled identity, matching zeroth.py's semantics. - ConstantZeroth.regularization_matrix_from omitted the required neighbors_sizes argument, raising TypeError before the shape bug was even reached. It now threads linear_obj.neighbors.sizes, mirroring Constant. Siblings adapt_split_zeroth.py and brightness_zeroth.py were checked and do not carry the pattern (their zeroth terms are diag over per-pixel weights). Adds numpy-only unit tests: matrix shape (S, S) with S != P, no null mode (smallest eigenvalue >= lambda_z^2), reduction to Constant's matrix + lambda_z^2 * I, and the class-API path returning without raising. Constant/Adapt/AdaptSplit numerics untouched. Co-Authored-By: Claude Claude-Session: https://claude.ai/code/session_013frRFAHyKN1EpE8sQHTogB --- .../regularization/constant_zeroth.py | 3 +- .../regularizations/test_constant_zeroth.py | 97 +++++++++++++++++++ 2 files changed, 99 insertions(+), 1 deletion(-) create mode 100644 test_autoarray/inversion/regularizations/test_constant_zeroth.py diff --git a/autoarray/inversion/regularization/constant_zeroth.py b/autoarray/inversion/regularization/constant_zeroth.py index f11f75a0..d156318a 100644 --- a/autoarray/inversion/regularization/constant_zeroth.py +++ b/autoarray/inversion/regularization/constant_zeroth.py @@ -68,7 +68,7 @@ class in the module ``autoarray.inversion.regularization``. reg_coeff = coefficient_zeroth**2.0 # Identity matrix scaled by reg_coeff does exactly ∑_i reg_coeff * e_i e_i^T - zeroth = xp.eye(P) * reg_coeff + zeroth = xp.eye(S) * reg_coeff return const + zeroth @@ -122,5 +122,6 @@ def regularization_matrix_from(self, linear_obj: LinearObj, xp=np) -> np.ndarray coefficient=self.coefficient_neighbor, coefficient_zeroth=self.coefficient_zeroth, neighbors=linear_obj.neighbors, + neighbors_sizes=linear_obj.neighbors.sizes, xp=xp, ) diff --git a/test_autoarray/inversion/regularizations/test_constant_zeroth.py b/test_autoarray/inversion/regularizations/test_constant_zeroth.py new file mode 100644 index 00000000..e8404c87 --- /dev/null +++ b/test_autoarray/inversion/regularizations/test_constant_zeroth.py @@ -0,0 +1,97 @@ +import autoarray as aa +import numpy as np +import pytest + +from autoarray.inversion.regularization.constant_zeroth import ( + constant_zeroth_regularization_matrix_from, +) + + +def test__regularization_matrix_from__shape_is_pixels_by_pixels(): + # 3-pixel chain (0-1, 0-2) with P=2 neighbor columns, so S != P and the + # zeroth term must be built from S, not the neighbor count. + neighbors = np.array([[1, 2], [0, -1], [0, -1]]) + neighbors_sizes = np.array([2, 1, 1]) + + regularization_matrix = constant_zeroth_regularization_matrix_from( + coefficient=1.0, + coefficient_zeroth=2.0, + neighbors=neighbors, + neighbors_sizes=neighbors_sizes, + ) + + assert regularization_matrix.shape == (3, 3) + + +def test__regularization_matrix_from__no_null_mode__smallest_eigenvalue_at_least_coefficient_zeroth_squared(): + neighbors = np.array( + [[1, 3, -1, -1], [0, 2, -1, -1], [1, 3, -1, -1], [0, 2, -1, -1]] + ) + neighbors_sizes = np.array([2, 2, 2, 2]) + + coefficient_zeroth = 2.0 + + regularization_matrix = constant_zeroth_regularization_matrix_from( + coefficient=1.0, + coefficient_zeroth=coefficient_zeroth, + neighbors=neighbors, + neighbors_sizes=neighbors_sizes, + ) + + eigenvalues = np.linalg.eigvalsh(regularization_matrix) + + assert np.min(eigenvalues) >= coefficient_zeroth**2.0 + + +def test__regularization_matrix_from__reduces_to_constant_matrix_plus_scaled_identity(): + neighbors = np.array( + [[1, 3, -1, -1], [0, 2, -1, -1], [1, 3, -1, -1], [0, 2, -1, -1]] + ) + neighbors_sizes = np.array([2, 2, 2, 2]) + + coefficient = 3.0 + coefficient_zeroth = 2.0 + + constant_matrix = aa.util.regularization.constant_regularization_matrix_from( + coefficient=coefficient, + neighbors=neighbors, + neighbors_sizes=neighbors_sizes, + ) + + constant_zeroth_matrix = constant_zeroth_regularization_matrix_from( + coefficient=coefficient, + coefficient_zeroth=coefficient_zeroth, + neighbors=neighbors, + neighbors_sizes=neighbors_sizes, + ) + + assert constant_zeroth_matrix == pytest.approx( + constant_matrix + coefficient_zeroth**2.0 * np.identity(4), 1.0e-8 + ) + + +def test__regularization_matrix_from__class_api_returns_without_raising(): + reg = aa.reg.ConstantZeroth(coefficient_neighbor=2.0, coefficient_zeroth=3.0) + + source_plane_mesh_grid = aa.Grid2D.no_mask( + values=[[0.1, 0.1], [1.1, 0.6], [2.1, 0.1], [0.4, 1.1], [1.1, 7.1], [2.1, 1.1]], + shape_native=(3, 2), + pixel_scales=1.0, + ) + + mesh_geometry = aa.MeshGeometryRectangular( + mesh=aa.mesh.RectangularUniform(shape=(3, 3)), + mesh_grid=source_plane_mesh_grid, + data_grid=None, + ) + + mapper = aa.m.MockMapper( + source_plane_mesh_grid=source_plane_mesh_grid, mesh_geometry=mesh_geometry + ) + + regularization_matrix = reg.regularization_matrix_from(linear_obj=mapper) + + assert regularization_matrix.shape == (9, 9) + # Constant leg diagonal (8.0000001 for coefficient 2, see test_constant.py) + # plus the zeroth term coefficient_zeroth**2 = 9. + assert regularization_matrix[0, 0] == pytest.approx(17.0000001, 1.0e-4)